C++多任務編程簡明教程 (1) - C++的多任務其實很簡單
C++多任務編程簡明教程 (1) - C++的多任務其實很簡單
用庫的方式無法實現徹底的線程安全!我們需要C++11
與很多同學交流的時候發現,一想到用C++寫多線程,還是想到pthread這樣的庫的方法實現。
但是,十幾年前的研究就證明了,線程安全是無法用庫的形勢來提供的,有興趣的同學可以參見原文:
https://www.hpl.hp.com/techreports/2004/HPL-2004-209.pdf
解釋需要大量的篇幅,作為快餐式的教程,我們隻講結論。
十幾年過去了,CPU的亂序執行,編譯器的亂序優化,使得通過pthread一樣的庫實現線程安全越來越困難。即使做到了,也是以犧牲性能為代價換來的。
我們以java為例,雖然在java 1.0中就支持多線程了。但是,真正完整的線程安全的基礎設施的完成是在java 5.0版本中才實現的。這其中最重要的一點,就是java 5.0明確定義了內存模型。有了內存模型的保證,我們才真正知道,什麼是可行的,什麼是不可行的。如何定義才能在多線程中實在可見性。
在有較大影響的語言中,java是率先達到了這一目標的。C++在當時基本上沒有為多線程或者多任務做過考慮,這個問題直到C++11中才被解決掉。
所以,至少對於多線程編程來講,C++11是必須要學習的。我們不能再停留在C++98/03的老黃曆上了!
所幸的是,對於最基本的C++11多任務編程來講,比起完整學會pthread這樣的庫還要容易.
多任務優於多線程
然後,我們從C++11的線程開始講?不,我寧願把整個內存模型講完了之後才教您如何使用線程,就怕學完了線程沒學內存模型就上手直接開始用了,導致遇到問題之後才知道內存模型的重要性。
但是,這還不是我最想講的,為什麼要學線程和內存模型呢?大部分的任務,我們根本不需要了解什麼是線程,什麼是鎖,更不用說無鎖編程之類的了。看過我的《Java多線程編程簡明教程(1) - Future模式與AsyncTask》都知道,如果有更簡單、更高級的封裝,我是不建議先學更低級的工具的。
為什麼需要多線程?其實很多同學沒有意識到,需要的隻是多任務,線程隻是手段。線程是操作係統級的概念,是操作係統級的資源,我們要管理到這麼細節,就得去處理比如管理線程狀態,處理線程數滿了的異常,如何做線程的負載均衡之類的管理工作,而這些根業務邏輯之間沒有半毛錢關係。
從java的發展來看,5.0提供了Future模式,7.0中提供了Fork-Join模式,封裝得越來越高級。C++11中暫時還沒有到Fork-Join這麼高級,不過,直接調用一個std::async函數就可以實現Future模式.
所謂的Future模式,就是在後台起一個任務,然後前台該幹嘛幹嘛,等到前台任務需要用到剛才的後台任務的時候,再去獲取後台任務的結果。如果後台任務已經結束了,自然是最好,繼續幹活就是了。即使後台還沒做完,至少也能少等一會兒。不管怎麼樣都不賠。
std::async 快餐
C++11如何去做一個後台任務呢,實在太簡單了,寫個函數,或者仿函數,或者是lambda表達式這樣可以被調用的邏輯,然後通過std:async去調用就是了。
唯一需要注意的一點,future模式不允許共享內存,複製一份隻讀的通過參數傳給你的函數吧。
太簡單了,一共就隻需要4步:
- 引入頭文件
- 寫需要後台執行功能的函數
- 通過async函數調用上麵寫的函數
- 主任務繼續幹活
- 需要後台任務的結果時,去讀它的返回值
引入future頭文件
#include <future>
實現功能的函數
將後台要做事兒的邏輯寫成一個函數吧,這裏寫個最簡單的:
void func1(int arg){
cout<<"I am running in background!"<<arg<<endl;
}
通過async調用您的邏輯
future<void> f1 = async(launch::async,func1,0);
大家可以看到,async有多麼的簡單,第一個參數是馬上就啟動後台任務,還是用的時候再啟動。我們一般都是希望馬上啟動,於是固定地給launch::async就好了。真的有特殊需求要用的時候再啟動,就給launch::deferred。隻有這兩種情況,簡單吧?
第二個參數就是剛才寫好的後台邏輯的函數。第三個是您的函數要用的參數。
返回一個future,類型與剛才您寫的函數的返回值一致。這時候不需要知道它是什麼。
甚至有個更絕情的辦法,我們根本不care返回值是什麼,直接給個auto讓編譯器自己推斷去。
像下麵這樣:
auto f1 = async(launch::async,func1,0);
不需要懂任何跟線程相關的知識吧?
主任務繼續幹活
這個就不多說了,既然要起後台任務,前台肯定有事兒要做。
獲取後台的值
到了需要返回值的時候,直接調用get函數。本例中是void類型,所以返回值獲取了我們也不用。
f1.get();
OK,我們的快餐教程就講完了,不需要懂什麼是線程,什麼是鎖,什麼是內存模型,原子變量是什麼,內存都有什麼順序之類的。大家可以快樂地去幹活去了~ 等將來我們詳詳細細地把剛才列舉的這些知識一一補齊,大家就會發現這其實是有多複雜了。
例子
Java曆史上借鑒了太多的C++的東西,但是內存模型和Future模式這些,Java是領先於C++的。
那我們向Java致敬吧,將Java多線程簡明教程中Future模式的例子用C++改寫一下。我們看看C++的優勢吧:
public class AsyncTaskSimple {
public static class Result implements Callable<String>{
@Override
public String call() throws Exception {
return doRealLogic();
}
private String doRealLogic(){
//Here to do the background logic
return new String("Done");
}
}
public static void main(String[] args) {
FutureTask<String> future = new FutureTask<String>(new Result());
ExecutorService executor = Executors.newFixedThreadPool(1);
executor.submit(future);
someThingToDo();
try {
String s = future.get();
System.out.println("The result is:"+s);
}catch (InterruptedException e){
//Deal with InterruptedExcpeiotn
}catch(ExecutionException ee){
//Deal with ExecutionException
}
}
private static void someThingToDo(){
//Main thread logic
}
}
Java寫了這麼一大坨,C++11隻用不到一半的篇幅就搞定了:
string doRealLogic(){
return string("Done");
}
void someThingToDo(){
//do something
}
int main(int argc, char** argv)
{
auto f2 = async(launch::async,doRealLogic);
someThingToDo();
cout<<f2.get()<<endl;
}
或者我們更現代一點,也不要寫函數了,直接上一個lambda表達式吧。
void someThingToDo(){
//do something
}
int main(int argc, char** argv)
{
auto f2 = async(launch::async,[](){
return string("Done");
});
someThingToDo();
cout<<f2.get()<<endl;
}
最後再強調一遍,輸出參數通過函數參數傳進去,輸出參數通過返回值,也就是future.get獲取回來。不要使用全局變量等方式來共享內存!
Enjoy it!
最後更新:2017-06-05 11:33:55