共用方式為


本文章是由機器翻譯。

Windows 使用 c + +

透過可取回的函式回到未來

Kenny Kerr

 

Kenny Kerr
我結束我的最後一列 (msdn.microsoft.com/magazine/jj618294) 通過突出顯示一些可能的改進,到 C + + 11 期貨和承諾,將從很大程度上學術和簡單,實際和有用建設的高效率和可組合的非同步系統對其進行轉換。在很大程度上這被鼓舞 Niklas Gustafsson 和阿圖爾 · Laksberg 從 Visual c + + 團隊的工作。

作為未來的期貨的代表性,我給了這些線沿線的示例:

 

int main()
{
  uint8 b[1024];
  auto f = storage_read(b, sizeof(b), 0).then([&]()
  {
    return storage_write(b, sizeof(b), 1024);
  });
  f.wait();
}

Storage_read 和 storage_write 函數返回一個代表可能在未來某個時刻完成各自的 I/O 操作的未來。 這些函數模型 1 KB 頁與一些儲存子系統。 整個程式從存儲中的第一頁讀取到緩衝區,然後將它複製回存儲的第二頁。 此示例的新奇是使用假設"然後"方法添加到未來類,允許讀取和寫入操作,然後可以對其無縫地等待為單個邏輯 I/O 操作組成。

這是一個巨大的改進,對世界的堆疊翻錄我所述我最後一列,但本身就是我我 8 月 2012年列,c +"輕型合作多工處理與 +"中描述的語言支援一個類似 coroutine 的設施仍不完全實現的烏托邦的夢想 (msdn.microsoft.com/magazine/jj553509)。 在該列中我成功地演示了一些戲劇性宏詭計如何才能實現這種設施 — — 但不是能沒有重大的缺點,主要涉及到無法使用的本地變數。 這個月我想分享一些想法如何,這可能會實現在 c + + 語言本身。

我一定開始本系列文章探索替代技術可以實現併發與實際的解決辦法,因為現實情況是我們需要的解決方案,今天的工作。 我們做的不過,需要,展望未來,迫使 c + + 社區轉發,要求更多更自然和更富有成效的方式寫入 I/O 密集型應用程式的支援。 當然寫作高度可擴展的系統不應該的 JavaScript 和 C# 程式師和足夠的意志力與罕見的 c + + 程式師的專屬管轄範圍。 另外,請記住這不只是方便和程式設計語法和樣式的優雅。 有多個活動在任何給定時間的 I/O 請求的能力有可能顯著地提高性能。 存儲和網路驅動程式旨在規模以及在飛行中有更多的 I/O 請求。 在存儲驅動程式的情況下請求可以組合以提高硬體緩衝並降低搜尋時間。 在網路驅動程式的情況下更多的請求是指較大的網路資料包、 優化滑動視窗操作及更多。

我要切換齒輪略來說明如何快速的複雜性抬頭。 而不從一個存放裝置只需讀取和寫入和,如何提供檔的內容通過網路連接嗎? 和以前一樣,我從開始同步的方法,並從那裡工作。 電腦可能從根本上是非同步但我們是凡人當然不是。 不了解你,但從來沒有過著很多的出發點。 以下列類別為例:

class file { uint32 read(void * b, uint32 s); };
class net { void write(void * b, uint32 s); };

使用你的想像力來填寫其他。 我只被需要允許從一些檔讀取的位元組數的一定數量的檔類。 我會進一步認為檔物件將跟蹤的偏移量。 同樣,淨類可能模型通過其滑動視窗的實現一定隱藏的調用方通過 TCP 處理資料偏移位置的 TCP 流。 由於各種原因,或許與相關的緩存或爭用,檔閱讀方法可能不會始終返回實際請求的位元組數。 只有它將,然而,返回零時已到達檔的末尾。 淨 write 方法是工作的更簡單,因為 TCP 實現中,根據設計,幸好不龐大,以保持這簡單的調用方。 這是基本的假想方案,但相當代表性的 OS I/O。 我現在可以編寫以下簡單的程式:

int main()
{
  file f = ...; net n = ...; uint8 b[4096];
  while (auto actual = f.read(b, sizeof(b)))
  {
    n.write(b, actual);
  }
}

鑒於 10 KB 的檔,你可以想像下面的迴圈耗盡之前的事件序列:

read 4096 bytes -> write 4096 bytes ->
read 4096 bytes -> write 4096 bytes ->
read 2048 bytes -> write 2048 bytes ->
read 0 bytes

像我的最後一列中的同步示例,不難弄清楚怎麼在這裡,因為 c + + 的順序存取特性。交換器成為非同步組成是有點困難。第一步是要轉換的檔和淨類返回期貨:

class file { future<uint32> read(void * b, uint32 s); };
class net { future<void> write(void * b, uint32 s); };

這是比較容易的部分。 要充分利用這些方法中的任何不同步的主函數重寫的幾個挑戰。 它不再足夠使用未來的假設"然後"方法,因為我不再只在處理順序組成。 是的真的寫如下一讀,但只,如果實際讀取重新置放的東西。 若要使問題複雜化進一步,讀也遵循寫在所有情況下。 你可能會忍不住要想到封鎖,但這一概念涵蓋組成的狀態和行為和不行為和其他行為的組成。

我可以首先創建關閉僅為讀取和寫入操作:

auto read = [&]() { return f.read(b, sizeof(b)); };
auto write = [&](uint32 actual) { n.write(b, actual); };

當然,這相當不能工作,因為未來的然後方法,不知道什麼要傳遞給寫函數:

read().then(write);

