欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

C++協(xié)程實現(xiàn)序列生成器的案例分享

 更新時間:2024年05月28日 09:27:32   作者:膚淺的羊  
序列生成器通常的實現(xiàn)是在一個協(xié)程內(nèi)部通過某種方式向外部傳一個值出去,并且將自己掛起,本文圍繞序列生成器這個經(jīng)典的協(xié)程案例介紹了協(xié)程的銷毀、co_await 運算符、await_transform 以及 yield_value 的用法,需要的朋友可以參考下

目標

序列生成器通常的實現(xiàn)是在一個協(xié)程內(nèi)部通過某種方式向外部傳一個值出去,并且將自己掛起,外部調(diào)用者則可以獲取到這個值,并且在后續(xù)繼續(xù)恢復(fù)執(zhí)行序列生成器來獲取下一個值。

掛起和向外部傳值的任務(wù)就需要通過co_await來完成,外部獲取值的任務(wù)就要通過協(xié)程的返回值來完成。

程序大概可以寫成下面這樣:

Generator sequence() {
  int i = 0;
  while (true) {
    co_await i++;
  }
}

int main() {
  auto generator = sequence();
  for (int i = 0; i < 10; ++i) {
    std::cout << generator.next() << std::endl;
  }
}

注意到 generator 有個 next 函數(shù),調(diào)用它時我們需要想辦法讓協(xié)程恢復(fù)執(zhí)行,并將下一個值傳出來。

好了,接下來我們就帶著這兩個問題去尋找解決辦法,順便把剩下的一點點 C++ 協(xié)程的知識補齊。

調(diào)用者獲取協(xié)程產(chǎn)生的值

我們觀察main函數(shù)中的這段代碼:

int main() {
  auto generator = sequence();
  for (int i = 0; i < 10; ++i) {
    std::cout << generator.next() << std::endl;
  }
}

generator 的類型就是我們即將實現(xiàn)的序列生成器類型 Generator,結(jié)合上一篇文章當(dāng)中對于協(xié)程返回值類型的介紹,我們先大致給出它的定義:

struct Generator {
  struct promise_type {
    
    // 開始執(zhí)行時直接掛起等待外部調(diào)用 resume 獲取下一個值
    std::suspend_always initial_suspend() { return {}; };

    // 執(zhí)行結(jié)束后不需要掛起
    std::suspend_never final_suspend() noexcept { return {}; }

    // 為了簡單,我們認為序列生成器當(dāng)中不會拋出異常,這里不做任何處理
    void unhandled_exception() { }

    // 構(gòu)造協(xié)程的返回值類型
    Generator get_return_object() {
      return Generator{};
    }

    // 沒有返回值
    void return_void() { }
  };

  int next() {
    ???.resume();
    return ???;
  }
};

代碼當(dāng)中有兩處我們標注為 ???,表示暫時還不知道怎么處理。

如果我們想要在Generatorresume協(xié)程的話,需要拿到coroutine_handle,要如何做到這一點呢?

這個時候可以記住一點,promise_type是鏈接協(xié)程內(nèi)外的橋梁,想要拿到什么,找promise_type要。標準庫提供了一個通過promise_type的對象的地址獲取coroutine_handle的函數(shù),它實際上是coroutine_handle的一個靜態(tài)函數(shù):

template <class _Promise>
struct coroutine_handle {
    static coroutine_handle from_promise(_Promise& _Prom) noexcept {
      ...
    }

    ...
}

這樣看來的話,我們只需要在get_return_object函數(shù)調(diào)用時,先獲取coroutine_handle,然后再傳給即將構(gòu)造出來的Generator就行,因此我們稍微修改一下前面的代碼:

struct Generator {
  struct promise_type {
    ...

    // 構(gòu)造協(xié)程的返回值類型
    Generator get_return_object() {
      return Generator{ std::coroutine_handle<promise_type>::from_promise(*this) };
    }

    ...
  };

  std::coroutine_handle<promise_type> handle;

  int next() {
    handle.resume();
    return ???;
  }

};

接下來就是如何獲取協(xié)程內(nèi)部傳出來的值的問題了。同樣,本著有事找promise_type的原則,我們可以直接給它定義一個value成員:

struct Generator {
  struct promise_type {
    int value;

    ...
  };

  std::coroutine_handle<promise_type> handle;

