2015 年 10 月

第 30 卷,第 10 期

本文章是由機器翻譯。

Windows with C++ - Visual C++ 2015 中的 Coroutine

Kenny Kerr |2015 年 10 月

我第一次學習 c + + 中的共同常式年代 2012年並撰寫了一系列的 MSDN magazine 》 文章這裡的概念。我會探討輕量型合作多工作業播放聰明技巧 switch 陳述式來模擬共同常式的表單。然後我討論了一些工作來改善效率和複合性的使用建議的延伸模組來承諾與 future 的非同步系統。最後,我涵蓋即使有工具了預見未來功能以及提案,所謂繼續函式存在一些挑戰。我建議您閱讀這些如果您感興趣的一些挑戰和簡潔的並行處理的 c + + 與相關的記錄:

撰寫的大部分是理論上,因為沒有編譯器實作任何一種概念而且已模擬它們以各種方式。和 Visual Studio 2015 然後出貨今年初。此版本的 Visual c + + 包含一個名為實驗編譯器選項 / await 可直接由編譯器所支援的共同常式的實作會解除鎖定。沒有更多的駭客攻擊、 巨集或其他神奇。這是真實的事物是實驗和由 c + + 委員會尚未 unsanctioned。而且不在編譯器前端,像是您使用 C# yield 關鍵字和非同步方法中所找到的只是語法捷徑。C + + 實作包含深入的工程投資編譯器後端提供提供超強延展性的實作。事實上,勝什麼您可能會發現是否編譯器前端只是提供更方便的語法使用承諾和 future 或甚至是並行執行階段工作類別。讓我們重新瀏覽主題和看到這對今天的外觀。許多已經變更自 2012 年開始我將說明扼要說明來說明我們得從以及我們的之前查看一些更具體的範例和實際使用的位置。

我歸納出上述數列吸引人的範例為可繼續函式,因此我先那里從。假設有一組用於從檔案讀取和寫入網路連線的資源:

struct File
{
  unsigned Read(void * buffer, unsigned size);
};
struct Network
{
  void Write(void const * buffer, unsigned size);
};