要解決這個問題,我需要某種將允許期貨轉發狀態的公約 》。 一個明顯的選擇 (或許) 是轉發本身的未來。 然後方法然後期望將採取適當的類型,允許我寫這未來參數的運算式:

auto read = [&]() { return f.read(b, sizeof(b)); };
auto write = [&](future<uint32> previous) { n.write(b, 
  previous.get()); };
read().then(write);

這工作原理,以及我甚至可能希望改進可組合性進一步通過定義,然後方法預期的運算式還應該返回一個未來。 然而,這個問題依然是如何表達條件迴圈。 最終,這證明是更簡單,相反,作為 do...while 迴圈重新考慮原始迴圈,因為這是反覆運算的方式表達更容易。 有條件地連結期貨和使基於 <bool> 的未來的結果結束反覆運算組成可以然後制定要以非同步方式,模仿這種模式的 do_while 演算法 值,例如:

future<void> do_while(function<future<bool>()> body)
{
  auto done = make_shared<promise<void>>();
  iteration(body, done);
  return done->get_future();  
}

Do_while 函數首先創建一個引用計數的承諾,其最終的未來信號迴圈的終止。 這被傳遞到反覆運算功能和代表迴圈的主體的功能:

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

此反覆運算函數是心臟的 do_while 演算法,提供從一個調用連結到下一個,以及爆發和信號完成工作的能力。雖然看上去可能遞迴,但請記住整點是單獨從堆疊的非同步作業,因此在迴圈並不實際增長堆疊。使用 do_while 演算法是相對容易,並且我現在可以編寫中顯示的程式圖 1

圖 1 使用 do_while 演算法

int main()
{
  file f = ...; net n = ...; uint8 b[4096];
  auto loop = do_while([&]()
  {
    return f.read(b, sizeof(b)).then([&](future<uint32> previous)
    {
      return n.write(b, previous.get());
    }).then([&]()
    {
      promise<bool> p;
      p.set_value(!f.eof);
      return p.get_future();
    });
  });
  loop.wait();
}

Do_while 函數自然返回一個未來,和在這種情況下它等待後,但這很容易本來可以避免通過將主要函數的區域變數存儲與 shared_ptrs 堆上。 內傳遞給 do_while 函數的 lambda 運算式,讀取的操作開始,其次是寫操作。 為使此示例簡單,我假設這寫將立即返回,如果它曾告訴寫零位元組。 當寫入操作完成後時,我檢查該檔的檔結束狀態,並返回一個提供迴圈條件值的未來。 這可確保將重複迴圈的主體,直到用盡了該檔的內容。

儘管此代碼不是特別令人討厭 — — 事實上,是可以說比堆疊翻錄很乾淨,並 — — 有點支援的語言會走很長的路。 Niklas Gustafsson 已建議這種設計並稱之為"可恢復功能"。期貨和承諾的基礎上改進擬議並添加少許句法糖,我可以編寫一個可恢復的函數來封裝的令人驚訝的複雜的非同步作業,如下所示:

future<void> file_to_net(shared_ptr<file> f, 
  shared_ptr<net> n) resumable
{
  uint8 b[4096];
  while (auto actual = await f->read(b, sizeof(b)))
  {
    await n->write(b, actual);
  }
}

這種設計的好處就是,代碼具有相似,初始同步版本,並且屬於什麼我在尋找,畢竟。 請注意"可恢復"上下文關鍵字之後,該函數的參數清單。 這是類似于我我 8 月 2012年列中所描述的假想的"非同步"關鍵字。 不像我在該列中說明的事情,但是,這將由執行編譯器本身。 因此將無併發症和我面對宏執行的限制。 您可以使用 switch 語句以及本地變數 — — 和建構函式和析構函數將按預期工作 — — 但您功能現在可以暫停和繼續以類似于我原型與宏的方式。 不但如此,但你將擺脫捕獲區域變數只是為了讓他們超出範圍,一個常見的錯誤時使用 lambda 運算式的陷阱。 編譯器會照顧為堆上的可恢復函數內的本地變數提供的存儲。

在前面的示例還會通知"等待"關鍵字之前,讀取和編寫方法調用。 此關鍵字定義的復原點,並期望未來類似的物件,它可用於確定是否要暫停和恢復以後是否只是繼續執行,如果碰巧同步完成非同步作業導致的運算式。 顯然,為了實現最佳的性能,我需要處理非同步作業的完成同步,也許由於快取記憶體太常見方案或快速故障情形。

請注意我說,等待關鍵字預計未來類似的物件。 嚴格地說,沒有任何理由,它需要一個實際的未來物件。 它只需要提供必要的行為,以支援非同步完成和信號轉導的檢測。 這是今天的範本工作的方式類似。 這類似于未來的物件將需要支援我的最後一列所示的然後方法以及現有的 get 方法。 為了提高性能,結果立即可用的情況下,擬議的 try_get 和 is_done 方法也會很有用。 當然,編譯器可以優化基於這種方法的可用性。

這並不是像有些牽強,也許看起來。 C# 已有形式的非同步方法,可恢復功能的道德等效的幾乎相同設施。 它甚至提供了等待的關鍵字,我已經說明了在相同的方式工作。 我的希望是 c + + 社區會擁抱可恢復的功能,或者類似,使到我們所有能夠編寫高效、 可組合的非同步系統自然和輕鬆。

可恢復功能的詳細分析,包括看看如何可能實現它們,請閱讀 Niklas Gustafsson 紙張,"可恢復功能,"在 bit.ly/zvPr0a

Kenny Kerr 是充滿熱情的本機 Windows 開發的軟體工匠。他在聯繫 kennykerr.ca

由於以下的技術專家對本文的審閱:阿圖爾 · Laksberg