  int next() {
    handle.resume();
    // 通過 handle 獲取 promise,然后再取到 value
    return handle.promise().value;
  }
};

協(xié)程內(nèi)部掛起并傳值

現(xiàn)在的問題就是如何從協(xié)程內(nèi)部傳值給promise_type了。

我們再觀察一下最終實現(xiàn)的效果:

Generator sequence() {
  int i = 0;
  while (true) {
    co_await i++;
  }
}

特別需要注意的是co_await i++;這一句,我們發(fā)現(xiàn)co_await后面的是一個整型值,而不是我們在前面文章中提到的滿足等待體(awaiter)條件的類型,這種情況下該怎么辦呢?

實際上,對于co_await <expr>表達式中expr的處理,C++有一套完善的流程:

  • 如果promise_type當(dāng)中定義了await_transform函數(shù),那么先通過promise.await_transform(expr)來對expr做一次轉(zhuǎn)換,得到的對象則為awaitable;否則awaitable就是expr本身。
  • 接下來使用awaitable對象獲取等待體(awaiter)。如果awaitable對象有operator co_await運算符重載,那么等待體就是operator co_await(awaitable),否則等待體就是awaitable對象本身。

按照上面的說法,我們要么給promise_type實現(xiàn)一個await_transform(int)函數(shù),要么就為整型實現(xiàn)一個operator co_await的運算符重載,二者選一個就可以了。

方案 1:實現(xiàn) operator co_await

這個方案就是給 int 定義 operator co_await 的重載:

auto operator co_await(int value) {
  struct IntAwaiter {
    int value;

    bool await_ready() const noexcept {
      return false;
    }
    void await_suspend(std::coroutine_handle<Generator::promise_type> handle) const {
      handle.promise().value = value;
    }
    void await_resume() {  }
  };
  return IntAwaiter{.value = value};
}

當(dāng)然,這個方案對于我們這個特定的場景下是行不通的,因為在 C++ 當(dāng)中我們是無法給基本類型定義運算符重載的。

不過,如果我們遇到的情況不是基本類型,那么運算符重載的思路就可以行得通。operator co_await 的重載我們將會在后面給出例子。

方案 2:await_transform

運算符重載行不通,那就只能通過 await_tranform 來做轉(zhuǎn)換了。

代碼比較簡單:

struct Generator {
  struct promise_type {
    int value;

    // 傳值的同時要掛起,值存入 value 當(dāng)中
    std::suspend_always await_transform(int value) {
      this->value = value;
      return {};
    }

    ...
  };

  std::coroutine_handle<promise_type> handle;

  int next() {
    handle.resume();

  // 外部調(diào)用者或者恢復(fù)者可以通過讀取 value
    return handle.promise().value;
  }
};

定義了 await_transform 函數(shù)之后,co_await expr 就相當(dāng)于 co_await promise.await_transform(expr) 了。

至此,我們的例子就可以運行了:

Generator sequence() {
  int i = 0;
  while (true) {
    co_await i++;
  }
}

int main() {
  auto gen = sequence();
  for (int i = 0; i < 5; ++i) {
    std::cout << gen.next() << std::endl;
  }
}

運行結(jié)果如下:

0
1
2
3
4

協(xié)程的銷毀

雖然我們的協(xié)程已經(jīng)能夠正常工作,但它仍然存在一些問題。

問題1: 無法確定是否存在下一個元素

當(dāng)外部調(diào)用者或者恢復(fù)者試圖調(diào)用 next 來獲取下一個元素的時候,它其實并不知道能不能真的得到一個結(jié)果。程序也可能拋出異常:

如下例:

Generator sequence() {
  int i = 0;
  // 只傳出 5 個值
  while (i < 5) {
    co_await i++;
  }
}

int main() {
  auto gen = sequence();
  for (int i = 0; i < 15; ++i) {
    // 試圖讀取 15 個值
    std::cout << gen.next() << std::endl;
  }
  return 0;
}

程序的結(jié)果是什么呢?

0
1
2
3
4
4

Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

最后一個輸出的 4 實際上是恰好遇到協(xié)程銷毀之前的狀態(tài),此時 promise 當(dāng)中的 value 值還是之前的 4。而當(dāng)我們試圖不斷的去讀取協(xié)程的值,程序就拋出 SIGSEGV 的錯誤。錯誤的原因你可能已經(jīng)想到了,當(dāng)協(xié)程體執(zhí)行完之后,協(xié)程的狀態(tài)就會被銷毀,如果我們再訪問協(xié)程的話,就相當(dāng)于訪問了一個野指針。

