C++多工程式設計簡明教程(1)-C++的多工其實很簡單

lusing發表於2017-06-05

C++多工程式設計簡明教程 (1) – C++的多工其實很簡單

用庫的方式無法實現徹底的執行緒安全!我們需要C++11

與很多同學交流的時候發現,一想到用C++寫多執行緒,還是想到pthread這樣的庫的方法實現。
但是,十幾年前的研究就證明了,執行緒安全是無法用庫的形勢來提供的,有興趣的同學可以參見原文:
http://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!


相關文章