您可以使用您的想像力來填入其餘部分,但這是相當代表傳統同步 I/O 的樣子。檔案的讀取方法會嘗試從目前的檔案位置到最大尺寸緩衝區讀取資料並會傳回實際的複製的位元組數目。如果傳回的值小於所要求的大小,通常表示已經到達檔案結尾。網路類別建立模型的一般連線導向通訊協定如 TCP 或具名管道的 Windows。Write 方法會將特定的位元組數目複製到網路堆疊。一般同步的複製作業是很容易想像得到,但我會協助您處理 [圖 1 ,讓您在需要參照。

[圖 1 同步的複製作業

File file = // Open file
Network network = // Open connection
uint8_t buffer[4096];
while (unsigned const actual = file.Read(buffer, sizeof(buffer)))
{
  network.Write(buffer, actual);
}

Read 方法會傳回某個值大於零,因為產生的位元組會從中繼緩衝區複製到使用寫入方法的網路。這是任何合理的程式設計人員必須沒有問題了解,不論其背景的程式碼種類。當然,Windows 提供可以卸載作業完全避免轉換的所有核心這類的服務但這些服務僅限於特定案例和這是代表應用程式通常與繫結上的封鎖作業的種類。

C + + 標準程式庫提供 future 和承諾,為了支援非同步作業,但它們已經由於其貝氏設計更 maligned。我在 2012年討論這些問題。即使怎麼也發掘這些問題中的檔案-網路複製範例重寫 [圖 1 很可觀。最直接的轉譯的同步 (簡單) while 迴圈需要可以放心離開 future 鏈結仔細手動反覆項目演算法:

template <typename F>
future<void> do_while(F body)
{
  shared_ptr<promise<void>> done = make_shared<promise<void>>();
  iteration(body, done);
  return done->get_future();
}

此演算法就能在反覆項目函式:

template <typename F>
void iteration(F body, shared_ptr<promise<void>> const & done)
{
  body().then([=](future<bool> const & previous)
  {
    if (previous.get()) { iteration(body, done); }
    else { done->set_value(); }
  });
}

Lambda 必須擷取共用的承諾值,因為這真的是反覆而不是遞迴。但因為這表示一對每個反覆項目的連鎖作業是有問題。此外,未來還沒有鏈結接續的"then"方法雖然您無法模擬這立即與並行執行階段工作類別。還是假設這類工具了演算法和接續存在,我可以重寫同步的複製作業從 [圖 1 以非同步方式。我必須先將非同步多載加入至檔案和網路類別。可能是像這樣:

struct File
{
  unsigned Read(void * buffer, unsigned const size);
  future<unsigned> ReadAsync(void * buffer, unsigned const size);
};
struct Network
{
  void Write(void const * buffer, unsigned const size);
  future<unsigned> WriteAsync(void const * buffer, unsigned const size)
};

WriteAsync 方法的未來必須回應已複製的位元組數目,因為這是所有這些任何接續可能有才能決定是否要終止反覆項目。另一個選項可能是檔案類別提供 EndOfFile 方法。在任何情況下,提供這些新的基本項目,複製作業可以用來表示以了解如果您已經 imbibed 徹夜足夠數量的方式。[圖 2 說明此方法。

[圖 2 與未來的複製作業

File file = // Open file
Network network = // Open connection
uint8_t buffer[4096];
future<void> operation = do_while([&]
{
  return file.ReadAsync(buffer, sizeof(buffer))
    .then([&](task<unsigned> const & read)
    {
      return network.WriteAsync(buffer, read.get());
    })
    .then([&](task<unsigned> const & write)
    {
      return write.get() == sizeof(buffer);
    });
});
operation.get();

Do_while 演算法幫助您只要在迴圈的 「 主體 」 就會傳回 true 接續鏈結。因此會呼叫 ReadAsync,WriteAsync,做為迴圈條件測試其結果是會使用其結果。這不是火箭科學般複雜,但有沒有想要這類的撰寫程式碼。它故意很快會變得太複雜而有關的原因。輸入可繼續函式。

加入 / await 編譯器選項可讓編譯器支援可繼續的函式的 c + + 的共同常式的實作。因為他們要的行為與其多 like 傳統 c + + 函式盡可能之所以稱為可繼續函式而不是只是共同常式。確實不像我曾在 2012年回,某些函式的取用者不應該要知道是否,其實根本實作為 coroutine 擁有。

撰寫本文時 / await 編譯器選項也必須 /Zi 選項而不是預設 /ZI 選項以停用偵錯工具的編輯後繼續 」 功能。您也必須停用與 /sdl-option SDL 檢查並避免編譯器的執行階段檢查與不相容共同常式 /RTC 選項。所有這些限制是暫時性的因為實作的實驗性的本質和我會希望他們消除在未來的更新給編譯器。但很值得,您可以看到在 [圖 3。這是簡單明瞭 unquestionably 更容易撰寫和更容易了解比為何需要實作與未來的複製作業。事實上,它看起來很像中的原始同步範例 [圖 1。另外還有不需要在此情況下為 WriteAsync 未來傳回特定的值。

[圖 3 繼續函式中的複製作業

future<void> Copy()
{
  File file = // Open file
  Network network = // Open connection
  uint8_t buffer[4096];
  while (unsigned copied = await file.ReadAsync(buffer, sizeof(buffer)))
  {
    await network.WriteAsync(buffer, copied);
  }
}

使用中的 await 關鍵字 [圖 3, 以及其他 /await 編譯器選項所提供的新關鍵字可以只會出現在可繼續函式,因此傳回未來的周圍複製函式。我使用相同的 ReadAsync 和 WriteAsync 方法 future 上例中,但請務必了解編譯器並不知道未來一無所知。事實上,它們不需要完全是 future。那麼要如何運作? 其實它無法運作除非特定配接器函式會寫入至編譯器提供必要的繫結。這是怎麼接通尋找適當的範圍架構 for 陳述式的編譯器數字開頭和結尾的函式的方式類似。在 await 運算式,而不是尋求開始和結束時,編譯器會尋找適合的函式呼叫 await_ready、 await_suspend 和 await_resume。像開始和結束,這些新的函式可能是成員函式或免費的函式。然後您可以撰寫配接器提供所需語意的現有類型在此情況下使用工具了我探討了到目前為止的未來寫入非成員函式的能力會極大的差異有幫助。[圖 4 提供一組配接器會滿足繼續函式中的編譯器解譯 [圖 3

[圖 4 假設未來 Await 配接器

namespace std
{
  template <typename T>
  bool await_ready(future<T> const & t)
  {
    return t.is_done();
  }
  template <typename T, typename F>
  void await_suspend(future<T> const & t, F resume)
  {
    t.then([=](future<T> const &)
    {
      resume();
    });
  }
  template <typename T>
  T await_resume(future<T> const & t)
  {
    return t.get();
  }
}

同樣地,請記住的 c + + 標準程式庫的未來類別樣板還不能提供"then"方法將接續,但僅此而已花費若要讓此範例使用現今的編譯器。Await 關鍵字可繼續函式中的有效地設定一個潛在的暫止的時間點位置執行可能會導致函式如果作業尚未完成。如果 await_ready 傳回 true,然後暫停執行不是和 await_resume 則會立即呼叫以取得結果。如果手動,await_ready 會傳回 false,會呼叫 await_suspend,允許註冊編譯器提供繼續函式呼叫的最終完成作業。只要呼叫該繼續函式、 共同常式繼續先前暫停點並繼續執行到下一個 await 運算式或函式的終止。

請記住繼續發生在任何執行緒呼叫編譯器繼續函式。這表示它是很有可能繼續函式可以開始在一個執行緒上的存留期和之後繼續和另一個執行緒上繼續執行。這是從效能觀點來看其實理想的替代方案就表示分派到另一個執行緒,通常是昂貴又不需要繼續。相反地,可能情況會是理想和甚至需要任何後續的程式碼應該有執行緒相似性與大部分的圖形程式碼的情況會。不幸的是,await 關鍵字還沒有一種方法讓書的作者 await 運算式提供這類編譯器的提示。這不是沒有優先順序。並行執行階段沒有這類選項,但有趣的是,c + + 語言本身提供您可遵循的模式:

int * p = new int(1);
// Or
int * p = new (nothrow) int(1);

同樣地,在 await 運算式需要某種機制,提供提示給 await_suspend 函式會影響繼續發生的執行緒內容:

await network.WriteAsync(buffer, copied);
// Or
await (same_thread) network.WriteAsync(buffer, copied);

根據預設,繼續進行中最有效率的方式作業。Same_thread 一些假設 std::same_thread_t 類型常數會釐清 await_suspend 函式的多載。在 await_suspend [圖 3 會是預設值和最有效率的選項,因為想必會在背景工作執行緒上繼續執行而不需要進一步的內容切換完成。Same_thread 多載所示 [圖 5 可能會要求時取用者要求執行緒相似性。

[圖 5 假設 await_suspend 多載

template <typename T, typename F>
void await_suspend(future<T> const & t, F resume, same_thread_t const &)
{
  ComPtr<IContextCallback> context;
  check(CoGetObjectContext(__uuidof(context),
    reinterpret_cast<void **>(set(context))));
  t.then([=](future<T> const &)
  {
    ComCallData data = {};
    data.pUserDefined = resume.to_address();
    check(context->ContextCallback([](ComCallData * data)
    {
      F::from_address(data->pUserDefined)();
      return S_OK;
    },
    &data,
    IID_ICallbackWithNoReentrancyToApplicationSTA,
    5,
    nullptr));
  });
}

這個多載擷取 IContextCallback 介面呼叫的執行緒 (或 apartment)。接續然後最後呼叫編譯器繼續函式從這個相同的內容。如果發生這種情況是應用程式的 STA,應用程式可能值得高興的是繼續執行緒相似性與其他服務互動。ComPtr 類別範本並檢查 helper 函式是您可以從下載的現代程式庫的一部分 github.com/kennykerr/modern, ,但是您也可以使用任何可能手上。

討論過很多,其中有些仍然是有點理論上,但是 Visual c + + 編譯器已提供所有困難的工作來進行這項作業。它是適用於 c + + 開發人員興趣並行令人興奮的時間和我希望您會跟我一次下個月我深入 Visual c + + 可繼續函式。


Kenny Kerr是電腦程式設計人員在加拿大和作者基礎 Pluralsight 和 Microsoft MVP。他的部落格網址 kennykerr.ca 以及您可以在 Twitter 上追隨他 @kennykerr

感謝以下的微軟技術專家對本文的審閱: Gor Nishanov