為了解決這個問題,我們需要增加一個 has_next 函數(shù),用來判斷是否還有新的值傳出來,has_next 函數(shù)調(diào)用的時候有兩種情況:

  • 已經(jīng)有一個值傳出來了,還沒有被外部消費
  • 還沒有現(xiàn)成的值可以用,需要嘗試恢復(fù)執(zhí)行協(xié)程來看看還有沒有下一個值傳出來

這里我們需要有一種有效的辦法來判斷 value 是不是有效的,單憑 value 本身我們其實是無法確定它的值是不是被消費了,因此我們需要加一個值來存儲這個狀態(tài):

struct Generator {

  // 協(xié)程執(zhí)行完成之后,外部讀取值時拋出的異常
  class ExhaustedException: std::exception { };

  struct promise_type {
    int value;
    bool is_ready = false;
    ...
  }
  ...
}

我們定義一個成員 state 來記錄協(xié)程執(zhí)行的狀態(tài),狀態(tài)的類型一共三種,只有 READY 的時候我們才能拿到值。

接下來改造 next 函數(shù),同時增加 has_next 函數(shù)來描述協(xié)程是否仍然可以有值傳出:

struct Generator {
  ...

  bool has_next() {
    // 協(xié)程已經(jīng)執(zhí)行完成
    if (handle.done()) {
      return false;
    }

    // 協(xié)程還沒有執(zhí)行完成,并且下一個值還沒有準備好
    if (!handle.promise().is_ready) {
      handle.resume();
    }

    if (handle.done()) {
      // 恢復(fù)執(zhí)行之后協(xié)程執(zhí)行完,這時候必然沒有通過 co_await 傳出值來
      return false;
    } else {
      return true;
    }
  }

  int next() {
    if (has_next()) {
      // 此時一定有值,is_ready 為 true 
      // 消費當(dāng)前的值,重置 is_ready 為 false
      handle.promise().is_ready = false;
      return handle.promise().value;
    }
    throw ExhaustedException();
  }
};

注意,我們在promise_type中產(chǎn)生新值后需要將is_ready置為true如下:

struct Generator {
  ...

  struct promise_type {
    ...

    std::suspend_always await_transform(T value) {
      this->value = value;
      is_ready = true;
      return {};
    }
    
  };
};

這樣外部使用時就需要先通過 has_next 來判斷是否有下一個值,然后再去讀取了:

...

int main() {
  auto generator = sequence();
  for (int i = 0; i < 15; ++i) {
    if (generator.has_next()) {
      std::cout << generator.next() << std::endl;
    } else {
      break;
    }
  }
  return 0;
}

問題2:協(xié)程狀態(tài)的銷毀比Generator對象的銷毀更早

我們前面提到過,協(xié)程的狀態(tài)在協(xié)程體執(zhí)行完之后就會銷毀,除非協(xié)程掛起在 final_suspend 調(diào)用時。

我們的例子當(dāng)中 final_suspend 返回了 std::suspend_never,因此協(xié)程的銷毀時機其實比 Generator 更早:

auto generator = sequence();
for (int i = 0; i < 15; ++i) {
  if (generator.has_next()) {
    std::cout << generator.next() << std::endl;
  } else {
    // 協(xié)程已經(jīng)執(zhí)行完,協(xié)程的狀態(tài)已經(jīng)銷毀
    break;
  }
}

// generator 對象在此仍然有效

這看上去似乎問題不大,因為我們在前面通過 has_next 的判斷保證了讀取值的安全性。

但實際上情況并非如此。我們在 has_next 當(dāng)中調(diào)用了 coroutine_handle::done 來判斷協(xié)程體是否執(zhí)行完成,判斷之前很可能協(xié)程已經(jīng)銷毀,coroutine_handle 這時候都已經(jīng)是無效的了:

bool has_next() {
  // 如果協(xié)程已經(jīng)執(zhí)行完成,理論上協(xié)程的狀態(tài)已經(jīng)銷毀,handle 指向的是一個無效的協(xié)程
  // 如果 handle 本身已經(jīng)無效,因此 done 函數(shù)的調(diào)用此時也是無效的
  if (handle.done()) {
    return false;
  }
  ...
}

