以前程序運行的時候,都是單線程,順序執行,不需要考慮並發,但是隨著科技的發展,對性能提出了更高的要求,多線程的出現也給單個進程運行的並發提供了基礎。雖然程序支持了並發操作,提高了程序的響應速度,增強了用戶的體驗感,但是同時也給程式設計師的開發工作帶來了難度,需要考慮並發操作時的許多問題,特別是多個線程對共享資源的訪問,如果控制不好,就會造成程序的運行沒法按照預期的效果執行。
所謂的原子操作,就是多線程下「最小並且不可並行化」的操作。而C++11提供了原子類型,這很大程度上簡化程式設計師對並發操作的難度。
本文首先從posix標準的pthread庫提供的互斥鎖開始介紹早期控制多線程並發的操作方式, 接著再重點說明C++11提供的內置原子類型以及模版原子類型的用法,最後再簡單介紹幾個內存模型的含義。
一、poxis互斥鎖
早期實現原子操作,可以藉助posix標準提供的互斥鎖來實現粗力度的互斥。這裡的粗力度是區別於C++11的內存模型來說的。我們來看下poxis互斥鎖的例子。
測試代碼啟動了兩個線程對全局變量g_ll_total進行自增操作,為了保證g_ll_total的原子性,對全局變量g_ll_total修改之前進行加鎖pthread_mutex_lock,修改之後解鎖pthread_mutex_unlock。
運行之後的輸出結果如下,其結果是正確的。從上面的例子看,每次對共享變量的修改,都要進行加鎖解鎖的操作,同時程式設計師還要注意管理互斥鎖,這無疑增加了負擔。
二、內置原子類型
C++11提供了原子類型,分別有內置的原子類型和模版原子類型。我們看下使用內置的原子類型來實現上面相同的功能。
上面的例子中,我們使用了內置原子類型atomic_llong,不需要對共享變量進行加解鎖的操作,但是線程卻能夠對變量g_all_total正確的訪問。
C++11支持的內置原子類型如下表所示
再來看下C++11的內置原子類型基本操作的例子。既可以定義atomic_int類型的變量,然後通過等於進行賦值,也可以在定義atomic_int類型的變量之後通過大括號來直接進行賦值。
三、模版原子類型
C++11除了提供內置的原子類型,更加通用使用方法是類模版的原子類型,通過該類模版,可以自定義需要的原子類型。其使用格式如下,聲明類型為T的原子變量t, 多線程對t的訪問操作就能夠保障正確性。
下面通過例子來說明其使用方法。類模版的定義與內置原子類型的定義基本相似, 但是注意不能使用已經定義的原子類型去初始化新的原子類型。這是因為標準不允許原子類型進行拷貝構造、移動構造等操作。
但是可以使用模版類型變量來初始化其模版參數類型T,比如下面的例子中,我們定義了類型為std::atomic<int>的變量atomic_number1,然後將其賦值給類型為int的變量i_number3。原子類型也提供了load接口來獲取原子類型的值。
類模版原子類型也提供了store的寫操作,來將數據寫入原子類型變量中,store的操作等同於=賦值操作。
C++11支持的操作如下表所示,atomic-integral-type和integral-type指整型的原子類型,class-type是自定義類型。
C++11提供了std::atomic_flag是無鎖的,即線程對其的訪問是不需要加鎖。它支持兩個接口test_and_set和clear, test_and_set是修改atomic_flag為true,然後返回它的舊值。test_and_set的默認參數是std::memory_order_seq_cst,表示順序一致性,這涉及到內存模型的知識,後面將會進行簡單說明。只需要記住的一點是,所有的原子類型都是採用該默認值。
通過std::atomic_flag可以實現線程之間的同步,下面的例子中,首先定義了atomic_flag類型的變量flg_lock,並初始化為ATOMIC_FLAG_INIT。然後再實現兩個線程的執行函數,第一個函數aFunc,通過調用test_and_set來等待獲取atomic_flag的值,如果返回false,那麼往下執行代碼,如果返回true,那麼繼續等待;第二個函數bFunc則執行atomic_flag的接口clear來將其設置為false。最後編寫測試代碼,程序開始的時候調用test_and_set,目的是將flg_lock設置為true, 然後再啟動兩個線程。這樣就實現了線程1等待線程2的通知之後,才繼續往下執行的功能。
四、內存模型含義
C++11的原子類型提供的操作,大部分都使用std::memory_order作為參數,memory_order是一個枚舉類型,其定義如下所示。
memory_order枚舉類型的含義如下
如果不指定參數,那麼原子類型的默認值是memory_order_seq_cst,即順序一致性。但是這會影響程序的並發性能。因此,在實際程序開發過程中,需要根據具體場景來來設置memory_order值。從上表的描述中,可以看出memory_order_acquire是針對當前線程的讀操作,而memory_order_release是針對當前線程的寫操作。特別需要注意的是memory_order_consume是針對當前線程的本原子,但是不影響當前線程的其他原子。
五、總結
至此,已經將原子類型的基本知識及其使用介紹完成。本文主要介紹了內置原子類型和類模版原子類型,另外還簡單介紹了內存模型的知識,關於內存模型只是說明了其概念。深入的介紹,計劃後續通過新的章節來總結。