在異步操作里,如異步連接、異步讀寫之類的協(xié)程,co_await這些協(xié)程時(shí)需要注意線程切換的細(xì)節(jié)。
以asio異步連接協(xié)程為例:
classclient{
public:
client(){
thd_=std::thread([this]{
io_ctx_.run();
});
}
async_simple::Lazyasync_connect(autohost,autoport){
boolret=co_awaitutil::async_connect(host,port);#1
co_returnret;#2
}
~client(){
io_ctx_.stop();
if(thd_.joinable()){
thd_.join();
}
}
private:
asio::io_contextio_ctx_;
std::threadthd_;
};
intmain(){
clientc;
async_simple::syncAwait(c.async_connect());
std::cout<<"quit
";#3
}
這個(gè)例子很簡(jiǎn)單,client在連接之后就析構(gòu)了,看起來沒什么問題。但是運(yùn)行之后就會(huì)發(fā)生線程join的錯(cuò)誤,錯(cuò)誤的意思是在線程里join自己了。這是怎么回事?co_await一個(gè)異步連接的協(xié)程,當(dāng)連接成功后協(xié)程返回,這時(shí)候發(fā)生了線程切換。異步連接返回的時(shí)候是在io_context的線程里,代碼中的#1在主線程,#2在io_context線程,之后就co_return 返回到main函數(shù)的#3,這時(shí)候#3仍然在io_context線程里,接著client就會(huì)析構(gòu)了,這時(shí)候仍然在io_context線程里,析構(gòu)的時(shí)候會(huì)調(diào)用thd_.join(); 然后就導(dǎo)致了在io_context的線程里join自己的錯(cuò)誤。
這是使用協(xié)程時(shí)容易犯錯(cuò)的一個(gè)地方,解決方法就是避免co_await回來之后去析構(gòu)client,或者co_await回來仍然回到主線程。這里可以考慮用協(xié)程條件變量,在異步連接的時(shí)候發(fā)起一個(gè)新的協(xié)程并傳入?yún)f(xié)程條件變量并在連接返回后set_value,主線程去co_await這個(gè)條件變量,這樣連接返回后就回到主線程了,就可以解決在io線程里join自己的問題了。
還是以上面的異步連接為例子,需要對(duì)之前的async_connect協(xié)程增加一個(gè)超時(shí)功能,代碼稍作修改:
classclient{
public:
client():socket_(io_ctx_){
thd_=std::thread([this]{
io_ctx_.run();
});
}
async_simple::Lazyasync_connect(autohost,autoport,autoduration){
coro_timertimer(io_ctx_);
timeout(timer,duration).start([](auto&&){});//#1啟動(dòng)一個(gè)新協(xié)程做超時(shí)處理
boolret=co_awaitutil::async_connect(host,port,socket_);//假設(shè)這里co_await返回后回到主線程
co_returnret;
}
~client(){
io_ctx_.stop();
if(thd_.joinable()){
thd_.join();
}
}
private:
async_simple::Lazytimeout(auto&timer,autoduration){
boolis_timeout=co_awaittimer.async_wait(duration);
if(is_timeout){
asio::error_codeignored_ec;
socket_.shutdown(tcp::shutdown_both,ignored_ec);
socket_.close(ignored_ec);
}
co_return;
}
asio::io_contextio_ctx_;
tcp::socketsocket_;
std::threadthd_;
boolis_timeout_;
};
intmain(){
clientc;
async_simple::syncAwait(c.async_connect("localhost","9000",5s));
std::cout<<"quit
";#3
}
這個(gè)代碼增加連接超時(shí)處理的協(xié)程,注意#1那里為什么需要新啟動(dòng)一個(gè)協(xié)程,而不能用co_await呢?因?yàn)閏o_await是阻塞語義,co_await會(huì)導(dǎo)致永遠(yuǎn)超時(shí),啟動(dòng)一個(gè)新的協(xié)程不會(huì)阻塞當(dāng)前協(xié)程從而可以去調(diào)用async_connect。
當(dāng)timeout超時(shí)發(fā)生時(shí)就關(guān)閉socket,這時(shí)候async_connect就會(huì)返回錯(cuò)誤然后返回到調(diào)用者,這看起來似乎可以對(duì)異步連接做超時(shí)處理了,但是這個(gè)代碼是有問題的。假如異步連接沒有超時(shí)會(huì)發(fā)生什么?沒有超時(shí)的話就返回到main函數(shù)了,然后client就析構(gòu)了,當(dāng)timeout協(xié)程resume回來的時(shí)候client其實(shí)已經(jīng)析構(gòu)了,這時(shí)候再去調(diào)用成員變量socket_ close將會(huì)導(dǎo)致一個(gè)訪問已經(jīng)析構(gòu)對(duì)象的錯(cuò)誤。
也許有人會(huì)說,那就在co_return之前去取消timer不就好了嗎?這個(gè)辦法也不行,因?yàn)槿∠鹴imer,timeout協(xié)程并不會(huì)立即返回,仍然會(huì)存在訪問已經(jīng)析構(gòu)對(duì)象的問題。
正確的做法應(yīng)該是對(duì)兩個(gè)協(xié)程進(jìn)行同步,timeout協(xié)程和async_connect協(xié)程需要同步,在async_connect協(xié)程返回之前需要確保timeout協(xié)程已經(jīng)完成,這樣就可以避免訪問已經(jīng)析構(gòu)對(duì)象的問題了。
這個(gè)問題其實(shí)也是異步回調(diào)安全返回的一個(gè)經(jīng)典問題,協(xié)程也同樣會(huì)遇到這個(gè)問題,上面提到的對(duì)兩個(gè)協(xié)程進(jìn)行同步是解決方法之一,另外一個(gè)方法就是使用shared_from_this,就像異步安全回調(diào)那樣處理。
還是以異步連接為例:
async_simple::Lazyasync_connect(conststd::string&host,conststd::string&port){
co_returnco_awaitutil::async_connect(host,port);
}
async_simple::Lazytest_connect(){
boolok=co_awaitasync_connect("localhost","8000");
if(!ok){
std::cout<<"connectfailed
";
}
std::cout<<"connectok
";
}
intmain(){
async_simple::syncAwait(test_connect());
}
這個(gè)代碼簡(jiǎn)單明了,就是測(cè)試一下異步連接是否成功,運(yùn)行也是正常的。如果稍微改一下test_connect:
async_simple::Lazytest_connect(){
autolazy=async_connect("localhost","8000");
boolok=co_awaitlazy;
if(!ok){
std::cout<<"connectfailed
";
}
std::cout<<"connectok
";
}
很遺憾,這個(gè)代碼會(huì)導(dǎo)致連接總是失敗,似乎很奇怪,后面發(fā)現(xiàn)原因是因?yàn)閍sync_connect的兩個(gè)參數(shù)失效了,但是寫法和剛開始的寫法幾乎一樣,為啥后面這種寫法會(huì)導(dǎo)致參數(shù)失效呢?
原因是co_await一個(gè)協(xié)程函數(shù)時(shí),其實(shí)做了兩件事:
-
調(diào)用協(xié)程函數(shù)創(chuàng)建協(xié)程,這個(gè)步驟會(huì)創(chuàng)建協(xié)程幀,把參數(shù)和局部變量拷貝到協(xié)程幀里;
-
co_await執(zhí)行協(xié)程函數(shù);
回過頭來看auto lazy = async_connect("localhost", "8000"); 這個(gè)代碼調(diào)用協(xié)程函數(shù)創(chuàng)建了協(xié)程,這時(shí)候拷貝到協(xié)程幀里面的是兩個(gè)臨時(shí)變量,在這一行結(jié)束的時(shí)候臨時(shí)變量就析構(gòu)了,在下一行去co_await執(zhí)行這個(gè)協(xié)程的時(shí)候就會(huì)出現(xiàn)參數(shù)失效的問題了。
co_await async_connect("localhost", "8000"); 這樣為什么沒問題呢,因?yàn)閰f(xié)程創(chuàng)建和協(xié)程調(diào)用都在一行完成的,臨時(shí)變量知道協(xié)程執(zhí)行之后才會(huì)失效,因此不會(huì)有問題。
問題的本質(zhì)其實(shí)是C++臨時(shí)變量生命周期的問題。使用協(xié)程的時(shí)候稍微注意一下就好了,可以把const std::string&改成std::string,這樣就不會(huì)臨時(shí)變量生命周期的問題了,如果不想改參數(shù)類型就co_await 協(xié)程函數(shù)就好了,不分成兩行去執(zhí)行協(xié)程。
審核編輯 :李倩
-
變量
+關(guān)注
關(guān)注
0文章
613瀏覽量
28306 -
線程
+關(guān)注
關(guān)注
0文章
503瀏覽量
19634
原文標(biāo)題:C++ 使用協(xié)程需要注意的問題
文章出處:【微信號(hào):程序喵大人,微信公眾號(hào):程序喵大人】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論