因此為了讓協(xié)程的狀態(tài)的生成周期與 Generator 一致,我們必須將協(xié)程的銷毀交給 Generator 來處理:

struct Generator {

  class ExhaustedException: std::exception { };

  struct promise_type {
    ...

    // 總是掛起,讓 Generator 來銷毀
    std::suspend_always final_suspend() noexcept { return {}; }

    ...
  };

  ...

  ~Generator() {
    // 銷毀協(xié)程
    handle.destroy();
  }
};

問題3:復(fù)制對象導(dǎo)致協(xié)程被銷毀

這個問題確切地說是問題 2的解決方案不完善引起的。

我們在 Generator 的析構(gòu)函數(shù)當(dāng)中銷毀協(xié)程,這本身沒有什么問題,但如果我們把 Generator 對象做一下復(fù)制,例如從一個函數(shù)當(dāng)中返回,情況可能就會變得復(fù)雜。例如:

Generator returns_generator() {
  auto g = sequence();
  if (g.has_next()) {
    std::cout << g.next() << std::endl;
  }
  return g;
}

int main() {
  auto generator = returns_generator();
  for (int i = 0; i < 15; ++i) {
    if (generator.has_next()) {
      std::cout << generator.next() << std::endl;
    } else {
      break;
    }
  }
  return 0;
}

這段代碼乍一看似乎沒什么問題,但由于我們把 g 當(dāng)做返回值返回了,這時候 g 這個對象就發(fā)生了一次復(fù)制,然后臨時對象被銷毀。接下來的事兒大家就很容易想到了,運行結(jié)果如下:

0
-572662307

Process finished with exit code -1073741819 (0xC0000005)

為了解決這個問題,我們需要妥善地處理 Generator 的復(fù)制構(gòu)造器:

struct Generator {
  ...

  explicit Generator(std::coroutine_handle<promise_type> handle) noexcept
      : handle(handle) {}

  Generator(Generator &&generator) noexcept
      : handle(std::exchange(generator.handle, {})) {}

  Generator(Generator &) = delete;
  Generator &operator=(Generator &) = delete;

  ~Generator() {
    if (handle) handle.destroy();
  }
}

我們只提供了右值復(fù)制構(gòu)造器,對于左值復(fù)制構(gòu)造器,我們直接刪除掉以禁止使用。原因也很簡單,對于每一個協(xié)程實例,都有且僅能有一個 Generator 實例與之對應(yīng),因此我們只支持移動對象,而不支持復(fù)制對象。

使用co_yield

序列生成器這個需求的實現(xiàn)其實有個更好的選擇,那就是使用 co_yieldco_yield 就是專門為向外傳值來設(shè)計的,如果大家對其他語言的協(xié)程有了解,也一定見到過各種 yield 的實現(xiàn)。

C++ 當(dāng)中的 co_yield expr 等價于 co_await promise.yield_value(expr),我們只需要將前面例子當(dāng)中的 await_transform 函數(shù)替換成 yield_value 就可以使用 co_yield 來傳值了:

struct Generator {

  class ExhaustedException: std::exception { };

  struct promise_type {
    ...

    // 將 await_transform 替換為 yield_value
    std::suspend_always yield_value(int value) {
      this->value = value;
      is_ready = true;
      return {};
    }
    ...
  };
  ...
};

Generator sequence() {
  int i = 0;
  while (i < 5) {
    // 使用 co_yield 來替換 co_await
    co_yield i++;
  }
}

可以看到改動點非常少,運行效果與前面的例子一致。

盡管可以實現(xiàn)相同的效果,但通常情況下我們使用 co_await 更多的關(guān)注點在掛起自己,等待別人上,而使用 co_yield 則是掛起自己傳值出去。因此我們應(yīng)該針對合適的場景做出合適的選擇。

使用序列生成器生成fibonacci數(shù)列

接下來我們要使用序列生成器來實現(xiàn)一個更有意義的例子,即斐波那契數(shù)列。

Generator fibonacci() {
  co_yield 0; // fib(0)
  co_yield 1; // fib(1)

  int a = 0;
  int b = 1;
  while(true) {
    co_yield a + b; // fib(N), N > 1
    b = a + b;
    a = b - a;
  }
}

