1、條件變量
當線程需要等待特定事件發(fā)生、或是某個條件成立時,可以使用條件變量std::condition_variable
,它在標準庫頭文件<condition_variable>
內(nèi)聲明。
std::mutex mut;
std::queue<data_chunk> data_queue;
std::condition_variable data_cond;
void data_preparation_thread()
{
while (more_data_to_prepare())
{
const data_chunk data = prepare_data();
std::lock_guard<std::mutex> lk(mut);
data_queue.push(data);
data_cond.notify_one();
}
}
void data_processing_thread()
{
while (true)
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk, [] { return !data_queue.empty(); });
data_chunk data = data_queue.front();
data_queue.pop();
lk.unlock();
process(data);
if (is_last_chunk(data)) { break; }
}
}
wait()
會先在內(nèi)部調(diào)用lambda函數(shù)判斷條件是否成立,若條件成立則wait()
返回,否則解鎖互斥并讓當前線程進入等待狀態(tài)。當其它線程調(diào)用notify_one()
時,當前調(diào)用wait()
的線程被喚醒,重新獲取互斥鎖并查驗條件,若條件成立則wait()
返回(互斥仍被鎖?。?,否則解鎖互斥并繼續(xù)等待。
wait()
函數(shù)的第二個參數(shù)可以傳入lambda函數(shù),也可以傳入普通函數(shù)或可調(diào)用對象,也可以不傳。
notify_one()
喚醒正在等待當前條件的線程中的一個,如果沒有線程在等待,則函數(shù)不執(zhí)行任何操作,如果正在等待的線程多于一個,則喚醒的線程是不確定的。notify_all()
喚醒正在等待當前條件的所有線程,如果沒有正在等待的線程,則函數(shù)不執(zhí)行任何操作。
2、使用future等待一次性事件發(fā)生
C++標準程序庫有兩種future
,分別由兩個類模板實現(xiàn),即std::future<>
和std::shared_future<>
,它們的聲明位于頭文件<future>
內(nèi)。
2.1、從后臺任務返回值
由于std::thread
沒有提供直接回傳結(jié)果的方法,所以我們使用函數(shù)模板std::async()
來解決這個問題。std::async()
以異步方式啟動任務,并返回一個std::future
對象,運行函數(shù)一旦完成,其返回值就由該對象持有。在std::future
對象上調(diào)用get()
方法時,當前線程就會阻塞,直到std::future
準備妥當并返回異步線程的結(jié)果。std::future
模擬了對異步結(jié)果的獨占行為,get()
僅能被有效調(diào)用一次,調(diào)用時會對目標值進行移動操作。
int find_the_answer_to_ltuae();
void do_other_stuff();
int main()
{
std::future<int> the_answer = std::async(find_the_answer_to_ltuae);
do_other_stuff();
std::cout << "The answer is " << the_answer.get() << std::endl;
}
在調(diào)用std::async()
時,它可以接收附加參數(shù)進而傳遞給任務函數(shù)作為其參數(shù),此方式與std::thread
的構(gòu)造函數(shù)相同。更多啟動異步線程的方法可參考下面的例程:
struct X
{
void foo(int, const std::string&);
std::string bar(const std::string&);
};
X x;
auto f1 = std::async(&X::foo, &x, 42, "hello"); // 調(diào)用p->foo(42, "hello"),p是指向x的指針
auto f2 = std::async(&X::bar, x, "goodbye"); // 調(diào)用tmpx.bar("goodbye"), tmpx是x的拷貝副本
struct Y
{
double operator()(double);
};
Y y;
auto f3 = std::async(Y(), 3.141); // 調(diào)用tmpy(3.141),tmpy是由Y()生成的匿名變量
auto f4 = std::async(std::ref(y), 2.718); // 調(diào)用y(2.718)
X baz(X&);
std::async(baz, std::ref(x)); // 調(diào)用baz(x)
我們還能為std::async()
補充一個std::launch
類型的參數(shù),來指定采用哪種方式運行:std::launch::deferred
指定在當前線程上延后調(diào)用任務函數(shù),等到在future
上調(diào)用了wait()
或get()
,任務函數(shù)才會執(zhí)行;std::launch::async
指定必須開啟專屬的線程,在其上運行任務函數(shù)。該參數(shù)的還可以是std::launch::deferred | std::launch::async
,表示由std::async()
的實現(xiàn)自行選擇運行方式,這也是這項參數(shù)的默認值。
auto f6 = std::async(std::launch::async, Y(), 1.2); // 在新線程上執(zhí)行
auto f7 = std::async(std::launch::deferred, baz, std::ref(x)); // 在wait()或get()調(diào)用時執(zhí)行
auto f8 = std::async(std::launch::deferred | std::launch::async, baz, std::ref(x)); // 交由實現(xiàn)自行選擇執(zhí)行方式
auto f9 = std::async(baz, std::ref(x));
f7.wait(); // 調(diào)用延遲函數(shù)
2.2、關(guān)聯(lián)future實例和任務
std::packaged_task<>
連結(jié)future對象與函數(shù)(或可調(diào)用對象,下同)。std::packaged_task<>
對象在執(zhí)行任務時,會調(diào)用關(guān)聯(lián)的函數(shù),把返回值保存為future的內(nèi)部數(shù)據(jù),并令future準備就緒。若一項龐雜的操作能分解為多個子任務,則可以把它們分別包裝到多個std::packaged_task<>
實例之中,再傳遞給任務調(diào)度器或線程池,這就隱藏了細節(jié),使任務抽象化,讓調(diào)度器得以專注處理std::packaged_task<>
實例,無需糾纏于形形色色的任務函數(shù)。
std::packaged_task<>
是類模板,其模板參數(shù)是函數(shù)簽名(例如void()
表示一個函數(shù),不接收參數(shù),也沒有返回值),傳入的函數(shù)必須與之相符,即它應接收指定類型的參數(shù),返回值也必須可以轉(zhuǎn)換成指定類型。這些類型不必嚴格匹配,若某函數(shù)接收int
類型參數(shù)并返回float
值,則可以為其構(gòu)建std::packaged_task<double(double)>
的實例,因為對應的類型可以隱式轉(zhuǎn)換。
std::packaged_task<>
具有成員函數(shù)get_future()
,它返回std::future<>
實例,該future的特化類型取決于函數(shù)簽名指定的返回值。std::packaged_task<>
還具備函數(shù)調(diào)用操作符,它的參數(shù)取決于函數(shù)簽名的參數(shù)列表。
std::mutex m;
std::deque<std::packaged_task<void()>> tasks;
bool gui_shutdown_message_received();
void get_and_process_gui_message();
void gui_thread()
{
while (!gui_shutdown_message_received())
{
get_and_process_gui_message();
std::packaged_task<void()> task;
{
std::lock_guard<std::mutex> lk(m);
if (tasks.empty()) { continue; }
task = std::move(tasks.front());
tasks.pop_front();
}
task();
}
}
std::thread gui_bg_thread(gui_thread);
template<typename Func>
std::future<void> post_task_for_gui_thread(Func f)
{
std::packaged_task<void()> task(f);
std::future<void> res = task.get_future();
std::lock_guard<std::mutex> lk(m);
tasks.push_back(std::move(task));
return res;
}
2.3、創(chuàng)建std::promise
有些任務無法以簡單的函數(shù)調(diào)用表達出來,還有一些任務的執(zhí)行結(jié)果可能來自多個部分的代碼,這時可以借助std::promise
顯式地異步求值。配對的std::promise
和std::future
可以實現(xiàn)下面的工作機制:等待數(shù)據(jù)的線程在future上阻塞,而提供數(shù)據(jù)的線程利用相配的std::promise
設(shè)定關(guān)聯(lián)的值,使future準備就緒。
若需從給定的std::promise
實例獲取關(guān)聯(lián)的std::future
對象,調(diào)用前者的成員函數(shù)get_future()
即可,這與std::package_task
一樣。promise的值通過成員函數(shù)set_value()
設(shè)置,只要設(shè)置好,future即準備就緒,憑借它就能獲取該值。如果std::promise
在被銷毀時仍未曾設(shè)置值,保存的數(shù)據(jù)則由異常代替。
void f(std::promise<int> ps)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
ps.set_value(42);
}
int main()
{
std::promise<int> ps;
std::future<int> ft = ps.get_future();
std::thread t(f, std::move(ps));
int val = ft.get();
std::cout << val << std::endl;
t.join();
}
2.4、將異常保存到future中
若經(jīng)由std::async()
調(diào)用的函數(shù)拋出異常,則會被保存到future中,future隨之進入就緒狀態(tài),等到其成員函數(shù)get()
被調(diào)用,存儲在內(nèi)的異常即被重新拋出。std::packaged_task
也是同理,若包裝的任務函數(shù)在執(zhí)行時拋出異常,則也會被保存到future中,只要調(diào)用get()
,該異常就會被再次拋出。自然而然,std::promise
也具有同樣的功能,它通過成員函數(shù)顯式調(diào)用實現(xiàn)。假如我們不想保存值,而想保存異常,就不應調(diào)用set_value()
,而應調(diào)用成員函數(shù)set_exception()
。
2.5、多個線程一起等待
若我們在多個線程上訪問同一個std::future
對象,而不采取額外的同步措施,將引發(fā)數(shù)據(jù)競爭并導致未定義的行為。std::future
僅能移動構(gòu)造和移動賦值,而std::shared_future
的實例則能復制出副本。但即便改用std::shared_future
,同一個對象的成員函數(shù)卻依然沒有同步,若我們從多個線程訪問同一個對象,首選方式是:向每個線程傳遞std::shared_future
對象的副本,它們?yōu)楦骶€程獨有,這些副本就作為各線程的內(nèi)部數(shù)據(jù),由標準庫正確地同步,可以安全地訪問。
future和promise都具備成員函數(shù)valid()
,用于判別異步狀態(tài)是否有效。std::shared_future
的實例依據(jù)std::future
的實例構(gòu)造而得,前者所指向的異步狀態(tài)由后者決定。因為std::future
對象獨占異步狀態(tài),所以若要按默認方式構(gòu)造std::shared_future
對象,則須用std::move
向其默認構(gòu)造函數(shù)傳遞歸屬權(quán)。
std::promise<int> p;
std::future<int> f(p.get_future());
assert(f.valid());
std::shared_future<int> sf(std::move(f));
assert(!f.valid());
assert(sf.valid());
std::future
具有成員函數(shù)share()
,直接創(chuàng)建新的std::shared_future
對象,并向它轉(zhuǎn)移歸屬權(quán)。文章來源:http://www.zghlxwxcb.cn/news/detail-694008.html
std::promise<std::map<SomeIndexType, SomeDataType, SomeComparator, SomeAllocator>::iterator> p;
auto sf = p.get_future().share();
3、限時等待
有兩種超時機制可供選擇:一是延遲超時,線程根據(jù)指定的時長而繼續(xù)等待;二是絕對超時,在某個特定時間點來臨之前,線程一直等待。大部分等待函數(shù)都有變體,專門處理這兩種機制的超時。處理延遲超時的函數(shù)變體以_for
為后綴,而處理絕對超時的函數(shù)變體以_until
為后綴。例如std::condition_variable
的成員函數(shù)wait_for()
和wait_until()
。文章來源地址http://www.zghlxwxcb.cn/news/detail-694008.html
到了這里,關(guān)于《C++并發(fā)編程實戰(zhàn)》讀書筆記(3):并發(fā)操作的同步的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!