我們看到這個實現(xiàn)非常的直接,完全不需要考慮 fib(N - 1) 和 fib(N - 2) 的存儲問題。

如果沒有協(xié)程,我們的實現(xiàn)可能是這樣的:

class Fibonacci {
 public:
  int next() {
    // 初值不符合整體的規(guī)律,需要單獨處理
    if (a == -1){
      a = 0;
      b = 1;
      return 0;
    }

    int next = b;
    b = a + b;
    a = b - a;
    return next;
  }

 private:
  int a = -1;
  int b = 0;
};

使用時先構(gòu)造一個 Fibonacci 對象,然后調(diào)用 next 函數(shù)來獲取下一個值。對比之下,協(xié)程的實現(xiàn)帶來的好處是顯而易見的。協(xié)程生成的這個序列是個懶序列(Lazy Sequence),沒用到的項就不會被生成。

總結(jié)

本文圍繞序列生成器這個經(jīng)典的協(xié)程案例介紹了協(xié)程的銷毀、co_await 運算符、await_transform 以及 yield_value 的用法。

以上就是C++協(xié)程實現(xiàn)序列生成器的案例分享的詳細內(nèi)容,更多關(guān)于C++序列生成器的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Qt實現(xiàn)將qsqlite數(shù)據(jù)庫中的數(shù)據(jù)導(dǎo)出為Excel表格

    Qt實現(xiàn)將qsqlite數(shù)據(jù)庫中的數(shù)據(jù)導(dǎo)出為Excel表格

    這篇文章主要為大家詳細介紹了如何通過Qt實現(xiàn)將qsqlite數(shù)據(jù)庫中的數(shù)據(jù)導(dǎo)出為Excel表格,文中的示例代碼簡潔易懂,有需要的小伙伴可以了解一下
    2024-12-12
  • C/C++實現(xiàn)蛇形矩陣的示例代碼

    C/C++實現(xiàn)蛇形矩陣的示例代碼

    本文主要介紹了C/C++實現(xiàn)蛇形矩陣的示例代碼,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • C/C++ 左移<<, 右移>>的作用及說明

    C/C++ 左移<<, 右移>>的作用及說明

    這篇文章主要介紹了C/C++ 左移<<, 右移>>的作用及說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-07-07
  • C指針原理教程之C快速入門

    C指針原理教程之C快速入門

    C語言作為大學(xué)編程或者計算機專業(yè)的一門必修課,把很多初學(xué)編程的小伙伴都難住了,感覺無從下手,今天呢,我們來簡單介紹下,如何快速入門C語言
    2019-02-02
  • C++ seekg函數(shù)用法案例詳解

    C++ seekg函數(shù)用法案例詳解

    這篇文章主要介紹了C++ seekg函數(shù)用法案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下
    2021-08-08
  • C++中成員函數(shù)和友元函數(shù)的使用及區(qū)別詳解

    C++中成員函數(shù)和友元函數(shù)的使用及區(qū)別詳解

    大家好,本篇文章主要講的是C++中成員函數(shù)和友元函數(shù)的使用及區(qū)別詳解,感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下
    2022-01-01
  • C++中std::tuple和std::pair的高級用法

    C++中std::tuple和std::pair的高級用法

    本文主要介紹了C++標準庫中std::pair和std::tuple的使用,包括它們的基本概念、使用場景、區(qū)別以及高級用法,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2024-11-11
  • C++ 中 socket編程實例詳解

    C++ 中 socket編程實例詳解

    這篇文章主要介紹了C++ 中 socket編程實例詳解的相關(guān)資料,需要的朋友可以參考下
    2017-06-06
  • C語言入門篇--關(guān)鍵字static詳解

    C語言入門篇--關(guān)鍵字static詳解

    本篇文章是C語言系列基礎(chǔ)篇,C語言中,static是用來修飾變量和函數(shù):1.修飾局部變量–>靜態(tài)局部變量2.修飾全局變量–>靜態(tài)全局變量3.修飾函數(shù)–>靜態(tài)函數(shù)
    2021-08-08
  • C++實現(xiàn)LeetCode(118.楊輝三角)

    C++實現(xiàn)LeetCode(118.楊輝三角)

    這篇文章主要介紹了C++實現(xiàn)LeetCode(118.楊輝三角),本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下
    2021-07-07

最新評論