[轉] C++11 併發指南系列



C++11 併發指南一(C++11 多線程初探)


引言

C++11 自2011年發佈以來已經快兩年了,之前一直沒怎麼關注,直到最近幾個月纔看了一些 C++11 的新特性,今後幾篇博客我都會寫一些關於 C++11 的特性,算是記錄一下自己學到的東西吧,和大家共勉。

相信 Linux 程序員都用過 Pthread, 但有了 C++11 的 std::thread 以後,你可以在語言層面編寫多線程程序了,直接的好處就是多線程程序的可移植性得到了很大的提高,所以作爲一名 C++ 程序員,熟悉 C++11 的多線程編程方式還是很有益處的。

如果你對 C++11 不太熟悉,建議先看看維基百科上關於 C++11 新特性的介紹,中文C++11介紹英文C++11介紹 ,另外C++之父 Bjarne Stroustrup 的關於 C++11 的 FAQ 也是必看的,我也收集了一些關於C++11的資料,供大家查閱:

資料匯

http://www.open-std.org/jtc1/sc22/wg21/

C++0x/C++11 Support in GCC:http://gcc.gnu.org/projects/cxx0x.html

What is C++0x:https://www2.research.att.com/~bs/what-is-2009.pdf

Overview of the New C++http://www.artima.com/shop/overview_of_the_new_cpp

Overview of the New C++ (C++0x).pdf:http://ishare.iask.sina.com.cn/f/20120005.html?from=like

A Brief Look at C++0xhttp://www.artima.com/cppsource/cpp0x.html

Summary of C++11 Feature Availability in gcc and MSVC:http://www.aristeia.com/C++11/C++11FeatureAvailability.htm

C++ 11: Come Closer:http://www.codeproject.com/Articles/344282/Cplusplus-11-Come-Closer

C++11 threads, locks and condition variables: http://www.codeproject.com/Articles/598695/Cplusplus11-threads-locks-and-condition-variables

Move Semantics and Perfect Forwarding in C++11:http://www.codeproject.com/Articles/397492/Move-Semantics-and-Perfect-Forwarding-in-Cplusplus

http://solarianprogrammer.com/categories/C++11/

C++11 Concurrency:http://www.baptiste-wicht.com/2012/03/cpp11-concurrency-part1-start-threads/

http://www.hpl.hp.com/personal/Hans_Boehm/misc_slides/sfacm-cleaned.pdf

http://en.cppreference.com/w/cpp/thread

http://isocpp.org/blog/2012/12/c11-a-cheat-sheet-alex-sinyakov

The Biggest Changes in C++11:http://blog.smartbear.com/c-plus-plus/the-biggest-changes-in-c11-and-why-you-should-care/

Ten C++11 Features Every C++ Developer Should Use:http://www.codeproject.com/Articles/570638/Ten-Cplusplus11-Features-Every-Cplusplus-Developer

 C++11 – A Glance [part 1 of n]:http://www.codeproject.com/Articles/312029/Cplusplus11-A-Glance-part-1-of-n

 C++11 – A Glance [part 2 of n]:http://www.codeproject.com/Articles/314415/Cplusplus11-A-Glance-part-2-of-n

C++11(及現代C++風格)和快速迭代式開發:http://mindhacks.cn/2012/08/27/modern-cpp-practices/

Lambda Functions in C++11 - the Definitive Guide:http://www.cprogramming.com/c++11/c++11-lambda-closures.html

Better types in C++11 - nullptr, enum classes (strongly typed enumerations) and cstdint:http://www.cprogramming.com/c++11/c++11-nullptr-strongly-typed-enum-class.html

Rvalue-references-and-move-semantics-in-c++11:http://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html

http://www.gotw.ca/publications/index.htm

http://www.devx.com/SpecialReports/Door/38865

Multi-threading in C++0x:http://accu.org/index.php/journals/1584

C++ 0X feature summary cheat sheat:http://www.iesensor.com/blog/2011/05/31/c-0x-feature-summary-cheat-sheat/

Multithreading in C++0x part 1: Starting Threads:http://www.justsoftwaresolutions.co.uk/threading/multithreading-in-c++0x-part-1-starting-threads.html

http://en.cppreference.com/w/cpp/thread

http://www.cplusplus.com/reference/multithreading/

好了,下面來說正題吧 ;-)

與 C++11 多線程相關的頭文件

C++11 新標準中引入了四個頭文件來支持多線程編程,他們分別是<atomic> ,<thread>,<mutex>,<condition_variable>和<future>。

  • <atomic>:該頭文主要聲明瞭兩個類, std::atomic 和 std::atomic_flag,另外還聲明瞭一套 C 風格的原子類型和與 C 兼容的原子操作的函數。
  • <thread>:該頭文件主要聲明瞭 std::thread 類,另外 std::this_thread 命名空間也在該頭文件中。
  • <mutex>:該頭文件主要聲明瞭與互斥量(mutex)相關的類,包括 std::mutex 系列類,std::lock_guard, std::unique_lock, 以及其他的類型和函數。
  • <condition_variable>:該頭文件主要聲明瞭與條件變量相關的類,包括 std::condition_variable 和 std::condition_variable_any。
  • <future>:該頭文件主要聲明瞭 std::promise, std::package_task 兩個 Provider 類,以及 std::future 和 std::shared_future 兩個 Future 類,另外還有一些與之相關的類型和函數,std::async() 函數就聲明在此頭文件中。

std::thread "Hello world"

下面是一個最簡單的使用 std::thread 類的例子:

複製代碼
#include <stdio.h>
#include <stdlib.h>

#include <iostream> // std::cout
#include <thread>   // std::thread

void thread_task() {
    std::cout << "hello thread" << std::endl;
}

/*
 * ===  FUNCTION  =========================================================
 *         Name:  main
 *  Description:  program entry routine.
 * ========================================================================
 */
int main(int argc, const char *argv[])
{
    std::thread t(thread_task);
    t.join();

    return EXIT_SUCCESS;
}  /* ----------  end of function main  ---------- */
複製代碼

Makefile 如下:

複製代碼
all:Thread

CC=g++
CPPFLAGS=-Wall -std=c++11 -ggdb
LDFLAGS=-pthread

Thread:Thread.o
    $(CC) $(LDFLAGS) -o $@ $^

Thread.o:Thread.cc
    $(CC) $(CPPFLAGS) -o $@ -c $^


.PHONY:
    clean

clean:
    rm Thread.o Thread
複製代碼

注意在 Linux GCC4.6 環境下,編譯時需要加 -pthread,否則執行時會出現:

$ ./Thread
terminate called after throwing an instance of 'std::system_error'
  what():  Operation not permitted
Aborted (core dumped)

原因是 GCC 默認沒有加載 pthread 庫,據說在後續的版本中可以不用在編譯時添加 -pthread 選項。

更多的有關 C++11 Concurrency 的介紹將在後續的一系列博客中寫出,希望自己勤快一點吧 ;-)



C++11 併發指南二(std::thread 詳解)



上一篇博客《C++11 併發指南一(C++11 多線程初探)》中只是提到了 std::thread 的基本用法,並給出了一個最簡單的例子,本文將稍微詳細地介紹 std::thread 的用法。

std::thread 在 <thread> 頭文件中聲明,因此使用 std::thread 時需要包含 <thread> 頭文件。

std::thread 構造

default (1)
thread() noexcept;
initialization (2)
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
copy [deleted] (3)
thread (const thread&) = delete;
move (4)
thread (thread&& x) noexcept;
  • (1). 默認構造函數,創建一個空的 thread 執行對象。
  • (2). 初始化構造函數,創建一個 thread對象,該 thread對象可被 joinable,新產生的線程會調用 fn 函數,該函數的參數由 args 給出。
  • (3). 拷貝構造函數(被禁用),意味着 thread 不可被拷貝構造。
  • (4). move 構造函數,move 構造函數,調用成功之後 x 不代表任何 thread 執行對象。
  • 注意:可被 joinable 的 thread 對象必須在他們銷燬之前被主線程 join 或者將其設置爲 detached.

std::thread 各種構造函數例子如下(參考):

複製代碼
#include <iostream>
#include <utility>
#include <thread>
#include <chrono>
#include <functional>
#include <atomic>
 
void f1(int n)
{
    for (int i = 0; i < 5; ++i) {
        std::cout << "Thread " << n << " executing\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}
 
void f2(int& n)
{
    for (int i = 0; i < 5; ++i) {
        std::cout << "Thread 2 executing\n";
        ++n;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}
 
int main()
{
    int n = 0;
    std::thread t1; // t1 is not a thread
    std::thread t2(f1, n + 1); // pass by value
    std::thread t3(f2, std::ref(n)); // pass by reference
    std::thread t4(std::move(t3)); // t4 is now running f2(). t3 is no longer a thread
    t2.join();
    t4.join();
    std::cout << "Final value of n is " << n << '\n';
}
複製代碼

move 賦值操作

move (1)
thread& operator= (thread&& rhs) noexcept;
copy [deleted] (2)
thread& operator= (const thread&) = delete;
  • (1). move 賦值操作,如果當前對象不可 joinable,需要傳遞一個右值引用(rhs)給 move 賦值操作;如果當前對象可被 joinable,則 terminate() 報錯。
  • (2). 拷貝賦值操作被禁用,thread 對象不可被拷貝。

請看下面的例子:

複製代碼
#include <stdio.h>
#include <stdlib.h>

#include <chrono>    // std::chrono::seconds
#include <iostream>  // std::cout
#include <thread>    // std::thread, std::this_thread::sleep_for

void thread_task(int n) {
    std::this_thread::sleep_for(std::chrono::seconds(n));
    std::cout << "hello thread "
        << std::this_thread::get_id()
        << " paused " << n << " seconds" << std::endl;
}

/*
 * ===  FUNCTION  =========================================================
 *         Name:  main
 *  Description:  program entry routine.
 * ========================================================================
 */
int main(int argc, const char *argv[])
{
    std::thread threads[5];
    std::cout << "Spawning 5 threads...\n";
    for (int i = 0; i < 5; i++) {
        threads[i] = std::thread(thread_task, i + 1);
    }
    std::cout << "Done spawning threads! Now wait for them to join\n";
    for (auto& t: threads) {
        t.join();
    }
    std::cout << "All threads joined.\n";

    return EXIT_SUCCESS;
}  /* ----------  end of function main  ---------- */
複製代碼

其他成員函數



C++11 併發指南三(std::mutex 詳解)



上一篇《C++11 併發指南二(std::thread 詳解)》中主要講到了 std::thread 的一些用法,並給出了兩個小例子,本文將介紹 std::mutex 的用法。

Mutex 又稱互斥量,C++ 11中與 Mutex 相關的類(包括鎖類型)和函數都聲明在 <mutex> 頭文件中,所以如果你需要使用 std::mutex,就必須包含 <mutex> 頭文件。

<mutex> 頭文件介紹

Mutex 系列類(四種)

  • std::mutex,最基本的 Mutex 類。
  • std::recursive_mutex,遞歸 Mutex 類。
  • std::time_mutex,定時 Mutex 類。
  • std::recursive_timed_mutex,定時遞歸 Mutex 類。

Lock 類(兩種)

  • std::lock_guard,與 Mutex RAII 相關,方便線程對互斥量上鎖。
  • std::unique_lock,與 Mutex RAII 相關,方便線程對互斥量上鎖,但提供了更好的上鎖和解鎖控制。

其他類型

  • std::once_flag
  • std::adopt_lock_t
  • std::defer_lock_t
  • std::try_to_lock_t

函數

  • std::try_lock,嘗試同時對多個互斥量上鎖。
  • std::lock,可以同時對多個互斥量上鎖。
  • std::call_once,如果多個線程需要同時調用某個函數,call_once 可以保證多個線程對該函數只調用一次。

std::mutex 介紹

下面以 std::mutex 爲例介紹 C++11 中的互斥量用法。

std::mutex 是C++11 中最基本的互斥量,std::mutex 對象提供了獨佔所有權的特性——即不支持遞歸地對 std::mutex 對象上鎖,而 std::recursive_lock 則可以遞歸地對互斥量對象上鎖。

std::mutex 的成員函數

  • 構造函數,std::mutex不允許拷貝構造,也不允許 move 拷貝,最初產生的 mutex 對象是處於 unlocked 狀態的。
  • lock(),調用線程將鎖住該互斥量。線程調用該函數會發生下面 3 種情況:(1). 如果該互斥量當前沒有被鎖住,則調用線程將該互斥量鎖住,直到調用 unlock之前,該線程一直擁有該鎖。(2). 如果當前互斥量被其他線程鎖住,則當前的調用線程被阻塞住。(3). 如果當前互斥量被當前調用線程鎖住,則會產生死鎖(deadlock)。
  • unlock(), 解鎖,釋放對互斥量的所有權。
  • try_lock(),嘗試鎖住互斥量,如果互斥量被其他線程佔有,則當前線程也不會被阻塞。線程調用該函數也會出現下面 3 種情況,(1). 如果當前互斥量沒有被其他線程佔有,則該線程鎖住互斥量,直到該線程調用 unlock 釋放互斥量。(2). 如果當前互斥量被其他線程鎖住,則當前調用線程返回 false,而並不會被阻塞掉。(3). 如果當前互斥量被當前調用線程鎖住,則會產生死鎖(deadlock)。

下面給出一個與 std::mutex 的小例子(參考

複製代碼
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex

volatile int counter(0); // non-atomic counter
std::mutex mtx;           // locks access to counter

void attempt_10k_increases() {
    for (int i=0; i<10000; ++i) {
        if (mtx.try_lock()) {   // only increase if currently not locked:
            ++counter;
            mtx.unlock();
        }
    }
}

int main (int argc, const char* argv[]) {
    std::thread threads[10];
    for (int i=0; i<10; ++i)
        threads[i] = std::thread(attempt_10k_increases);

    for (auto& th : threads) th.join();
    std::cout << counter << " successful increases of the counter.\n";

    return 0;
}
複製代碼

std::recursive_mutex 介紹

std::recursive_mutex 與 std::mutex 一樣,也是一種可以被上鎖的對象,但是和 std::mutex 不同的是,std::recursive_mutex 允許同一個線程對互斥量多次上鎖(即遞歸上鎖),來獲得對互斥量對象的多層所有權,std::recursive_mutex 釋放互斥量時需要調用與該鎖層次深度相同次數的 unlock(),可理解爲 lock() 次數和 unlock() 次數相同,除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。

std::time_mutex 介紹

std::time_mutex 比 std::mutex 多了兩個成員函數,try_lock_for(),try_lock_until()。

try_lock_for 函數接受一個時間範圍,表示在這一段時間範圍之內線程如果沒有獲得鎖則被阻塞住(與 std::mutex 的 try_lock() 不同,try_lock 如果被調用時沒有獲得鎖則直接返回 false),如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間內還是沒有獲得鎖),則返回 false。

try_lock_until 函數則接受一個時間點作爲參數,在指定時間點未到來之前線程如果沒有獲得鎖則被阻塞住,如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間內還是沒有獲得鎖),則返回 false。

下面的小例子說明了 std::time_mutex 的用法(參考)。

複製代碼
#include <iostream>       // std::cout
#include <chrono>         // std::chrono::milliseconds
#include <thread>         // std::thread
#include <mutex>          // std::timed_mutex

std::timed_mutex mtx;

void fireworks() {
  // waiting to get a lock: each thread prints "-" every 200ms:
  while (!mtx.try_lock_for(std::chrono::milliseconds(200))) {
    std::cout << "-";
  }
  // got a lock! - wait for 1s, then this thread prints "*"
  std::this_thread::sleep_for(std::chrono::milliseconds(1000));
  std::cout << "*\n";
  mtx.unlock();
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(fireworks);

  for (auto& th : threads) th.join();

  return 0;
}
複製代碼

std::recursive_timed_mutex 介紹

和 std:recursive_mutex 與 std::mutex 的關係一樣,std::recursive_timed_mutex 的特性也可以從 std::timed_mutex 推導出來,感興趣的同鞋可以自行查閱。 ;-)

std::lock_guard 介紹

與 Mutex RAII 相關,方便線程對互斥量上鎖。例子(參考):

複製代碼
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock_guard
#include <stdexcept>      // std::logic_error

std::mutex mtx;

void print_even (int x) {
    if (x%2==0) std::cout << x << " is even\n";
    else throw (std::logic_error("not even"));
}

void print_thread_id (int id) {
    try {
        // using a local lock_guard to lock mtx guarantees unlocking on destruction / exception:
        std::lock_guard<std::mutex> lck (mtx);
        print_even(id);
    }
    catch (std::logic_error&) {
        std::cout << "[exception caught]\n";
    }
}

int main ()
{
    std::thread threads[10];
    // spawn 10 threads:
    for (int i=0; i<10; ++i)
        threads[i] = std::thread(print_thread_id,i+1);

    for (auto& th : threads) th.join();

    return 0;
}
複製代碼

std::unique_lock 介紹

與 Mutex RAII 相關,方便線程對互斥量上鎖,但提供了更好的上鎖和解鎖控制。例子(參考):

複製代碼
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::unique_lock

std::mutex mtx;           // mutex for critical section

void print_block (int n, char c) {
    // critical section (exclusive access to std::cout signaled by lifetime of lck):
    std::unique_lock<std::mutex> lck (mtx);
    for (int i=0; i<n; ++i) {
        std::cout << c;
    }
    std::cout << '\n';
}

int main ()
{
    std::thread th1 (print_block,50,'*');
    std::thread th2 (print_block,50,'$');

    th1.join();
    th2.join();

    return 0;
}
複製代碼

好了,本文暫時講到這裏,還剩下 std::try_lock,std::lock,std::call_once 三個函數沒有講到,留在下一篇博客中講吧 ;-)




C++11 併發指南三(Lock 詳解)


在 《C++11 併發指南三(std::mutex 詳解)》一文中我們主要介紹了 C++11 標準中的互斥量(Mutex),並簡單介紹了一下兩種鎖類型。本節將詳細介紹一下 C++11 標準的鎖類型。

C++11 標準爲我們提供了兩種基本的鎖類型,分別如下:

  • std::lock_guard,與 Mutex RAII 相關,方便線程對互斥量上鎖。
  • std::unique_lock,與 Mutex RAII 相關,方便線程對互斥量上鎖,但提供了更好的上鎖和解鎖控制。

另外還提供了幾個與鎖類型相關的 Tag 類,分別如下:

  • std::adopt_lock_t,一個空的標記類,定義如下:
struct adopt_lock_t {};

 該類型的常量對象adopt_lock(adopt_lock 是一個常量對象,定義如下:

constexpr adopt_lock_t adopt_lock {};,// constexpr 是 C++11 中的新關鍵字)

通常作爲參數傳入給 unique_lock 或 lock_guard 的構造函數。

  • std::defer_lock_t,一個空的標記類,定義如下:
struct defer_lock_t {};

 該類型的常量對象 defer_lockdefer_lock 是一個常量對象,定義如下:

constexpr defer_lock_t defer_lock {};,// constexpr 是 C++11 中的新關鍵字)

通常作爲參數傳入給 unique_lock 或 lock_guard 的構造函數。

  • std::try_to_lock_t,一個空的標記類,定義如下:
struct try_to_lock_t {};

 該類型的常量對象 try_to_locktry_to_lock 是一個常量對象,定義如下:

constexpr try_to_lock_t try_to_lock {};,// constexpr 是 C++11 中的新關鍵字)

通常作爲參數傳入給 unique_lock 或 lock_guard 的構造函數。後面我們會詳細介紹以上三種 Tag 類型在配合 lock_gurad 與 unique_lock 使用時的區別。

std::lock_guard 介紹

std::lock_gurad 是 C++11 中定義的模板類。定義如下:

template <class Mutex> class lock_guard;

lock_guard 對象通常用於管理某個鎖(Lock)對象,因此與 Mutex RAII 相關,方便線程對互斥量上鎖,即在某個 lock_guard 對象的聲明週期內,它所管理的鎖對象會一直保持上鎖狀態;而 lock_guard 的生命週期結束之後,它所管理的鎖對象會被解鎖(注:類似 shared_ptr 等智能指針管理動態分配的內存資源 )。

模板參數 Mutex 代表互斥量類型,例如 std::mutex 類型,它應該是一個基本的 BasicLockable 類型,標準庫中定義幾種基本的 BasicLockable 類型,分別 std::mutex, std::recursive_mutex, std::timed_mutex,std::recursive_timed_mutex (以上四種類型均已在上一篇博客中介紹)以及 std::unique_lock(本文後續會介紹 std::unique_lock)。(注:BasicLockable 類型的對象只需滿足兩種操作,lock 和 unlock,另外還有 Lockable 類型,在 BasicLockable 類型的基礎上新增了 try_lock 操作,因此一個滿足 Lockable 的對象應支持三種操作:lock,unlock 和 try_lock;最後還有一種 TimedLockable 對象,在 Lockable 類型的基礎上又新增了 try_lock_for 和 try_lock_until 兩種操作,因此一個滿足 TimedLockable 的對象應支持五種操作:lock, unlock, try_lock, try_lock_for, try_lock_until)。

在 lock_guard 對象構造時,傳入的 Mutex 對象(即它所管理的 Mutex 對象)會被當前線程鎖住。在lock_guard 對象被析構時,它所管理的 Mutex 對象會自動解鎖,由於不需要程序員手動調用 lock 和 unlock 對 Mutex 進行上鎖和解鎖操作,因此這也是最簡單安全的上鎖和解鎖方式,尤其是在程序拋出異常後先前已被上鎖的 Mutex 對象可以正確進行解鎖操作,極大地簡化了程序員編寫與 Mutex 相關的異常處理代碼。

值得注意的是,lock_guard 對象並不負責管理 Mutex 對象的生命週期,lock_guard 對象只是簡化了 Mutex 對象的上鎖和解鎖操作,方便線程對互斥量上鎖,即在某個 lock_guard 對象的聲明週期內,它所管理的鎖對象會一直保持上鎖狀態;而 lock_guard 的生命週期結束之後,它所管理的鎖對象會被解鎖。

std::lock_guard 構造函數

lock_guard 構造函數如下表所示:

locking (1)
explicit lock_guard (mutex_type& m);
adopting (2)
lock_guard (mutex_type& m, adopt_lock_t tag);
copy [deleted](3)
lock_guard (const lock_guard&) = delete;
  1. locking 初始化
    • lock_guard 對象管理 Mutex 對象 m,並在構造時對 m 進行上鎖(調用 m.lock())。
  2. adopting初始化
    • lock_guard 對象管理 Mutex 對象 m,與 locking 初始化(1) 不同的是, Mutex 對象 m 已被當前線程鎖住。
  3. 拷貝構造
    • lock_guard 對象的拷貝構造和移動構造(move construction)均被禁用,因此 lock_guard 對象不可被拷貝構造或移動構造。

我們來看一個簡單的例子(參考):

複製代碼
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock_guard, std::adopt_lock

std::mutex mtx;           // mutex for critical section

void print_thread_id (int id) {
  mtx.lock();
  std::lock_guard<std::mutex> lck(mtx, std::adopt_lock);
  std::cout << "thread #" << id << '\n';
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(print_thread_id,i+1);

  for (auto& th : threads) th.join();

  return 0;
}
複製代碼

在 print_thread_id 中,我們首先對 mtx 進行上鎖操作(mtx.lock();),然後用 mtx 對象構造一個 lock_guard 對象(std::lock_guard<std::mutex> lck(mtx, std::adopt_lock);),注意此時 Tag 參數爲 std::adopt_lock,表明當前線程已經獲得了鎖,此後 mtx 對象的解鎖操作交由 lock_guard 對象 lck 來管理,在 lck 的生命週期結束之後,mtx 對象會自動解鎖。

lock_guard 最大的特點就是安全易於使用,請看下面例子(參考),在異常拋出的時候通過 lock_guard 對象管理的 Mutex 可以得到正確地解鎖。

複製代碼
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock_guard
#include <stdexcept>      // std::logic_error

std::mutex mtx;

void print_even (int x) {
  if (x%2==0) std::cout << x << " is even\n";
  else throw (std::logic_error("not even"));
}

void print_thread_id (int id) {
  try {
    // using a local lock_guard to lock mtx guarantees unlocking on destruction / exception:
    std::lock_guard<std::mutex> lck (mtx);
    print_even(id);
  }
  catch (std::logic_error&) {
    std::cout << "[exception caught]\n";
  }
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(print_thread_id,i+1);

  for (auto& th : threads) th.join();

  return 0;
}
複製代碼

std::unique_lock 介紹

但是 lock_guard 最大的缺點也是簡單,沒有給程序員提供足夠的靈活度,因此,C++11 標準中定義了另外一個與 Mutex RAII 相關類 unique_lock,該類與 lock_guard 類相似,也很方便線程對互斥量上鎖,但它提供了更好的上鎖和解鎖控制。

顧名思義,unique_lock 對象以獨佔所有權的方式( unique owership)管理 mutex 對象的上鎖和解鎖操作,所謂獨佔所有權,就是沒有其他的 unique_lock 對象同時擁有某個 mutex 對象的所有權。

在構造(或移動(move)賦值)時,unique_lock 對象需要傳遞一個 Mutex 對象作爲它的參數,新創建的 unique_lock 對象負責傳入的 Mutex 對象的上鎖和解鎖操作。

std::unique_lock 對象也能保證在其自身析構時它所管理的 Mutex 對象能夠被正確地解鎖(即使沒有顯式地調用 unlock 函數)。因此,和 lock_guard 一樣,這也是一種簡單而又安全的上鎖和解鎖方式,尤其是在程序拋出異常後先前已被上鎖的 Mutex 對象可以正確進行解鎖操作,極大地簡化了程序員編寫與 Mutex 相關的異常處理代碼。

值得注意的是,unique_lock 對象同樣也不負責管理 Mutex 對象的生命週期,unique_lock 對象只是簡化了 Mutex 對象的上鎖和解鎖操作,方便線程對互斥量上鎖,即在某個 unique_lock 對象的聲明週期內,它所管理的鎖對象會一直保持上鎖狀態;而 unique_lock 的生命週期結束之後,它所管理的鎖對象會被解鎖,這一點和 lock_guard 類似,但 unique_lock 給程序員提供了更多的自由,我會在下面的內容中給大家介紹 unique_lock 的用法。

另外,與 lock_guard 一樣,模板參數 Mutex 代表互斥量類型,例如 std::mutex 類型,它應該是一個基本的 BasicLockable 類型,標準庫中定義幾種基本的 BasicLockable 類型,分別 std::mutex, std::recursive_mutex, std::timed_mutex,std::recursive_timed_mutex (以上四種類型均已在上一篇博客中介紹)以及 std::unique_lock(本文後續會介紹 std::unique_lock)。(注:BasicLockable 類型的對象只需滿足兩種操作,lock 和 unlock,另外還有 Lockable 類型,在 BasicLockable 類型的基礎上新增了 try_lock 操作,因此一個滿足 Lockable 的對象應支持三種操作:lock,unlock 和 try_lock;最後還有一種 TimedLockable 對象,在 Lockable 類型的基礎上又新增了 try_lock_for 和 try_lock_until 兩種操作,因此一個滿足 TimedLockable 的對象應支持五種操作:lock, unlock, try_lock, try_lock_for, try_lock_until)。

std::unique_lock 構造函數

std::unique_lock 的構造函數的數目相對來說比 std::lock_guard 多,其中一方面也是因爲 std::unique_lock 更加靈活,從而在構造 std::unique_lock 對象時可以接受額外的參數。總地來說,std::unique_lock 構造函數如下:

default (1)
unique_lock() noexcept;
locking (2)
explicit unique_lock(mutex_type& m);
try-locking (3)
unique_lock(mutex_type& m, try_to_lock_t tag);
deferred (4)
unique_lock(mutex_type& m, defer_lock_t tag) noexcept;
adopting (5)
unique_lock(mutex_type& m, adopt_lock_t tag);
locking for (6)
template <class Rep, class Period>
unique_lock(mutex_type& m, const chrono::duration<Rep,Period>& rel_time);
locking until (7)
template <class Clock, class Duration>
unique_lock(mutex_type& m, const chrono::time_point<Clock,Duration>& abs_time);
copy [deleted] (8)
unique_lock(const unique_lock&) = delete;
move (9)
unique_lock(unique_lock&& x);

下面我們來分別介紹以上各個構造函數:

(1) 默認構造函數
新創建的 unique_lock 對象不管理任何 Mutex 對象。
(2) locking 初始化
新創建的 unique_lock 對象管理 Mutex 對象 m,並嘗試調用 m.lock() 對 Mutex 對象進行上鎖,如果此時另外某個 unique_lock 對象已經管理了該 Mutex 對象 m,則當前線程將會被阻塞。
(3) try-locking 初始化
新創建的 unique_lock 對象管理 Mutex 對象 m,並嘗試調用 m.try_lock() 對 Mutex 對象進行上鎖,但如果上鎖不成功,並不會阻塞當前線程。
(4) deferred 初始化
新創建的 unique_lock 對象管理 Mutex 對象 m,但是在初始化的時候並不鎖住 Mutex 對象。 m 應該是一個沒有當前線程鎖住的 Mutex 對象。
(5) adopting 初始化
新創建的 unique_lock 對象管理 Mutex 對象 m, m 應該是一個已經被當前線程鎖住的 Mutex 對象。(並且當前新創建的 unique_lock 對象擁有對鎖(Lock)的所有權)。
(6) locking 一段時間(duration)
新創建的 unique_lock 對象管理 Mutex 對象 m,並試圖通過調用 m.try_lock_for(rel_time) 來鎖住 Mutex 對象一段時間(rel_time)。
(7) locking 直到某個時間點(time point)
新創建的 unique_lock 對象管理 Mutex 對象m,並試圖通過調用 m.try_lock_until(abs_time) 來在某個時間點(abs_time)之前鎖住 Mutex 對象。
(8) 拷貝構造 [被禁用]
unique_lock 對象不能被拷貝構造。
(9) 移動(move)構造
新創建的 unique_lock 對象獲得了由 x 所管理的 Mutex 對象的所有權(包括當前 Mutex 的狀態)。調用 move 構造之後, x 對象如同通過默認構造函數所創建的,就不再管理任何 Mutex 對象了。

綜上所述,由 (2) 和 (5) 創建的 unique_lock 對象通常擁有 Mutex 對象的鎖。而通過 (1) 和 (4) 創建的則不會擁有鎖。通過 (3),(6) 和 (7) 創建的 unique_lock 對象,則在 lock 成功時獲得鎖。

關於unique_lock 的構造函數,請看下面例子(參考):

複製代碼
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock, std::unique_lock
                          // std::adopt_lock, std::defer_lock
std::mutex foo,bar;

void task_a () {
  std::lock (foo,bar);         // simultaneous lock (prevents deadlock)
  std::unique_lock<std::mutex> lck1 (foo,std::adopt_lock);
  std::unique_lock<std::mutex> lck2 (bar,std::adopt_lock);
  std::cout << "task a\n";
  // (unlocked automatically on destruction of lck1 and lck2)
}

void task_b () {
  // foo.lock(); bar.lock(); // replaced by:
  std::unique_lock<std::mutex> lck1, lck2;
  lck1 = std::unique_lock<std::mutex>(bar,std::defer_lock);
  lck2 = std::unique_lock<std::mutex>(foo,std::defer_lock);
  std::lock (lck1,lck2);       // simultaneous lock (prevents deadlock)
  std::cout << "task b\n";
  // (unlocked automatically on destruction of lck1 and lck2)
}


int main ()
{
  std::thread th1 (task_a);
  std::thread th2 (task_b);

  th1.join();
  th2.join();

  return 0;
}
複製代碼

std::unique_lock 移動(move assign)賦值操作

std::unique_lock 支持移動賦值(move assignment),但是普通的賦值被禁用了,

move (1)
unique_lock& operator= (unique_lock&& x) noexcept;
copy [deleted] (2)
unique_lock& operator= (const unique_lock&) = delete;

移動賦值(move assignment)之後,由 x 所管理的 Mutex 對象及其狀態將會被新的 std::unique_lock 對象取代。

如果被賦值的對象之前已經獲得了它所管理的 Mutex 對象的鎖,則在移動賦值(move assignment)之前會調用 unlock 函數釋放它所佔有的鎖。

調用移動賦值(move assignment)之後, x 對象如同通過默認構造函數所創建的,也就不再管理任何 Mutex 對象了。請看下面例子(參考):

複製代碼
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::unique_lock

std::mutex mtx;           // mutex for critical section

void print_fifty (char c) {
  std::unique_lock<std::mutex> lck;         // default-constructed
  lck = std::unique_lock<std::mutex>(mtx);  // move-assigned
  for (int i=0; i<50; ++i) { std::cout << c; }
  std::cout << '\n';
}

int main ()
{
  std::thread th1 (print_fifty,'*');
  std::thread th2 (print_fifty,'$');

  th1.join();
  th2.join();

  return 0;
}
複製代碼

std::unique_lock 主要成員函數

本節我們來看看 std::unique_lock 的主要成員函數。由於 std::unique_lock 比 std::lock_guard 操作靈活,因此它提供了更多成員函數。具體分類如下:

  1. 上鎖/解鎖操作:lock,try_lock,try_lock_for,try_lock_until  unlock
  2. 修改操作:移動賦值(move assignment)(前面已經介紹過了),交換(swap)(與另一個 std::unique_lock 對象交換它們所管理的 Mutex 對象的所有權),釋放(release)(返回指向它所管理的 Mutex 對象的指針,並釋放所有權)
  3. 獲取屬性操作:owns_lock(返回當前 std::unique_lock 對象是否獲得了鎖)、operator bool()(與 owns_lock 功能相同,返回當前 std::unique_lock 對象是否獲得了鎖)、mutex(返回當前 std::unique_lock 對象所管理的 Mutex 對象的指針)。

std::unique_lock::lock請看下面例子(參考):

上鎖操作,調用它所管理的 Mutex 對象的 lock 函數。如果在調用  Mutex 對象的 lock 函數時該 Mutex 對象已被另一線程鎖住,則當前線程會被阻塞,直到它獲得了鎖。

該函數返回時,當前的 unique_lock 對象便擁有了它所管理的 Mutex 對象的鎖。如果上鎖操作失敗,則拋出 system_error 異常。

複製代碼
// unique_lock::lock/unlock
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::unique_lock, std::defer_lock

std::mutex mtx;           // mutex for critical section

void print_thread_id (int id) {
  std::unique_lock<std::mutex> lck (mtx,std::defer_lock);
  // critical section (exclusive access to std::cout signaled by locking lck):
  lck.lock();
  std::cout << "thread #" << id << '\n';
  lck.unlock();
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(print_thread_id,i+1);

  for (auto& th : threads) th.join();

  return 0;
}
複製代碼

std::unique_lock::try_lock

上鎖操作,調用它所管理的 Mutex 對象的 try_lock 函數,如果上鎖成功,則返回 true,否則返回 false。

請看下面例子(參考):

複製代碼
#include <iostream>       // std::cout
#include <vector>         // std::vector
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::unique_lock, std::defer_lock

std::mutex mtx;           // mutex for critical section

void print_star () {
  std::unique_lock<std::mutex> lck(mtx,std::defer_lock);
  // print '*' if successfully locked, 'x' otherwise: 
  if (lck.try_lock())
    std::cout << '*';
  else                    
    std::cout << 'x';
}

int main ()
{
  std::vector<std::thread> threads;
  for (int i=0; i<500; ++i)
    threads.emplace_back(print_star);

  for (auto& x: threads) x.join();

  return 0;
}
複製代碼

std::unique_lock::try_lock_for

上鎖操作,調用它所管理的 Mutex 對象的 try_lock_for 函數,如果上鎖成功,則返回 true,否則返回 false。

請看下面例子(參考):

複製代碼
#include <iostream>       // std::cout
#include <chrono>         // std::chrono::milliseconds
#include <thread>         // std::thread
#include <mutex>          // std::timed_mutex, std::unique_lock, std::defer_lock

std::timed_mutex mtx;

void fireworks () {
  std::unique_lock<std::timed_mutex> lck(mtx,std::defer_lock);
  // waiting to get a lock: each thread prints "-" every 200ms:
  while (!lck.try_lock_for(std::chrono::milliseconds(200))) {
    std::cout << "-";
  }
  // got a lock! - wait for 1s, then this thread prints "*"
  std::this_thread::sleep_for(std::chrono::milliseconds(1000));
  std::cout << "*\n";
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(fireworks);

  for (auto& th : threads) th.join();

  return 0;
}
複製代碼

std::unique_lock::try_lock_until

上鎖操作,調用它所管理的 Mutex 對象的 try_lock_for 函數,如果上鎖成功,則返回 true,否則返回 false。

請看下面例子(參考):

複製代碼
#include <iostream>       // std::cout
#include <chrono>         // std::chrono::milliseconds
#include <thread>         // std::thread
#include <mutex>          // std::timed_mutex, std::unique_lock, std::defer_lock

std::timed_mutex mtx;

void fireworks () {
  std::unique_lock<std::timed_mutex> lck(mtx,std::defer_lock);
  // waiting to get a lock: each thread prints "-" every 200ms:
  while (!lck.try_lock_for(std::chrono::milliseconds(200))) {
    std::cout << "-";
  }
  // got a lock! - wait for 1s, then this thread prints "*"
  std::this_thread::sleep_for(std::chrono::milliseconds(1000));
  std::cout << "*\n";
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(fireworks);

  for (auto& th : threads) th.join();

  return 0;
}
複製代碼

std::unique_lock::unlock

解鎖操作,調用它所管理的 Mutex 對象的 unlock 函數。

請看下面例子(參考):

複製代碼
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::unique_lock, std::defer_lock

std::mutex mtx;           // mutex for critical section

void print_thread_id (int id) {
  std::unique_lock<std::mutex> lck (mtx,std::defer_lock);
  // critical section (exclusive access to std::cout signaled by locking lck):
  lck.lock();
  std::cout << "thread #" << id << '\n';
  lck.unlock();
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(print_thread_id,i+1);

  for (auto& th : threads) th.join();

  return 0;
}
複製代碼

std::unique_lock::release

返回指向它所管理的 Mutex 對象的指針,並釋放所有權。

請看下面例子(參考):

複製代碼
#include <iostream>       // std::cout
#include <vector>         // std::vector
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::unique_lock

std::mutex mtx;
int count = 0;

void print_count_and_unlock (std::mutex* p_mtx) {
  std::cout << "count: " << count << '\n';
  p_mtx->unlock();
}

void task() {
  std::unique_lock<std::mutex> lck(mtx);
  ++count;
  print_count_and_unlock(lck.release());
}

int main ()
{
  std::vector<std::thread> threads;
  for (int i=0; i<10; ++i)
    threads.emplace_back(task);

  for (auto& x: threads) x.join();

  return 0;
}
複製代碼

std::unique_lock::owns_lock

返回當前 std::unique_lock 對象是否獲得了鎖。

請看下面例子(參考):

複製代碼
#include <iostream>       // std::cout
#include <vector>         // std::vector
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::unique_lock, std::try_to_lock

std::mutex mtx;           // mutex for critical section

void print_star () {
  std::unique_lock<std::mutex> lck(mtx,std::try_to_lock);
  // print '*' if successfully locked, 'x' otherwise: 
  if (lck.owns_lock())
    std::cout << '*';
  else                    
    std::cout << 'x';
}

int main ()
{
  std::vector<std::thread> threads;
  for (int i=0; i<500; ++i)
    threads.emplace_back(print_star);

  for (auto& x: threads) x.join();

  return 0;
}
複製代碼

std::unique_lock::operator bool()

與 owns_lock 功能相同,返回當前 std::unique_lock 對象是否獲得了鎖。

請看下面例子(參考):

複製代碼
#include <iostream>       // std::cout
#include <vector>         // std::vector
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::unique_lock, std::try_to_lock

std::mutex mtx;           // mutex for critical section

void print_star () {
  std::unique_lock<std::mutex> lck(mtx,std::try_to_lock);
  // print '*' if successfully locked, 'x' otherwise: 
  if (lck)
    std::cout << '*';
  else                    
    std::cout << 'x';
}

int main ()
{
  std::vector<std::thread> threads;
  for (int i=0; i<500; ++i)
    threads.emplace_back(print_star);

  for (auto& x: threads) x.join();

  return 0;
}
複製代碼

std::unique_lock::mutex

返回當前 std::unique_lock 對象所管理的 Mutex 對象的指針。

請看下面例子(參考):

複製代碼
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::unique_lock, std::defer_lock

class MyMutex : public std::mutex {
  int _id;
public:
  MyMutex (int id) : _id(id) {}
  int id() {return _id;}
};

MyMutex mtx (101);

void print_ids (int id) {
  std::unique_lock<MyMutex> lck (mtx);
  std::cout << "thread #" << id << " locked mutex " << lck.mutex()->id() << '\n';
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(print_ids,i+1);

  for (auto& th : threads) th.join();

  return 0;
}
複製代碼

好了,本文先介紹到這裏,我們基本上介紹完了 C++11 多線程編程中兩種最基本的鎖類型,後面我會繼續更新有關 C++11 併發編程的博客,希望感興趣的同學繼續關注 ;-)


C++11 併發指南四(<future> 詳解一 std::promise 介紹)


前面兩講《C++11 併發指南二(std::thread 詳解)》,《C++11 併發指南三(std::mutex 詳解)》分別介紹了 std::thread 和 std::mutex,相信讀者對 C++11 中的多線程編程有了一個最基本的認識,本文將介紹 C++11 標準中 <future> 頭文件裏面的類和相關函數。

<future> 頭文件中包含了以下幾個類和函數:

  • Providers 類:std::promise, std::package_task
  • Futures 類:std::future, shared_future.
  • Providers 函數:std::async()
  • 其他類型:std::future_error, std::future_errc, std::future_status, std::launch.

std::promise 類介紹

promise 對象可以保存某一類型 T 的值,該值可被 future 對象讀取(可能在另外一個線程中),因此 promise 也提供了一種線程同步的手段。在 promise 對象構造時可以和一個共享狀態(通常是std::future)相關聯,並可以在相關聯的共享狀態(std::future)上保存一個類型爲 T 的值。

可以通過 get_future 來獲取與該 promise 對象相關聯的 future 對象,調用該函數之後,兩個對象共享相同的共享狀態(shared state)

  • promise 對象是異步 Provider,它可以在某一時刻設置共享狀態的值。
  • future 對象可以異步返回共享狀態的值,或者在必要的情況下阻塞調用者並等待共享狀態標誌變爲 ready,然後才能獲取共享狀態的值。

下面以一個簡單的例子來說明上述關係

複製代碼
#include <iostream>       // std::cout
#include <functional>     // std::ref
#include <thread>         // std::thread
#include <future>         // std::promise, std::future

void print_int(std::future<int>& fut) {
    int x = fut.get(); // 獲取共享狀態的值.
    std::cout << "value: " << x << '\n'; // 打印 value: 10.
}

int main ()
{
    std::promise<int> prom; // 生成一個 std::promise<int> 對象.
    std::future<int> fut = prom.get_future(); // 和 future 關聯.
    std::thread t(print_int, std::ref(fut)); // 將 future 交給另外一個線程t.
    prom.set_value(10); // 設置共享狀態的值, 此處和線程t保持同步.
    t.join();
    return 0;
}
複製代碼

std::promise 構造函數

default (1)
promise();
with allocator (2)
template <class Alloc> promise (allocator_arg_t aa, const Alloc& alloc);
copy [deleted] (3)
promise (const promise&) = delete;
move (4)
promise (promise&& x) noexcept;
  1. 默認構造函數,初始化一個空的共享狀態。
  2. 帶自定義內存分配器的構造函數,與默認構造函數類似,但是使用自定義分配器來分配共享狀態。
  3. 拷貝構造函數,被禁用。
  4. 移動構造函數。

另外,std::promise 的 operator= 沒有拷貝語義,即 std::promise 普通的賦值操作被禁用,operator= 只有 move 語義,所以 std::promise 對象是禁止拷貝的。

例子:

複製代碼
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <future>         // std::promise, std::future

std::promise<int> prom;

void print_global_promise () {
    std::future<int> fut = prom.get_future();
    int x = fut.get();
    std::cout << "value: " << x << '\n';
}

int main ()
{
    std::thread th1(print_global_promise);
    prom.set_value(10);
    th1.join();

    prom = std::promise<int>();    // prom 被move賦值爲一個新的 promise 對象.

    std::thread th2 (print_global_promise);
    prom.set_value (20);
    th2.join();

  return 0;
}
複製代碼

 std::promise::get_future 介紹

該函數返回一個與 promise 共享狀態相關聯的 future 返回的 future 對象可以訪問由 promise 對象設置在共享狀態上的值或者某個異常對象。只能從 promise 共享狀態獲取一個 future 對象。在調用該函數之後,promise 對象通常會在某個時間點準備好(設置一個值或者一個異常對象),如果不設置值或者異常,promise 對象在析構時會自動地設置一個 future_error 異常(broken_promise)來設置其自身的準備狀態。上面的例子中已經提到了 get_future,此處不再重複。

std::promise::set_value 介紹

generic template (1)
void set_value (const T& val);
void set_value (T&& val);
specializations (2)
void promise<R&>::set_value (R& val);   // when T is a reference type (R&)
void promise<void>::set_value (void);   // when T is void

設置共享狀態的值,此後 promise 的共享狀態標誌變爲 ready.

 std::promise::set_exception 介紹

爲 promise 設置異常,此後 promise 的共享狀態變標誌變爲 ready,例子如下,線程1從終端接收一個整數,線程2將該整數打印出來,如果線程1接收一個非整數,則爲 promise 設置一個異常(failbit) ,線程2 在std::future::get 是拋出該異常。

複製代碼
#include <iostream>       // std::cin, std::cout, std::ios
#include <functional>     // std::ref
#include <thread>         // std::thread
#include <future>         // std::promise, std::future
#include <exception>      // std::exception, std::current_exception

void get_int(std::promise<int>& prom) {
    int x;
    std::cout << "Please, enter an integer value: ";
    std::cin.exceptions (std::ios::failbit);   // throw on failbit
    try {
        std::cin >> x;                         // sets failbit if input is not int
        prom.set_value(x);
    } catch (std::exception&) {
        prom.set_exception(std::current_exception());
    }
}

void print_int(std::future<int>& fut) {
    try {
        int x = fut.get();
        std::cout << "value: " << x << '\n';
    } catch (std::exception& e) {
        std::cout << "[exception caught: " << e.what() << "]\n";
    }
}

int main ()
{
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();

    std::thread th1(get_int, std::ref(prom));
    std::thread th2(print_int, std::ref(fut));

    th1.join();
    th2.join();
    return 0;
}
複製代碼

std::promise::set_value_at_thread_exit 介紹

設置共享狀態的值,但是不將共享狀態的標誌設置爲 ready,當線程退出時該 promise 對象會自動設置爲 ready。如果某個 std::future 對象與該 promise 對象的共享狀態相關聯,並且該 future 正在調用 get,則調用 get 的線程會被阻塞,當線程退出時,調用 future::get 的線程解除阻塞,同時 get 返回 set_value_at_thread_exit 所設置的值。注意,該函數已經設置了 promise 共享狀態的值,如果在線程結束之前有其他設置或者修改共享狀態的值的操作,則會拋出 future_error( promise_already_satisfied )。

std::promise::swap 介紹

交換 promise 的共享狀態。



C++11 併發指南四(<future> 詳解二 std::packaged_task 介紹)



上一講《C++11 併發指南四(<future> 詳解一 std::promise 介紹)》主要介紹了 <future> 頭文件中的 std::promise 類,本文主要介紹std::packaged_task。

std::packaged_task 包裝一個可調用的對象,並且允許異步獲取該可調用對象產生的結果,從包裝可調用對象意義上來講,std::packaged_task 與 std::function 類似,只不過 std::packaged_task 將其包裝的可調用對象的執行結果傳遞給一個 std::future 對象(該對象通常在另外一個線程中獲取 std::packaged_task 任務的執行結果)。

std::packaged_task 對象內部包含了兩個最基本元素,一、被包裝的任務(stored task),任務(task)是一個可調用的對象,如函數指針、成員函數指針或者函數對象,二、共享狀態(shared state),用於保存任務的返回值,可以通過 std::future 對象來達到異步訪問共享狀態的效果。

可以通過 std::packged_task::get_future 來獲取與共享狀態相關聯的 std::future 對象。在調用該函數之後,兩個對象共享相同的共享狀態,具體解釋如下:

  • std::packaged_task 對象是異步 Provider,它在某一時刻通過調用被包裝的任務來設置共享狀態的值。
  • std::future 對象是一個異步返回對象,通過它可以獲得共享狀態的值,當然在必要的時候需要等待共享狀態標誌變爲 ready.

std::packaged_task 的共享狀態的生命週期一直持續到最後一個與之相關聯的對象被釋放或者銷燬爲止。下面一個小例子大致講了 std::packaged_task 的用法:

複製代碼
#include <iostream>     // std::cout
#include <future>       // std::packaged_task, std::future
#include <chrono>       // std::chrono::seconds
#include <thread>       // std::thread, std::this_thread::sleep_for

// count down taking a second for each value:
int countdown (int from, int to) {
    for (int i=from; i!=to; --i) {
        std::cout << i << '\n';
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    std::cout << "Finished!\n";
    return from - to;
}

int main ()
{
    std::packaged_task<int(int,int)> task(countdown); // 設置 packaged_task
    std::future<int> ret = task.get_future(); // 獲得與 packaged_task 共享狀態相關聯的 future 對象.

    std::thread th(std::move(task), 10, 0);   //創建一個新線程完成計數任務.

    int value = ret.get();                    // 等待任務完成並獲取結果.

    std::cout << "The countdown lasted for " << value << " seconds.\n";

    th.join();
    return 0;
}
複製代碼

執行結果爲:

複製代碼
concurrency ) ./Packaged_Task1 
10
9
8
7
6
5
4
3
2
1
Finished!
The countdown lasted for 10 seconds.
複製代碼

std::packaged_task 構造函數

default (1)
packaged_task() noexcept;
initialization (2)
template <class Fn>
  explicit packaged_task (Fn&& fn);
with allocator (3)
template <class Fn, class Alloc>
  explicit packaged_task (allocator_arg_t aa, const Alloc& alloc, Fn&& fn);
copy [deleted] (4)
packaged_task (const packaged_task&) = delete;
move (5)
packaged_task (packaged_task&& x) noexcept;

std::packaged_task 構造函數共有 5 中形式,不過拷貝構造已經被禁用了。下面簡單地介紹一下上述幾種構造函數的語義:

  1. 默認構造函數,初始化一個空的共享狀態,並且該 packaged_task 對象無包裝任務。
  2. 初始化一個共享狀態,並且被包裝任務由參數 fn 指定。
  3. 帶自定義內存分配器的構造函數,與默認構造函數類似,但是使用自定義分配器來分配共享狀態。
  4. 拷貝構造函數,被禁用。
  5. 移動構造函數。

下面例子介紹了各類構造函數的用法:

複製代碼
#include <iostream>     // std::cout
#include <utility>      // std::move
#include <future>       // std::packaged_task, std::future
#include <thread>       // std::thread

int main ()
{
    std::packaged_task<int(int)> foo; // 默認構造函數.

    // 使用 lambda 表達式初始化一個 packaged_task 對象.
    std::packaged_task<int(int)> bar([](int x){return x*2;});

    foo = std::move(bar); // move-賦值操作,也是 C++11 中的新特性.

    // 獲取與 packaged_task 共享狀態相關聯的 future 對象.
    std::future<int> ret = foo.get_future();

    std::thread(std::move(foo), 10).detach(); // 產生線程,調用被包裝的任務.

    int value = ret.get(); // 等待任務完成並獲取結果.
    std::cout << "The double of 10 is " << value << ".\n";

return 0;
}
複製代碼

與 std::promise 類似, std::packaged_task 也禁用了普通的賦值操作運算,只允許 move 賦值運算。

std::packaged_task::valid 介紹

檢查當前 packaged_task 是否和一個有效的共享狀態相關聯,對於由默認構造函數生成的 packaged_task 對象,該函數返回 false,除非中間進行了 move 賦值操作或者 swap 操作。

請看下例:

複製代碼
#include <iostream>     // std::cout
#include <utility>      // std::move
#include <future>       // std::packaged_task, std::future
#include <thread>       // std::thread

// 在新線程中啓動一個 int(int) packaged_task.
std::future<int> launcher(std::packaged_task<int(int)>& tsk, int arg)
{
    if (tsk.valid()) {
        std::future<int> ret = tsk.get_future();
        std::thread (std::move(tsk),arg).detach();
        return ret;
    }
    else return std::future<int>();
}

int main ()
{
    std::packaged_task<int(int)> tsk([](int x){return x*2;});

    std::future<int> fut = launcher(tsk,25);

    std::cout << "The double of 25 is " << fut.get() << ".\n";

    return 0;
}
複製代碼

std::packaged_task::get_future 介紹

返回一個與 packaged_task 對象共享狀態相關的 future 對象。返回的 future 對象可以獲得由另外一個線程在該 packaged_task 對象的共享狀態上設置的某個值或者異常。

請看例子(其實前面已經講了 get_future 的例子):

複製代碼
#include <iostream>     // std::cout
#include <utility>      // std::move
#include <future>       // std::packaged_task, std::future
#include <thread>       // std::thread

int main ()
{
    std::packaged_task<int(int)> tsk([](int x) { return x * 3; })); // package task

    std::future<int> fut = tsk.get_future();   // 獲取 future 對象.

    std::thread(std::move(tsk), 100).detach();   // 生成新線程並調用packaged_task.

    int value = fut.get();                     // 等待任務完成, 並獲取結果.

    std::cout << "The triple of 100 is " << value << ".\n";

    return 0;
}
複製代碼

std::packaged_task::operator()(Args... args) 介紹

調用該 packaged_task 對象所包裝的對象(通常爲函數指針,函數對象,lambda 表達式等),傳入的參數爲 args. 調用該函數一般會發生兩種情況:

  • 如果成功調用 packaged_task 所包裝的對象,則返回值(如果被包裝的對象有返回值的話)被保存在 packaged_task 的共享狀態中。
  • 如果調用 packaged_task 所包裝的對象失敗,並且拋出了異常,則異常也會被保存在 packaged_task 的共享狀態中。

以上兩種情況都使共享狀態的標誌變爲 ready,因此其他等待該共享狀態的線程可以獲取共享狀態的值或者異常並繼續執行下去。

共享狀態的值可以通過在 future 對象(由 get_future獲得)上調用 get 來獲得。

由於被包裝的任務在 packaged_task 構造時指定,因此調用 operator() 的效果由 packaged_task 對象構造時所指定的可調用對象來決定:

  • 如果被包裝的任務是函數指針或者函數對象,調用 std::packaged_task::operator() 只是將參數傳遞給被包裝的對象。
  • 如果被包裝的任務是指向類的非靜態成員函數的指針,那麼 std::packaged_task::operator() 的第一個參數應該指定爲成員函數被調用的那個對象,剩餘的參數作爲該成員函數的參數。
  • 如果被包裝的任務是指向類的非靜態成員變量,那麼 std::packaged_task::operator() 只允許單個參數。

std::packaged_task::make_ready_at_thread_exit 介紹

該函數會調用被包裝的任務,並向任務傳遞參數,類似 std::packaged_task 的 operator() 成員函數。但是與 operator() 函數不同的是,make_ready_at_thread_exit 並不會立即設置共享狀態的標誌爲 ready,而是在線程退出時設置共享狀態的標誌。

如果與該 packaged_task 共享狀態相關聯的 future 對象在 future::get 處等待,則當前的 future::get 調用會被阻塞,直到線程退出。而一旦線程退出,future::get 調用繼續執行,或者拋出異常。

注意,該函數已經設置了 promise 共享狀態的值,如果在線程結束之前有其他設置或者修改共享狀態的值的操作,則會拋出 future_error( promise_already_satisfied )。

std::packaged_task::reset() 介紹

重置 packaged_task 的共享狀態,但是保留之前的被包裝的任務。請看例子,該例子中,packaged_task 被重用了多次:

複製代碼
#include <iostream>     // std::cout
#include <utility>      // std::move
#include <future>       // std::packaged_task, std::future
#include <thread>       // std::thread

// a simple task:
int triple (int x) { return x*3; }

int main ()
{
    std::packaged_task<int(int)> tsk (triple); // package task


    std::future<int> fut = tsk.get_future();
    std::thread (std::move(tsk), 100).detach();
    std::cout << "The triple of 100 is " << fut.get() << ".\n";


    // re-use same task object:
    tsk.reset();
    fut = tsk.get_future();
    std::thread(std::move(tsk), 200).detach();
    std::cout << "Thre triple of 200 is " << fut.get() << ".\n";

    return 0;
}
複製代碼

std::packaged_task::swap() 介紹

交換 packaged_task 的共享狀態。

好了,std::packaged_task 介紹到這裏,本文參考了 http://www.cplusplus.com/reference/future/packaged_task/ 相關的內容。後一篇文章我將向大家介紹 std::future,std::shared_future 以及 std::future_error,另外還會介紹 <future> 頭文件中的 std::async,std::future_category 函數以及相關枚舉類型。



C++11 併發指南四(<future> 詳解三 std::future & std::shared_future)



上一講《C++11 併發指南四(<future> 詳解二 std::packaged_task 介紹)》主要介紹了 <future> 頭文件中的 std::packaged_task 類,本文主要介紹 std::future,std::shared_future 以及 std::future_error,另外還會介紹 <future> 頭文件中的 std::async,std::future_category 函數以及相關枚舉類型。

std::future 介紹

前面已經多次提到過 std::future,那麼 std::future 究竟是什麼呢?簡單地說,std::future 可以用來獲取異步任務的結果,因此可以把它當成一種簡單的線程間同步的手段。std::future 通常由某個 Provider 創建,你可以把 Provider 想象成一個異步任務的提供者,Provider 在某個線程中設置共享狀態的值,與該共享狀態相關聯的 std::future 對象調用 get(通常在另外一個線程中) 獲取該值,如果共享狀態的標誌不爲 ready,則調用 std::future::get 會阻塞當前的調用者,直到 Provider 設置了共享狀態的值(此時共享狀態的標誌變爲 ready),std::future::get 返回異步任務的值或異常(如果發生了異常)。

一個有效(valid)的 std::future 對象通常由以下三種 Provider 創建,並和某個共享狀態相關聯。Provider 可以是函數或者類,其實我們前面都已經提到了,他們分別是:

一個 std::future 對象只有在有效(valid)的情況下才有用(useful),由 std::future 默認構造函數創建的 future 對象不是有效的(除非當前非有效的 future 對象被 move 賦值另一個有效的 future 對象)。

 在一個有效的 future 對象上調用 get 會阻塞當前的調用者,直到 Provider 設置了共享狀態的值或異常(此時共享狀態的標誌變爲 ready),std::future::get 將返回異步任務的值或異常(如果發生了異常)。

下面以一個簡單的例子說明上面一段文字吧(參考):

複製代碼
// future example
#include <iostream>             // std::cout
#include <future>               // std::async, std::future
#include <chrono>               // std::chrono::milliseconds

// a non-optimized way of checking for prime numbers:
bool
is_prime(int x)
{
    for (int i = 2; i < x; ++i)
        if (x % i == 0)
            return false;
    return true;
}

int
main()
{
    // call function asynchronously:
    std::future < bool > fut = std::async(is_prime, 444444443);

    // do something while waiting for function to set future:
    std::cout << "checking, please wait";
    std::chrono::milliseconds span(100);
    while (fut.wait_for(span) == std::future_status::timeout)
        std::cout << '.';

    bool x = fut.get();         // retrieve return value

    std::cout << "\n444444443 " << (x ? "is" : "is not") << " prime.\n";

    return 0;
}
複製代碼

 std::future 成員函數

std::future 構造函數

std::future 一般由 std::async, std::promise::get_future, std::packaged_task::get_future 創建,不過也提供了構造函數,如下表所示:

default (1)
future() noexcept;
copy [deleted] (2)
future (const future&) = delete;
move (3)
future (future&& x) noexcept;

 

不過 std::future 的拷貝構造函數是被禁用的,只提供了默認的構造函數和 move 構造函數(注:C++ 新特新)。另外,std::future 的普通賦值操作也被禁用,只提供了 move 賦值操作。如下代碼所示:

 std::future<int> fut;           // 默認構造函數
  fut = std::async(do_some_task);   // move-賦值操作。

std::future::share()

返回一個 std::shared_future 對象(本文後續內容將介紹 std::shared_future ),調用該函數之後,該 std::future 對象本身已經不和任何共享狀態相關聯,因此該 std::future 的狀態不再是 valid 的了。

複製代碼
#include <iostream>       // std::cout
#include <future>         // std::async, std::future, std::shared_future

int do_get_value() { return 10; }

int main ()
{
    std::future<int> fut = std::async(do_get_value);
    std::shared_future<int> shared_fut = fut.share();

    // 共享的 future 對象可以被多次訪問.
    std::cout << "value: " << shared_fut.get() << '\n';
    std::cout << "its double: " << shared_fut.get()*2 << '\n';

    return 0;
}
複製代碼

std::future::get()

std::future::get 一共有三種形式,如下表所示(參考):

generic template (1)
T get();
reference specialization (2)
R& future<R&>::get();       // when T is a reference type (R&)
void specialization (3)
void future<void>::get();   // when T is void

當與該 std::future 對象相關聯的共享狀態標誌變爲 ready 後,調用該函數將返回保存在共享狀態中的值,如果共享狀態的標誌不爲 ready,則調用該函數會阻塞當前的調用者,而此後一旦共享狀態的標誌變爲 ready,get 返回 Provider 所設置的共享狀態的值或者異常(如果拋出了異常)。

請看下面的程序:

複製代碼
#include <iostream>       // std::cin, std::cout, std::ios
#include <functional>     // std::ref
#include <thread>         // std::thread
#include <future>         // std::promise, std::future
#include <exception>      // std::exception, std::current_exception

void get_int(std::promise<int>& prom) {
    int x;
    std::cout << "Please, enter an integer value: ";
    std::cin.exceptions (std::ios::failbit);   // throw on failbit
    try {
        std::cin >> x;                         // sets failbit if input is not int
        prom.set_value(x);
    } catch (std::exception&) {
        prom.set_exception(std::current_exception());
    }
}

void print_int(std::future<int>& fut) {
    try {
        int x = fut.get();
        std::cout << "value: " << x << '\n';
    } catch (std::exception& e) {
        std::cout << "[exception caught: " << e.what() << "]\n";
    }
}

int main ()
{
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();

    std::thread th1(get_int, std::ref(prom));
    std::thread th2(print_int, std::ref(fut));

    th1.join();
    th2.join();
    return 0;
}
複製代碼

std::future::valid()

檢查當前的 std::future 對象是否有效,即釋放與某個共享狀態相關聯。一個有效的 std::future 對象只能通過 std::async(), std::future::get_future 或者 std::packaged_task::get_future 來初始化。另外由 std::future 默認構造函數創建的 std::future 對象是無效(invalid)的,當然通過 std::future 的 move 賦值後該 std::future 對象也可以變爲 valid。

複製代碼
#include <iostream>       // std::cout
#include <future>         // std::async, std::future
#include <utility>        // std::move

int do_get_value() { return 11; }

int main ()
{
    // 由默認構造函數創建的 std::future 對象,
    // 初始化時該 std::future 對象處於爲 invalid 狀態.
    std::future<int> foo, bar;
    foo = std::async(do_get_value); // move 賦值, foo 變爲 valid.
    bar = std::move(foo); // move 賦值, bar 變爲 valid, 而 move 賦值以後 foo 變爲 invalid.

    if (foo.valid())
        std::cout << "foo's value: " << foo.get() << '\n';
    else
        std::cout << "foo is not valid\n";

    if (bar.valid())
        std::cout << "bar's value: " << bar.get() << '\n';
    else
        std::cout << "bar is not valid\n";

    return 0;
}
複製代碼

std::future::wait()

等待與當前std::future 對象相關聯的共享狀態的標誌變爲 ready.

如果共享狀態的標誌不是 ready(此時 Provider 沒有在共享狀態上設置值(或者異常)),調用該函數會被阻塞當前線程,直到共享狀態的標誌變爲 ready。
一旦共享狀態的標誌變爲 ready,wait() 函數返回,當前線程被解除阻塞,但是 wait() 並不讀取共享狀態的值或者異常。下面的代碼說明了 std::future::wait() 的用法(參考

複製代碼
#include <iostream>                // std::cout
#include <future>                // std::async, std::future
#include <chrono>                // std::chrono::milliseconds

// a non-optimized way of checking for prime numbers:
bool do_check_prime(int x) // 爲了體現效果, 該函數故意沒有優化.
{
    for (int i = 2; i < x; ++i)
        if (x % i == 0)
            return false;
    return true;
}

int main()
{
    // call function asynchronously:
    std::future < bool > fut = std::async(do_check_prime, 194232491);

    std::cout << "Checking...\n";
    fut.wait();

    std::cout << "\n194232491 ";
    if (fut.get()) // guaranteed to be ready (and not block) after wait returns
        std::cout << "is prime.\n";
    else
        std::cout << "is not prime.\n";

    return 0;
}
複製代碼

執行結果如下:

concurrency ) ./Future-wait 
Checking...

194232491 is prime.
concurrency ) 

std::future::wait_for()

與 std::future::wait() 的功能類似,即等待與該 std::future 對象相關聯的共享狀態的標誌變爲 ready,該函數原型如下:

template <class Rep, class Period>
  future_status wait_for (const chrono::duration<Rep,Period>& rel_time) const;

而與 std::future::wait() 不同的是,wait_for() 可以設置一個時間段 rel_time,如果共享狀態的標誌在該時間段結束之前沒有被 Provider 設置爲 ready,則調用 wait_for 的線程被阻塞,在等待了 rel_time 的時間長度後 wait_until() 返回,返回值如下:

返回值描述
future_status::ready共享狀態的標誌已經變爲 ready,即 Provider 在共享狀態上設置了值或者異常。
future_status::timeout超時,即在規定的時間內共享狀態的標誌沒有變爲 ready。
future_status::deferred共享狀態包含一個 deferred 函數。

請看下面的例子:

複製代碼
#include <iostream>                // std::cout
#include <future>                // std::async, std::future
#include <chrono>                // std::chrono::milliseconds

// a non-optimized way of checking for prime numbers:
bool do_check_prime(int x) // 爲了體現效果, 該函數故意沒有優化.
{
    for (int i = 2; i < x; ++i)
        if (x % i == 0)
            return false;
    return true;
}

int main()
{
    // call function asynchronously:
    std::future < bool > fut = std::async(do_check_prime, 194232491);

    std::cout << "Checking...\n";
    std::chrono::milliseconds span(1000); // 設置超時間隔.

    // 如果超時,則輸出".",繼續等待
    while (fut.wait_for(span) == std::future_status::timeout)
        std::cout << '.';

    std::cout << "\n194232491 ";
    if (fut.get()) // guaranteed to be ready (and not block) after wait returns
        std::cout << "is prime.\n";
    else
        std::cout << "is not prime.\n";

    return 0;
}
複製代碼

std::future::wait_until()

與 std::future::wait() 的功能類似,即等待與該 std::future 對象相關聯的共享狀態的標誌變爲 ready,該函數原型如下:

template <class Rep, class Period>
  future_status wait_until (const chrono::time_point<Clock,Duration>& abs_time) const;

而 與 std::future::wait() 不同的是,wait_until() 可以設置一個系統絕對時間點 abs_time,如果共享狀態的標誌在該時間點到來之前沒有被 Provider 設置爲 ready,則調用 wait_until 的線程被阻塞,在 abs_time 這一時刻到來之後 wait_for() 返回,返回值如下:

返回值描述
future_status::ready共享狀態的標誌已經變爲 ready,即 Provider 在共享狀態上設置了值或者異常。
future_status::timeout超時,即在規定的時間內共享狀態的標誌沒有變爲 ready。
future_status::deferred共享狀態包含一個 deferred 函數。

 

std::shared_future 介紹

std::shared_future 與 std::future 類似,但是 std::shared_future 可以拷貝、多個 std::shared_future 可以共享某個共享狀態的最終結果(即共享狀態的某個值或者異常)。shared_future 可以通過某個 std::future 對象隱式轉換(參見 std::shared_future 的構造函數),或者通過 std::future::share() 顯示轉換,無論哪種轉換,被轉換的那個 std::future 對象都會變爲 not-valid.

std::shared_future 構造函數

std::shared_future 共有四種構造函數,如下表所示:

default (1)
shared_future() noexcept;
copy (2)
shared_future (const shared_future& x);
move (3)
shared_future (shared_future&& x) noexcept;
move from future (4)
shared_future (future<T>&& x) noexcept;

最後 move from future(4) 即從一個有效的 std::future 對象構造一個 std::shared_future,構造之後 std::future 對象 x 變爲無效(not-valid)。

std::shared_future 其他成員函數

std::shared_future 的成員函數和 std::future 大部分相同,如下(每個成員函數都給出了連接):

std::future_error 介紹

class future_error : public logic_error;

std::future_error 繼承子 C++ 標準異常體系中的 logic_error,有關 C++ 異常的繼承體系,請參考相關的C++教程 ;-)。

其他與 std::future 相關的函數介紹

與 std::future 相關的函數主要是 std::async(),原型如下:

unspecified policy (1)
template <class Fn, class... Args>
  future<typename result_of<Fn(Args...)>::type>
    async(Fn&& fn, Args&&... args);
specific policy (2)
template <class Fn, class... Args>
  future<typename result_of<Fn(Args...)>::type>
    async(launch policy, Fn&& fn, Args&&... args);

上面兩組 std::async() 的不同之處是第一類 std::async 沒有指定異步任務(即執行某一函數)的啓動策略(launch policy),而第二類函數指定了啓動策略,詳見 std::launch 枚舉類型,指定啓動策略的函數的 policy 參數可以是launch::async,launch::deferred,以及兩者的按位或( | )。

std::async() 的 fn 和 args 參數用來指定異步任務及其參數。另外,std::async() 返回一個 std::future 對象,通過該對象可以獲取異步任務的值或異常(如果異步任務拋出了異常)。

下面介紹一下 std::async 的用法。

複製代碼
#include <stdio.h>
#include <stdlib.h>

#include <cmath>
#include <chrono>
#include <future>
#include <iostream>

double ThreadTask(int n) {
    std::cout << std::this_thread::get_id()
        << " start computing..." << std::endl;

    double ret = 0;
    for (int i = 0; i <= n; i++) {
        ret += std::sin(i);
    }

    std::cout << std::this_thread::get_id()
        << " finished computing..." << std::endl;
    return ret;
}

int main(int argc, const char *argv[])
{
    std::future<double> f(std::async(std::launch::async, ThreadTask, 100000000));

#if 0
    while(f.wait_until(std::chrono::system_clock::now() + std::chrono::seconds(1))
            != std::future_status::ready) {
        std::cout << "task is running...\n";
    }
#else
    while(f.wait_for(std::chrono::seconds(1))
            != std::future_status::ready) {
        std::cout << "task is running...\n";
    }
#endif

    std::cout << f.get() << std::endl;

    return EXIT_SUCCESS;
}
複製代碼

 

其他與 std::future 相關的枚舉類介紹

下面介紹與 std::future 相關的枚舉類型。與 std::future 相關的枚舉類型包括:

enum class future_errc;
enum class future_status;
enum class launch;

下面分別介紹以上三種枚舉類型:

std::future_errc 類型

std::future_errc 類型描述如下(參考):

類型
取值
描述
broken_promise0與該 std::future 共享狀態相關聯的 std::promise 對象在設置值或者異常之前一被銷燬。
future_already_retrieved1與該 std::future 對象相關聯的共享狀態的值已經被當前 Provider 獲取了,即調用了 std::future::get 函數。
promise_already_satisfied2std::promise 對象已經對共享狀態設置了某一值或者異常。
no_state3無共享狀態。

std::future_status 類型(參考

std::future_status 類型主要用在 std::future(或std::shared_future)中的 wait_for 和 wait_until 兩個函數中的。

類型取值
描述
future_status::ready0wait_for(或wait_until) 因爲共享狀態的標誌變爲 ready 而返回。
future_status::timeout1超時,即 wait_for(或wait_until) 因爲在指定的時間段(或時刻)內共享狀態的標誌依然沒有變爲 ready而返回。
future_status::deferred2共享狀態包含了 deferred 函數。

std::launch 類型

該枚舉類型主要是在調用 std::async 設置異步任務的啓動策略的。

類型描述
launch::asyncAsynchronous: 異步任務會在另外一個線程中調用,並通過共享狀態返回異步任務的結果(一般是調用 std::future::get() 獲取異步任務的結果)。
launch::deferredDeferred: 異步任務將會在共享狀態被訪問時調用,相當與按需調用(即延遲(deferred)調用)。

請看下例(參考):

複製代碼
#include <iostream>                // std::cout
#include <future>                // std::async, std::future, std::launch
#include <chrono>                // std::chrono::milliseconds
#include <thread>                // std::this_thread::sleep_for

void
do_print_ten(char c, int ms)
{
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(ms));
        std::cout << c;
    }
}

int
main()
{
    std::cout << "with launch::async:\n";
    std::future < void >foo =
        std::async(std::launch::async, do_print_ten, '*', 100);
    std::future < void >bar =
        std::async(std::launch::async, do_print_ten, '@', 200);
    // async "get" (wait for foo and bar to be ready):
    foo.get();
    bar.get();
    std::cout << "\n\n";

    std::cout << "with launch::deferred:\n";
    foo = std::async(std::launch::deferred, do_print_ten, '*', 100);
    bar = std::async(std::launch::deferred, do_print_ten, '@', 200);
    // deferred "get" (perform the actual calls):
    foo.get();
    bar.get();
    std::cout << '\n';

    return 0;
}
複製代碼

在我的機器上執行結果:

with launch::async:
*@**@**@**@**@*@@@@@

with launch::deferred:
**********@@@@@@@@@@

 



C++11 併發指南五(std::condition_variable 詳解)



前面三講《C++11 併發指南二(std::thread 詳解)》,《C++11 併發指南三(std::mutex 詳解)》分別介紹了 std::thread,std::mutex,std::future 等相關內容,相信讀者對 C++11 中的多線程編程有了一個最基本的認識,本文將介紹 C++11 標準中 <condition_variable> 頭文件裏面的類和相關函數。

<condition_variable > 頭文件主要包含了與條件變量相關的類和函數。相關的類包括 std::condition_variable 和 std::condition_variable_any,還有枚舉類型std::cv_status。另外還包括函數 std::notify_all_at_thread_exit(),下面分別介紹一下以上幾種類型。

std::condition_variable 類介紹

std::condition_variable 是條件變量,更多有關條件變量的定義參考維基百科。Linux 下使用 Pthread 庫中的 pthread_cond_*() 函數提供了與條件變量相關的功能, Windows 則參考 MSDN

當 std::condition_variable 對象的某個 wait 函數被調用的時候,它使用 std::unique_lock(通過 std::mutex) 來鎖住當前線程。當前線程會一直被阻塞,直到另外一個線程在相同的 std::condition_variable 對象上調用了 notification 函數來喚醒當前線程。

std::condition_variable 對象通常使用 std::unique_lock<std::mutex> 來等待,如果需要使用另外的 lockable 類型,可以使用 std::condition_variable_any 類,本文後面會講到 std::condition_variable_any 的用法。

首先我們來看一個簡單的例子

複製代碼
#include <iostream>                // std::cout
#include <thread>                // std::thread
#include <mutex>                // std::mutex, std::unique_lock
#include <condition_variable>    // std::condition_variable

std::mutex mtx; // 全局互斥鎖.
std::condition_variable cv; // 全局條件變量.
bool ready = false; // 全局標誌位.

void do_print_id(int id)
{
    std::unique_lock <std::mutex> lck(mtx);
    while (!ready) // 如果標誌位不爲 true, 則等待...
        cv.wait(lck); // 當前線程被阻塞, 當全局標誌位變爲 true 之後,
    // 線程被喚醒, 繼續往下執行打印線程編號id.
    std::cout << "thread " << id << '\n';
}

void go()
{
    std::unique_lock <std::mutex> lck(mtx);
    ready = true; // 設置全局標誌位爲 true.
    cv.notify_all(); // 喚醒所有線程.
}

int main()
{
    std::thread threads[10];
    // spawn 10 threads:
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(do_print_id, i);

    std::cout << "10 threads ready to race...\n";
    go(); // go!

  for (auto & th:threads)
        th.join();

    return 0;
}
複製代碼

執行結果如下:

複製代碼
concurrency ) ./ConditionVariable-basic1 
10 threads ready to race...
thread 1
thread 0
thread 2
thread 3
thread 4
thread 5
thread 6
thread 7
thread 8
thread 9
複製代碼

好了,對條件變量有了一個基本的瞭解之後,我們來看看 std::condition_variable 的各個成員函數。

std::condition_variable 構造函數

default (1)
condition_variable();
copy [deleted] (2)
condition_variable (const condition_variable&) = delete;

std::condition_variable 的拷貝構造函數被禁用,只提供了默認構造函數。

std::condition_variable::wait() 介紹

unconditional (1)
void wait (unique_lock<mutex>& lck);
predicate (2)
template <class Predicate>
  void wait (unique_lock<mutex>& lck, Predicate pred);

std::condition_variable 提供了兩種 wait() 函數。當前線程調用 wait() 後將被阻塞(此時當前線程應該獲得了鎖(mutex),不妨設獲得鎖 lck),直到另外某個線程調用 notify_* 喚醒了當前線程。

在線程被阻塞時,該函數會自動調用 lck.unlock() 釋放鎖,使得其他被阻塞在鎖競爭上的線程得以繼續執行。另外,一旦當前線程獲得通知(notified,通常是另外某個線程調用 notify_* 喚醒了當前線程),wait() 函數也是自動調用 lck.lock(),使得 lck 的狀態和 wait 函數被調用時相同。

在第二種情況下(即設置了 Predicate),只有當 pred 條件爲 false 時調用 wait() 纔會阻塞當前線程,並且在收到其他線程的通知後只有當 pred 爲 true 時纔會被解除阻塞。因此第二種情況類似以下代碼:

while (!pred()) wait(lck);

請看下面例子(參考):

複製代碼
#include <iostream>                // std::cout
#include <thread>                // std::thread, std::this_thread::yield
#include <mutex>                // std::mutex, std::unique_lock
#include <condition_variable>    // std::condition_variable

std::mutex mtx;
std::condition_variable cv;

int cargo = 0;
bool shipment_available()
{
    return cargo != 0;
}

// 消費者線程.
void consume(int n)
{
    for (int i = 0; i < n; ++i) {
        std::unique_lock <std::mutex> lck(mtx);
        cv.wait(lck, shipment_available);
        std::cout << cargo << '\n';
        cargo = 0;
    }
}

int main()
{
    std::thread consumer_thread(consume, 10); // 消費者線程.

    // 主線程爲生產者線程, 生產 10 個物品.
    for (int i = 0; i < 10; ++i) {
        while (shipment_available())
            std::this_thread::yield();
        std::unique_lock <std::mutex> lck(mtx);
        cargo = i + 1;
        cv.notify_one();
    }

    consumer_thread.join();

    return 0;
}
複製代碼

程序執行結果如下:

複製代碼
concurrency ) ./ConditionVariable-wait 
1
2
3
4
5
6
7
8
9
10
複製代碼

std::condition_variable::wait_for() 介紹

unconditional (1)
template <class Rep, class Period>
  cv_status wait_for (unique_lock<mutex>& lck,
                      const chrono::duration<Rep,Period>& rel_time);
predicate (2)
template <class Rep, class Period, class Predicate>
       bool wait_for (unique_lock<mutex>& lck,
                      const chrono::duration<Rep,Period>& rel_time, Predicate pred);

與 std::condition_variable::wait() 類似,不過 wait_for 可以指定一個時間段,在當前線程收到通知或者指定的時間 rel_time 超時之前,該線程都會處於阻塞狀態。而一旦超時或者收到了其他線程的通知,wait_for 返回,剩下的處理步驟和 wait() 類似。

另外,wait_for 的重載版本(predicte(2))的最後一個參數 pred 表示 wait_for 的預測條件,只有當 pred 條件爲 false 時調用 wait() 纔會阻塞當前線程,並且在收到其他線程的通知後只有當 pred 爲 true 時纔會被解除阻塞,因此相當於如下代碼:

return wait_until (lck, chrono::steady_clock::now() + rel_time, std::move(pred));

請看下面的例子(參考),下面的例子中,主線程等待 th 線程輸入一個值,然後將 th 線程從終端接收的值打印出來,在 th 線程接受到值之前,主線程一直等待,每個一秒超時一次,並打印一個 ".":

複製代碼
#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <chrono>             // std::chrono::seconds
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable, std::cv_status

std::condition_variable cv;

int value;

void do_read_value()
{
    std::cin >> value;
    cv.notify_one();
}

int main ()
{
    std::cout << "Please, enter an integer (I'll be printing dots): \n";
    std::thread th(do_read_value);

    std::mutex mtx;
    std::unique_lock<std::mutex> lck(mtx);
    while (cv.wait_for(lck,std::chrono::seconds(1)) == std::cv_status::timeout) {
        std::cout << '.';
        std::cout.flush();
    }

    std::cout << "You entered: " << value << '\n';

    th.join();
    return 0;
}
複製代碼

std::condition_variable::wait_until 介紹

unconditional (1)
template <class Clock, class Duration>
  cv_status wait_until (unique_lock<mutex>& lck,
                        const chrono::time_point<Clock,Duration>& abs_time);
predicate (2)
template <class Clock, class Duration, class Predicate>
       bool wait_until (unique_lock<mutex>& lck,
                        const chrono::time_point<Clock,Duration>& abs_time,
                        Predicate pred);

與 std::condition_variable::wait_for 類似,但是 wait_until 可以指定一個時間點,在當前線程收到通知或者指定的時間點 abs_time 超時之前,該線程都會處於阻塞狀態。而一旦超時或者收到了其他線程的通知,wait_until 返回,剩下的處理步驟和 wait_until() 類似。

另外,wait_until 的重載版本(predicte(2))的最後一個參數 pred 表示 wait_until 的預測條件,只有當 pred 條件爲 false 時調用 wait() 纔會阻塞當前線程,並且在收到其他線程的通知後只有當 pred 爲 true 時纔會被解除阻塞,因此相當於如下代碼:

while (!pred())
  if ( wait_until(lck,abs_time) == cv_status::timeout)
    return pred();
return true;

std::condition_variable::notify_one() 介紹

喚醒某個等待(wait)線程。如果當前沒有等待線程,則該函數什麼也不做,如果同時存在多個等待線程,則喚醒某個線程是不確定的(unspecified)。

請看下例(參考):

複製代碼
#include <iostream>                // std::cout
#include <thread>                // std::thread
#include <mutex>                // std::mutex, std::unique_lock
#include <condition_variable>    // std::condition_variable

std::mutex mtx;
std::condition_variable cv;

int cargo = 0; // shared value by producers and consumers

void consumer()
{
    std::unique_lock < std::mutex > lck(mtx);
    while (cargo == 0)
        cv.wait(lck);
    std::cout << cargo << '\n';
    cargo = 0;
}

void producer(int id)
{
    std::unique_lock < std::mutex > lck(mtx);
    cargo = id;
    cv.notify_one();
}

int main()
{
    std::thread consumers[10], producers[10];

    // spawn 10 consumers and 10 producers:
    for (int i = 0; i < 10; ++i) {
        consumers[i] = std::thread(consumer);
        producers[i] = std::thread(producer, i + 1);
    }

    // join them back:
    for (int i = 0; i < 10; ++i) {
        producers[i].join();
        consumers[i].join();
    }

    return 0;
}
複製代碼

std::condition_variable::notify_all() 介紹

喚醒所有的等待(wait)線程。如果當前沒有等待線程,則該函數什麼也不做。請看下面的例子:

複製代碼
#include <iostream>                // std::cout
#include <thread>                // std::thread
#include <mutex>                // std::mutex, std::unique_lock
#include <condition_variable>    // std::condition_variable

std::mutex mtx; // 全局互斥鎖.
std::condition_variable cv; // 全局條件變量.
bool ready = false; // 全局標誌位.

void do_print_id(int id)
{
    std::unique_lock <std::mutex> lck(mtx);
    while (!ready) // 如果標誌位不爲 true, 則等待...
        cv.wait(lck); // 當前線程被阻塞, 當全局標誌位變爲 true 之後,
    // 線程被喚醒, 繼續往下執行打印線程編號id.
    std::cout << "thread " << id << '\n';
}

void go()
{
    std::unique_lock <std::mutex> lck(mtx);
    ready = true; // 設置全局標誌位爲 true.
    cv.notify_all(); // 喚醒所有線程.
}

int main()
{
    std::thread threads[10];
    // spawn 10 threads:
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(do_print_id, i);

    std::cout << "10 threads ready to race...\n";
    go(); // go!

  for (auto & th:threads)
        th.join();

    return 0;
}
複製代碼

 std::condition_variable_any 介紹

與 std::condition_variable 類似,只不過 std::condition_variable_any 的 wait 函數可以接受任何 lockable 參數,而 std::condition_variable 只能接受 std::unique_lock<std::mutex> 類型的參數,除此以外,和 std::condition_variable 幾乎完全一樣。

std::cv_status 枚舉類型介紹

cv_status::no_timeoutwait_for 或者 wait_until 沒有超時,即在規定的時間段內線程收到了通知。
cv_status::timeoutwait_for 或者 wait_until 超時。

std::notify_all_at_thread_exit

函數原型爲:

void notify_all_at_thread_exit (condition_variable& cond, unique_lock<mutex> lck);

當調用該函數的線程退出時,所有在 cond 條件變量上等待的線程都會收到通知。請看下例(參考):

複製代碼
#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id (int id) {
  std::unique_lock<std::mutex> lck(mtx);
  while (!ready) cv.wait(lck);
  // ...
  std::cout << "thread " << id << '\n';
}

void go() {
  std::unique_lock<std::mutex> lck(mtx);
  std::notify_all_at_thread_exit(cv,std::move(lck));
  ready = true;
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(print_id,i);
  std::cout << "10 threads ready to race...\n";

  std::thread(go).detach();   // go!

  for (auto& th : threads) th.join();

  return 0;
}
複製代碼

 

好了,到此爲止,<condition_variable> 頭文件中的兩個條件變量類(std::condition_variable 和 std::condition_variable_any)、枚舉類型(std::cv_status)、以及輔助函數(std::notify_all_at_thread_exit())都已經介紹完了。從下一章開始我會逐步開始介紹 <atomic> 頭文件中的內容,後續的文章還會介紹 C++11 的內存模型,涉及內容稍微底層一些,希望大家能夠保持興趣,學完 C++11 併發編程,如果你發現本文中的錯誤,也請給我反饋 ;-)。


C++11 併發指南六(atomic 類型詳解一 atomic_flag 介紹)



C++11 併發指南已經寫了 5 章,前五章重點介紹了多線程編程方面的內容,但大部分內容只涉及多線程、互斥量、條件變量和異步編程相關的 API,C++11 程序員完全可以不必知道這些 API 在底層是如何實現的,只需要清楚 C++11 多線程和異步編程相關 API 的語義,然後熟加練習即可應付大部分多線程編碼需求。但是在很多極端的場合下爲了性能和效率,我們需要開發一些 lock-free 的算法和數據結構,前面幾章的內容可能就派不上用場了,因此從本文開始介紹 C++11 標準中 <atomic> 頭文件裏面的類和相關函數。

本文介紹 <atomic> 頭文件中最簡單的原子類型: atomic_flag。atomic_flag 一種簡單的原子布爾類型,只支持兩種操作,test-and-set 和 clear。

std::atomic_flag 構造函數

std::atomic_flag 構造函數如下:

  • atomic_flag() noexcept = default;
  • atomic_flag (const atomic_flag&T) = delete;

std::atomic_flag 只有默認構造函數,拷貝構造函數已被禁用,因此不能從其他的 std::atomic_flag 對象構造一個新的 std::atomic_flag 對象。

如果在初始化時沒有明確使用 ATOMIC_FLAG_INIT初始化,那麼新創建的 std::atomic_flag 對象的狀態是未指定的(unspecified)(既沒有被 set 也沒有被 clear。)另外,atomic_flag不能被拷貝,也不能 move 賦值。

ATOMIC_FLAG_INIT: 如果某個 std::atomic_flag 對象使用該宏初始化,那麼可以保證該 std::atomic_flag 對象在創建時處於 clear 狀態。

下面先看一個簡單的例子,main() 函數中創建了 10 個線程進行計數,率先完成計數任務的線程輸出自己的 ID,後續完成計數任務的線程不會輸出自身 ID:

複製代碼
#include <iostream>              // std::cout
#include <atomic>                // std::atomic, std::atomic_flag, ATOMIC_FLAG_INIT
#include <thread>                // std::thread, std::this_thread::yield
#include <vector>                // std::vector

std::atomic<bool> ready(false);    // can be checked without being set
std::atomic_flag winner = ATOMIC_FLAG_INIT;    // always set when checked

void count1m(int id)
{
    while (!ready) {
        std::this_thread::yield();
    } // 等待主線程中設置 ready 爲 true.

    for (int i = 0; i < 1000000; ++i) {
    } // 計數.

    // 如果某個線程率先執行完上面的計數過程,則輸出自己的 ID.
    // 此後其他線程執行 test_and_set 是 if 語句判斷爲 false,
    // 因此不會輸出自身 ID.
    if (!winner.test_and_set()) {
        std::cout << "thread #" << id << " won!\n";
    }
};

int main()
{
    std::vector<std::thread> threads;
    std::cout << "spawning 10 threads that count to 1 million...\n";
    for (int i = 1; i <= 10; ++i)
        threads.push_back(std::thread(count1m, i));
    ready = true;

    for (auto & th:threads)
        th.join();

    return 0;
}
複製代碼

多次執行結果如下:

複製代碼
atomic ) ./Atomic-Flag1 
spawning 10 threads that count to 1 million...
thread #6 won!
atomic ) ./Atomic-Flag1 
spawning 10 threads that count to 1 million...
thread #1 won!
atomic ) ./Atomic-Flag1 
spawning 10 threads that count to 1 million...
thread #5 won!
atomic ) ./Atomic-Flag1 
spawning 10 threads that count to 1 million...
thread #1 won!
atomic ) ./Atomic-Flag1 
spawning 10 threads that count to 1 million...
thread #1 won!
atomic ) ./Atomic-Flag1 
spawning 10 threads that count to 1 million...
thread #10 won!
複製代碼

std::atomic_flag::test_and_set 介紹

std::atomic_flag 的 test_and_set 函數原型如下:

bool test_and_set (memory_order sync = memory_order_seq_cst) volatile noexcept;
bool test_and_set (memory_order sync = memory_order_seq_cst) noexcept;

test_and_set() 函數檢查 std::atomic_flag 標誌,如果 std::atomic_flag 之前沒有被設置過,則設置 std::atomic_flag 的標誌,並返回先前該 std::atomic_flag 對象是否被設置過,如果之前 std::atomic_flag 對象已被設置,則返回 true,否則返回 false。

test-and-set 操作是原子的(因此 test-and-set 是原子 read-modify-write (RMW)操作)。

test_and_set 可以指定 Memory Order(後續的文章會詳細介紹 C++11 的 Memory Order,此處爲了完整性列出 test_and_set 參數 sync 的取值),取值如下:

 

Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent

 一個簡單的例子:

複製代碼
#include <iostream>                // std::cout
#include <atomic>                // std::atomic_flag
#include <thread>                // std::thread
#include <vector>                // std::vector
#include <sstream>                // std::stringstream

std::atomic_flag lock_stream = ATOMIC_FLAG_INIT;
std::stringstream stream;

void append_number(int x)
{
    while (lock_stream.test_and_set()) {
    }
    stream << "thread #" << x << '\n';
    lock_stream.clear();
}

int main()
{
    std::vector < std::thread > threads;
    for (int i = 1; i <= 10; ++i)
        threads.push_back(std::thread(append_number, i));
    for (auto & th:threads)
        th.join();

    std::cout << stream.str() << std::endl;;
    return 0;
}
複製代碼

執行結果如下:

複製代碼
thread #1
thread #2
thread #3
thread #4
thread #5
thread #6
thread #7
thread #8
thread #9
thread #10
複製代碼

std::atomic_flag::clear() 介紹

清除 std::atomic_flag 對象的標誌位,即設置 atomic_flag 的值爲 false。clear 函數原型如下:

void clear (memory_order sync = memory_order_seq_cst) volatile noexcept;
void clear (memory_order sync = memory_order_seq_cst) noexcept;

清除 std::atomic_flag 標誌使得下一次調用 std::atomic_flag::test_and_set 返回 false。

std::atomic_flag::clear() 可以指定 Memory Order(後續的文章會詳細介紹 C++11 的 Memory Order,此處爲了完整性列出 clear 參數 sync 的取值),取值如下:

 

Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent

結合 std::atomic_flag::test_and_set() 和 std::atomic_flag::clear(),std::atomic_flag 對象可以當作一個簡單的自旋鎖使用,請看下例:

複製代碼
#include <thread>
#include <vector>
#include <iostream>
#include <atomic>

std::atomic_flag lock = ATOMIC_FLAG_INIT;

void f(int n)
{
    for (int cnt = 0; cnt < 100; ++cnt) {
        while (lock.test_and_set(std::memory_order_acquire))  // acquire lock
             ; // spin
        std::cout << "Output from thread " << n << '\n';
        lock.clear(std::memory_order_release);               // release lock
    }
}

int main()
{
    std::vector<std::thread> v;
    for (int n = 0; n < 10; ++n) {
        v.emplace_back(f, n);
    }
    for (auto& t : v) {
        t.join();
    }
}
複製代碼

在上面的程序中,std::atomic_flag 對象 lock 的上鎖操作可以理解爲 lock.test_and_set(std::memory_order_acquire); (此處指定了 Memory Order,更多有關 Memory Order 的概念,我會在後續的文章中介紹),解鎖操作相當與 lock.clear(std::memory_order_release)。

在上鎖的時候,如果 lock.test_and_set 返回 false,則表示上鎖成功(此時 while 不會進入自旋狀態),因爲此前 lock 的標誌位爲 false(即沒有線程對 lock 進行上鎖操作),但調用 test_and_set 後 lock 的標誌位爲 true,說明某一線程已經成功獲得了 lock 鎖。

如果在該線程解鎖(即調用 lock.clear(std::memory_order_release)) 之前,另外一個線程也調用 lock.test_and_set(std::memory_order_acquire) 試圖獲得鎖,則 test_and_set(std::memory_order_acquire) 返回 true,則 while 進入自旋狀態。如果獲得鎖的線程解鎖(即調用了 lock.clear(std::memory_order_release))之後,某個線程試圖調用 lock.test_and_set(std::memory_order_acquire) 並且返回 false,則 while 不會進入自旋,此時表明該線程成功地獲得了鎖。

按照上面的分析,我們知道在某種情況下 std::atomic_flag 對象可以當作一個簡單的自旋鎖使用。



C++11 併發指南六( <atomic> 類型詳解二 std::atomic )



C++11 併發指南六(atomic 類型詳解一 atomic_flag 介紹)  一文介紹了 C++11 中最簡單的原子類型 std::atomic_flag,但是 std::atomic_flag 過於簡單,只提供了 test_and_set 和 clear 兩個 API,不能滿足其他需求(如 store, load, exchange, compare_exchange 等),因此本文將介紹功能更加完善的 std::atomic 類。

std::atomic 基本介紹

std::atomic 是模板類,一個模板類型爲 T 的原子對象中封裝了一個類型爲 T 的值。

template <class T> struct atomic;

原子類型對象的主要特點就是從不同線程訪問不會導致數據競爭(data race)。因此從不同線程訪問某個原子對象是良性 (well-defined) 行爲,而通常對於非原子類型而言,併發訪問某個對象(如果不做任何同步操作)會導致未定義 (undifined) 行爲發生。

C++11 標準中的基本 std::atomic 模板定義如下:

複製代碼
template < class T > struct atomic {
    bool is_lock_free() const volatile;
    bool is_lock_free() const;
    void store(T, memory_order = memory_order_seq_cst) volatile;
    void store(T, memory_order = memory_order_seq_cst);
    T load(memory_order = memory_order_seq_cst) const volatile;
    T load(memory_order = memory_order_seq_cst) const;
    operator  T() const volatile;
    operator  T() const;
    T exchange(T, memory_order = memory_order_seq_cst) volatile;
    T exchange(T, memory_order = memory_order_seq_cst);
    bool compare_exchange_weak(T &, T, memory_order, memory_order) volatile;
    bool compare_exchange_weak(T &, T, memory_order, memory_order);
    bool compare_exchange_strong(T &, T, memory_order, memory_order) volatile;
    bool compare_exchange_strong(T &, T, memory_order, memory_order);
    bool compare_exchange_weak(T &, T, memory_order = memory_order_seq_cst) volatile;
    bool compare_exchange_weak(T &, T, memory_order = memory_order_seq_cst);
    bool compare_exchange_strong(T &, T, memory_order = memory_order_seq_cst) volatile;
    bool compare_exchange_strong(T &, T, memory_order = memory_order_seq_cst);
    atomic() = default;
    constexpr atomic(T);
    atomic(const atomic &) = delete;
    atomic & operator=(const atomic &) = delete;
    atomic & operator=(const atomic &) volatile = delete;
    T operator=(T) volatile;
    T operator=(T);
};
複製代碼

另外,C++11 標準庫 std::atomic 提供了針對整形(integral)和指針類型的特化實現,分別定義如下:

針對整形(integal)的特化,其中 integal 代表瞭如下類型char, signed char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long, char16_t, char32_t, wchar_t:

template <> struct atomic<integral> {
    bool is_lock_free() const volatile;
    bool is_lock_free() const;
 
    void store(integral, memory_order = memory_order_seq_cst) volatile;
    void store(integral, memory_order = memory_order_seq_cst);
 
    integral load(memory_order = memory_order_seq_cst) const volatile;
    integral load(memory_order = memory_order_seq_cst) const;
 
    operator integral() const volatile;
    operator integral() const;
 
    integral exchange(integral, memory_order = memory_order_seq_cst) volatile;
    integral exchange(integral, memory_order = memory_order_seq_cst);
 
    bool compare_exchange_weak(integral&, integral, memory_order, memory_order) volatile;
    bool compare_exchange_weak(integral&, integral, memory_order, memory_order);
 
    bool compare_exchange_strong(integral&, integral, memory_order, memory_order) volatile;
    bool compare_exchange_strong(integral&, integral, memory_order, memory_order);
 
    bool compare_exchange_weak(integral&, integral, memory_order = memory_order_seq_cst) volatile;
    bool compare_exchange_weak(integral&, integral, memory_order = memory_order_seq_cst);
 
    bool compare_exchange_strong(integral&, integral, memory_order = memory_order_seq_cst) volatile;
    bool compare_exchange_strong(integral&, integral, memory_order = memory_order_seq_cst);
 
    integral fetch_add(integral, memory_order = memory_order_seq_cst) volatile;
    integral fetch_add(integral, memory_order = memory_order_seq_cst);
 
    integral fetch_sub(integral, memory_order = memory_order_seq_cst) volatile;
    integral fetch_sub(integral, memory_order = memory_order_seq_cst);
 
    integral fetch_and(integral, memory_order = memory_order_seq_cst) volatile;
    integral fetch_and(integral, memory_order = memory_order_seq_cst);
 
    integral fetch_or(integral, memory_order = memory_order_seq_cst) volatile;
    integral fetch_or(integral, memory_order = memory_order_seq_cst);
 
    integral fetch_xor(integral, memory_order = memory_order_seq_cst) volatile;
    integral fetch_xor(integral, memory_order = memory_order_seq_cst);
     
    atomic() = default;
    constexpr atomic(integral);
    atomic(const atomic&) = delete;
 
    atomic& operator=(const atomic&) = delete;
    atomic& operator=(const atomic&) volatile = delete;
     
    integral operator=(integral) volatile;
    integral operator=(integral);
     
    integral operator++(int) volatile;
    integral operator++(int);
    integral operator--(int) volatile;
    integral operator--(int);
    integral operator++() volatile;
    integral operator++();
    integral operator--() volatile;
    integral operator--();
    integral operator+=(integral) volatile;
    integral operator+=(integral);
    integral operator-=(integral) volatile;
    integral operator-=(integral);
    integral operator&=(integral) volatile;
    integral operator&=(integral);
    integral operator|=(integral) volatile;
    integral operator|=(integral);
    integral operator^=(integral) volatile;
    integral operator^=(integral);
};

針對指針的特化:

template <class T> struct atomic<T*> {
    bool is_lock_free() const volatile;
    bool is_lock_free() const;
 
    void store(T*, memory_order = memory_order_seq_cst) volatile;
    void store(T*, memory_order = memory_order_seq_cst);
 
    T* load(memory_order = memory_order_seq_cst) const volatile;
    T* load(memory_order = memory_order_seq_cst) const;
 
    operator T*() const volatile;
    operator T*() const;
 
    T* exchange(T*, memory_order = memory_order_seq_cst) volatile;
    T* exchange(T*, memory_order = memory_order_seq_cst);
 
    bool compare_exchange_weak(T*&, T*, memory_order, memory_order) volatile;
    bool compare_exchange_weak(T*&, T*, memory_order, memory_order);
 
    bool compare_exchange_strong(T*&, T*, memory_order, memory_order) volatile;
    bool compare_exchange_strong(T*&, T*, memory_order, memory_order);
 
    bool compare_exchange_weak(T*&, T*, memory_order = memory_order_seq_cst) volatile;
    bool compare_exchange_weak(T*&, T*, memory_order = memory_order_seq_cst);
 
    bool compare_exchange_strong(T*&, T*, memory_order = memory_order_seq_cst) volatile;
    bool compare_exchange_strong(T*&, T*, memory_order = memory_order_seq_cst);
 
    T* fetch_add(ptrdiff_t, memory_order = memory_order_seq_cst) volatile;
    T* fetch_add(ptrdiff_t, memory_order = memory_order_seq_cst);
 
    T* fetch_sub(ptrdiff_t, memory_order = memory_order_seq_cst) volatile;
    T* fetch_sub(ptrdiff_t, memory_order = memory_order_seq_cst);
 
    atomic() = default;
    constexpr atomic(T*);
    atomic(const atomic&) = delete;
 
    atomic& operator=(const atomic&) = delete;
    atomic& operator=(const atomic&) volatile = delete;
 
    T* operator=(T*) volatile;
    T* operator=(T*);
    T* operator++(int) volatile;
    T* operator++(int);
    T* operator--(int) volatile;
    T* operator--(int);
    T* operator++() volatile;
    T* operator++();
    T* operator--() volatile;
    T* operator--();
    T* operator+=(ptrdiff_t) volatile;
    T* operator+=(ptrdiff_t);
    T* operator-=(ptrdiff_t) volatile;
    T* operator-=(ptrdiff_t);
};

std::atomic 成員函數

 好了,對 std::atomic 有了一個最基本認識之後我們來看 std::atomic 的成員函數吧。

std::atomic 構造函數

std::atomic 的構造函數如下:

default (1)
          atomic() noexcept = default;
initialization (2)
constexpr atomic (T val) noexcept;
copy [deleted] (3)
          atomic (const atomic&) = delete;
  1. 默認構造函數,由默認構造函數創建的 std::atomic 對象處於未初始化(uninitialized)狀態,對處於未初始化(uninitialized)狀態 std::atomic對象可以由 atomic_init 函數進行初始化。
  2. 初始化構造函數,由類型 T初始化一個 std::atomic對象。
  3. 拷貝構造函數被禁用。

請看下例:

#include <iostream>       // std::cout
#include <atomic>         // std::atomic, std::atomic_flag, ATOMIC_FLAG_INIT
#include <thread>         // std::thread, std::this_thread::yield
#include <vector>         // std::vector
 
// 由 false 初始化一個 std::atomic<bool> 類型的原子變量
std::atomic<bool> ready(false);
std::atomic_flag winner = ATOMIC_FLAG_INIT;
 
void do_count1m(int id)
{
    while (!ready) { std::this_thread::yield(); } // 等待 ready 變爲 true.
 
    for (volatile int i=0; i<1000000; ++i) {} // 計數
 
    if (!winner.test_and_set()) {
      std::cout << "thread #" << id << " won!\n";
    }
}
 
int main ()
{
    std::vector<std::thread> threads;
    std::cout << "spawning 10 threads that count to 1 million...\n";
    for (int i=1; i<=10; ++i) threads.push_back(std::thread(count1m,i));
    ready = true;
 
    for (auto& th : threads) th.join();
    return 0;
}

std::atomic::operator=() 函數

std::atomic 的賦值操作函數定義如下:

set value (1)
T operator= (T val) noexcept;
T operator= (T val) volatile noexcept;
copy [deleted] (2)
atomic& operator= (const atomic&) = delete;
atomic& operator= (const atomic&) volatile = delete;

可以看出,普通的賦值拷貝操作已經被禁用。但是一個類型爲 T 的變量可以賦值給相應的原子類型變量(相當與隱式轉換),該操作是原子的,內存序(Memory Order) 默認爲順序一致性(std::memory_order_seq_cst),如果需要指定其他的內存序,需使用 std::atomic::store()。

#include <iostream>             // std::cout
#include <atomic>               // std::atomic
#include <thread>               // std::thread, std::this_thread::yield
 
std::atomic <int> foo = 0;
 
void set_foo(int x)
{
    foo = x; // 調用 std::atomic::operator=().
}
 
void print_foo()
{
    while (foo == 0) { // wait while foo == 0
        std::this_thread::yield();
    }
    std::cout << "foo: " << foo << '\n';
}
 
int main()
{
    std::thread first(print_foo);
    std::thread second(set_foo, 10);
    first.join();
    second.join();
    return 0;
}

基本 std::atomic 類型操作

本節主要介紹基本 std::atomic 類型所具備的操作(即成員函數)。我們知道 std::atomic 是模板類,一個模板類型爲 T 的原子對象中封裝了一個類型爲 T 的值。本文<std::atomic 基本介紹>一節中也提到了 std::atomic 類模板除了基本類型以外,還針對整形和指針類型做了特化。 特化的 std::atomic 類型支持更多的操作,如 fetch_add, fetch_sub, fetch_and 等。本小節介紹基本 std::atomic 類型所具備的操作:

bool is_lock_free() const volatile noexcept;
bool is_lock_free() const noexcept;
void store (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
void store (T val, memory_order sync = memory_order_seq_cst) noexcept;
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_releaseRelease
memory_order_seq_cstSequentially consistent
#include <iostream>       // std::cout
#include <atomic>         // std::atomic, std::memory_order_relaxed
#include <thread>         // std::thread
 
std::atomic<int> foo(0); // 全局的原子對象 foo
 
void set_foo(int x)
{
    foo.store(x, std::memory_order_relaxed); // 設置(store) 原子對象 foo 的值
}
 
void print_foo()
{
    int x;
    do {
        x = foo.load(std::memory_order_relaxed); // 讀取(load) 原子對象 foo 的值
    } while (x == 0);
    std::cout << "foo: " << x << '\n';
}
 
int main ()
{
    std::thread first(print_foo); // 線程 first 打印 foo 的值
    std::thread second(set_foo, 10); // 線程 second 設置 foo 的值
    first.join();
    second.join();
    return 0;
}
T load (memory_order sync = memory_order_seq_cst) const volatile noexcept;
T load (memory_order sync = memory_order_seq_cst) const noexcept;
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_seq_cstSequentially consistent
#include <iostream>       // std::cout
#include <atomic>         // std::atomic, std::memory_order_relaxed
#include <thread>         // std::thread
 
std::atomic<int> foo(0); // 全局的原子對象 foo
 
void set_foo(int x)
{
    foo.store(x, std::memory_order_relaxed); // 設置(store) 原子對象 foo 的值
}
 
void print_foo()
{
    int x;
    do {
        x = foo.load(std::memory_order_relaxed); // 讀取(load) 原子對象 foo 的值
    } while (x == 0);
    std::cout << "foo: " << x << '\n';
}
 
int main ()
{
    std::thread first(print_foo); // 線程 first 打印 foo 的值
    std::thread second(set_foo, 10); // 線程 second 設置 foo 的值
    first.join();
    second.join();
    return 0;
}
operator T() const volatile noexcept;
operator T() const noexcept;
#include <iostream>       // std::cout
#include <atomic>         // std::atomic
#include <thread>         // std::thread, std::this_thread::yield
 
std::atomic<int> foo = 0;
std::atomic<int> bar = 0;
 
void set_foo(int x)
{
    foo = x;
}
 
void copy_foo_to_bar()
{
 
    // 如果 foo == 0,則該線程 yield,
    // 在 foo == 0 時, 實際也是隱含了類型轉換操作,
    // 因此也包含了 operator T() const 的調用.
    while (foo == 0) std::this_thread::yield();
 
    // 實際調用了 operator T() const, 將foo 強制轉換成 int 類型,
    // 然後調用 operator=().
    bar = static_cast<int>(foo);
}
 
void print_bar()
{
    // 如果 bar == 0,則該線程 yield,
    // 在 bar == 0 時, 實際也是隱含了類型轉換操作,
    // 因此也包含了 operator T() const 的調用.
    while (bar == 0) std::this_thread::yield();
    std::cout << "bar: " << bar << '\n';
}
 
int main ()
{
    std::thread first(print_bar);
    std::thread second(set_foo, 10);
    std::thread third(copy_foo_to_bar);
 
    first.join();
    second.join();
    third.join();
    return 0;
}

 

T exchange (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
T exchange (T val, memory_order sync = memory_order_seq_cst) noexcept;
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent

請看下面例子,各個線程計數至 1M,首先完成計數任務的線程打印自己的 ID,

#include <iostream>       // std::cout
#include <atomic>         // std::atomic
#include <thread>         // std::thread
#include <vector>         // std::vector
 
std::atomic<bool> ready(false);
std::atomic<bool> winner(false);
 
void count1m (int id)
{
    while (!ready) {}                  // wait for the ready signal
    for (int i = 0; i < 1000000; ++i) {}   // go!, count to 1 million
    if (!winner.exchange(true)) { std::cout << "thread #" << id << " won!\n"; }
};
 
int main ()
{
    std::vector<std::thread> threads;
    std::cout << "spawning 10 threads that count to 1 million...\n";
    for (int i = 1; i <= 10; ++i) threads.push_back(std::thread(count1m,i));
    ready = true;
    for (auto& th : threads) th.join();
 
    return 0;
}
(1)
bool compare_exchange_weak (T& expected, T val,
           memory_order sync = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_weak (T& expected, T val,
           memory_order sync = memory_order_seq_cst) noexcept;
(2)
bool compare_exchange_weak (T& expected, T val,
           memory_order success, memory_order failure) volatile noexcept;
bool compare_exchange_weak (T& expected, T val,
           memory_order success, memory_order failure) noexcept;
  • 相等,則用 val 替換原子對象的舊值。
  • 不相等,則用原子對象的舊值替換 expected ,因此調用該函數之後,如果被該原子對象封裝的值與參數 expected 所指定的值不相等,expected 中的內容就是原子對象的舊值。
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent
#include <iostream>       // std::cout
#include <atomic>         // std::atomic
#include <thread>         // std::thread
#include <vector>         // std::vector
 
// a simple global linked list:
struct Node { int value; Node* next; };
std::atomic<Node*> list_head(nullptr);
 
void append(int val)
{
    // append an element to the list
    Node* newNode = new Node{val, list_head};
 
    // next is the same as: list_head = newNode, but in a thread-safe way:
    while (!list_head.compare_exchange_weak(newNode->next,newNode)) {}
    // (with newNode->next updated accordingly if some other thread just appended another node)
}
 
int main ()
{
    // spawn 10 threads to fill the linked list:
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) threads.push_back(std::thread(append, i));
    for (auto& th : threads) th.join();
 
    // print contents:
    for (Node* it = list_head; it!=nullptr; it=it->next)
        std::cout << ' ' << it->value;
 
    std::cout << '\n';
 
    // cleanup:
    Node* it; while (it=list_head) {list_head=it->next; delete it;}
 
    return 0;
}
9 8 7 6 5 4 3 2 1 0

 

(1)
bool compare_exchange_strong (T& expected, T val,
           memory_order sync = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_strong (T& expected, T val,
           memory_order sync = memory_order_seq_cst) noexcept;
(2)
bool compare_exchange_strong (T& expected, T val,
           memory_order success, memory_order failure) volatile noexcept;
bool compare_exchange_strong (T& expected, T val,
           memory_order success, memory_order failure) noexcept;
  • 相等,則用 val 替換原子對象的舊值。
  • 不相等,則用原子對象的舊值替換 expected ,因此調用該函數之後,如果被該原子對象封裝的值與參數 expected 所指定的值不相等,expected 中的內容就是原子對象的舊值。
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent
#include <iostream>       // std::cout
#include <atomic>         // std::atomic
#include <thread>         // std::thread
#include <vector>         // std::vector
 
// a simple global linked list:
struct Node { int value; Node* next; };
std::atomic<Node*> list_head(nullptr);
 
void append(int val)
{
    // append an element to the list
    Node* newNode = new Node{val, list_head};
 
    // next is the same as: list_head = newNode, but in a thread-safe way:
 
    while (!(list_head.compare_exchange_strong(newNode->next, newNode)));
    // (with newNode->next updated accordingly if some other thread just appended another node)
}
 
int main ()
{
    // spawn 10 threads to fill the linked list:
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) threads.push_back(std::thread(append, i));
    for (auto& th : threads) th.join();
 
    // print contents:
    for (Node* it = list_head; it!=nullptr; it=it->next)
        std::cout << ' ' << it->value;
 
    std::cout << '\n';
 
    // cleanup:
    Node* it; while (it=list_head) {list_head=it->next; delete it;}
 
    return 0;
}


好了,本文花了大量的篇幅介紹 std::atomic 基本類型,下一篇博客我會給大家介紹 C++11 的標準庫中std::atomic 針對整形(integral)和指針類型的特化版本做了哪些改進。



C++11 併發指南六(atomic 類型詳解三 std::atomic (續))



C++11 併發指南六( <atomic> 類型詳解二 std::atomic ) 介紹了基本的原子類型 std::atomic 的用法,本節我會給大家介紹C++11 標準庫中的 std::atomic 針對整形(integral)和指針類型的特化版本做了哪些改進。

總地來說,C++11 標準庫中的 std::atomic 針對整形(integral)和指針類型的特化版本新增了一些算術運算和邏輯運算操作。具體如下:

integral fetch_add(integral, memory_order = memory_order_seq_cst) volatile;
integral fetch_add(integral, memory_order = memory_order_seq_cst);
integral fetch_sub(integral, memory_order = memory_order_seq_cst) volatile;
integral fetch_sub(integral, memory_order = memory_order_seq_cst);
integral fetch_and(integral, memory_order = memory_order_seq_cst) volatile;
integral fetch_and(integral, memory_order = memory_order_seq_cst);
integral fetch_or(integral, memory_order = memory_order_seq_cst) volatile;
integral fetch_or(integral, memory_order = memory_order_seq_cst);
integral fetch_xor(integral, memory_order = memory_order_seq_cst) volatile;
integral fetch_xor(integral, memory_order = memory_order_seq_cst);
 
integral operator++(int) volatile;
integral operator++(int);
integral operator--(int) volatile;
integral operator--(int);
integral operator++() volatile;
integral operator++();
integral operator--() volatile;
integral operator--();
integral operator+=(integral) volatile;
integral operator+=(integral);
integral operator-=(integral) volatile;
integral operator-=(integral);
integral operator&=(integral) volatile;
integral operator&=(integral);
integral operator|=(integral) volatile;
integral operator|=(integral);
integral operator^=(integral) volatile;
integral operator^=(integral);

 下面我們來簡單介紹以上的 std::atomic 特化版本的成員函數。

if T is integral (1)
T fetch_add (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
T fetch_add (T val, memory_order sync = memory_order_seq_cst) noexcept;
if T is pointer (2)
T fetch_add (ptrdiff_t val, memory_order sync = memory_order_seq_cst) volatile noexcept;
T fetch_add (ptrdiff_t val, memory_order sync = memory_order_seq_cst) noexcept;
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent
if T is integral (1)
T fetch_sub (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
T fetch_sub (T val, memory_order sync = memory_order_seq_cst) noexcept;
if T is pointer (2)
T fetch_sub (ptrdiff_t val, memory_order sync = memory_order_seq_cst) volatile noexcept;
T fetch_sub (ptrdiff_t val, memory_order sync = memory_order_seq_cst) noexcept;
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent
T fetch_and (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
T fetch_and (T val, memory_order sync = memory_order_seq_cst) noexcept;

 

Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent
T fetch_or (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
T fetch_or (T val, memory_order sync = memory_order_seq_cst) noexcept;

 

Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent
T fetch_xor (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
T fetch_xor (T val, memory_order sync = memory_order_seq_cst) noexcept;
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent
pre-increment (1)
T operator++() volatile noexcept;
T operator++() noexcept;
post-increment (2)
T operator++ (int) volatile noexcept;
T operator++ (int) noexcept;
if T is integral (1)
T operator+= (T val) volatile noexcept;
T operator+= (T val) noexcept;
T operator-= (T val) volatile noexcept;
T operator-= (T val) noexcept;
T operator&= (T val) volatile noexcept;
T operator&= (T val) noexcept;
T operator|= (T val) volatile noexcept;
T operator|= (T val) noexcept;
T operator^= (T val) volatile noexcept;
T operator^= (T val) noexcept;
if T is pointer (2)
T operator+= (ptrdiff_t val) volatile noexcept;
T operator+= (ptrdiff_t val) noexcept;
T operator-= (ptrdiff_t val) volatile noexcept;
T operator-= (ptrdiff_t val) noexcept;

以上各個 operator 都會有對應的 fetch_* 操作,詳細見下表:

操作符成員函數支持類型
複合賦值等價於整型指針類型其他類型
+atomic::operator+=atomic::fetch_add
-atomic::operator-=atomic::fetch_sub
&atomic::operator&=atomic::fetch_and
|atomic::operator|=atomic::fetch_or
^atomic::operator^=atomic::fetch_xor

好了,本節先介紹這裏,下一節我會介紹 C++11 中 C 風格的原子操作 API。



C++11 併發指南六(atomic 類型詳解四 C 風格原子操作介紹)



前面三篇文章《C++11 併發指南六(atomic 類型詳解一 atomic_flag 介紹)》、《C++11 併發指南六( <atomic> 類型詳解二 std::atomic )》、《C++11 併發指南六(atomic 類型詳解三 std::atomic (續))》都是採用 C++ 的方式介紹原子對象,本節我會給大家介紹 C++11 原子操作中 C 風格的 API。

總地來說,C++11 標準中規定了兩大類原子對象,std::atomic_flag 和 std::atomic,前者 std::atomic_flag 一種最簡單的原子布爾類型,只支持兩種操作,test-and-set 和 clear。而 std::atomic 是模板類,一個模板類型爲 T 的原子對象中封裝了一個類型爲 T 的值,並且C++11 標準中除了定義基本 std::atomic 模板類型外,還提供了針對整形(integral)和指針類型的特化實現,提供了大量的 API,極大地方便了開發者使用。下面我分別介紹基於 std::atomic_flag 和 std::atomic 的 C 風格 API。

基於 std::atomic_flag 類型的 C 風格 API

bool atomic_flag_test_and_set (volatile atomic_flag* obj) noexcept;
bool atomic_flag_test_and_set (atomic_flag* obj) noexcept;
bool atomic_flag_test_and_set (volatile atomic_flag* obj, memory_order sync) noexcept;
bool atomic_flag_test_and_set (atomic_flag* obj, memory_order sync) noexcept;
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent
void atomic_flag_clear (volatile atomic_flag* obj) noexcept;
void atomic_flag_clear (atomic_flag* obj) noexcept;
void atomic_flag_clear (volatile atomic_flag* obj, memory_order sync) noexcept;
void atomic_flag_clear (atomic_flag* obj, memory_order sync) noexcept;
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent

基於 std::atomic 模板類型的 C 風格 API

template (1)
template <class T> bool atomic_is_lock_free (const volatile atomic<T>* obj) noexcept;
template <class T> bool atomic_is_lock_free (const atomic<T>* obj) noexcept;
overloads (2)
bool atomic_is_lock_free (const volatile A* obj) noexcept;
bool atomic_is_lock_free (const A* obj) noexcept;
template (1)
template <class T> void atomic_init (volatile atomic<T>* obj, T val) noexcept;
template <class T> void atomic_init (atomic<T>* obj, T val) noexcept;
overloads (2)
void atomic_init (volatile A* obj, T val) noexcept;
void atomic_init (A* obj, T val) noexcept;
template (1)
template <class T> void atomic_store (volatile atomic<T>* obj, T val) noexcept;
template <class T> void atomic_store (atomic<T>* obj, T val) noexcept;
overloads (2)
void atomic_store (volatile A* obj, T val) noexcept;
void atomic_store (A* obj, T val) noexcept;
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_releaseRelease
memory_order_seq_cstSequentially consistent
template (1)
template <class T> T atomic_load (const volatile atomic<T>* obj) noexcept;
template <class T> T atomic_load (const atomic<T>* obj) noexcept;
overloads (2)
T atomic_load (const volatile A* obj) noexcept;
T atomic_load (const A* obj) noexcept;
template (1)
template <class T>
T atomic_load_explicit (const volatile atomic<T>* obj, memory_order sync) noexcept;
template <class T>
T atomic_load_explicit (const atomic<T>* obj, memory_order sync) noexcept;
overloads (2)
T atomic_load_explicit (const volatile A* obj, memory_order sync) noexcept;
T atomic_load_explicit (const A* obj, memory_order sync) noexcept;
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_seq_cstSequentially consistent
template (1)
template <class T> T atomic_exchange (volatile atomic<T>* obj, T val) noexcept;
template <class T> T atomic_exchange (atomic<T>* obj, T val) noexcept;
overloads (2)
T atomic_exchange (volatile A* obj, T val) noexcept;
T atomic_exchange (A* obj, T val) noexcept;
template (1)
template <class T>
T atomic_store_explicit (volatile atomic<T>* obj, T val, memory_order sync) noexcept;
template <class T>
T atomic_store_explicit (atomic<T>* obj, T val, memory_order sync) noexcept;
overloads (2)
T atomic_store_explicit (volatile A* obj, T val, memory_order sync) noexcept;
T atomic_store_explicit (A* obj, T val, memory_order sync) noexcept;
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent
template (1)
template <class T>
bool atomic_compare_exchange_weak (volatile atomic<T>* obj, T* expected, T val) noexcept;
template <class T>
bool atomic_compare_exchange_weak (atomic<T>* obj, T* expected, T val) noexcept;
overloads (2)
bool atomic_compare_exchange_weak (volatile A* obj, T* expected, T val) noexcept;
bool atomic_compare_exchange_weak (A* obj, T* expected, T val) noexcept;
  • 相等,則用 val 替換原子對象的舊值。
  • 不相等,則用原子對象的舊值替換 expected ,因此調用該函數之後,如果被該原子對象封裝的值與參數 expected 所指定的值不相等,expected 中的內容就是原子對象的舊值。
template (1)
template <class T>
bool atomic_compare_exchange_weak_explicit (volatile atomic<T>* obj,
        T* expected, T val, memory_order success, memory_order failure) noexcept;
template <class T>
bool atomic_compare_exchange_weak_explicit (atomic<T>* obj,
        T* expected, T val, memory_order success, memory_order failure) noexcept;
overloads (2)
bool atomic_compare_exchange_weak_explicit (volatile A* obj,
        T* expected, T val, memory_order success, memory_order failure) noexcept;
bool atomic_compare_exchange_weak_explicit (A* obj,
        T* expected, T val, memory_order success, memory_order failure) noexcept;
  • 相等,則用 val 替換原子對象的舊值。
  • 不相等,則用原子對象的舊值替換 expected ,因此調用該函數之後,如果被該原子對象封裝的值與參數 expected 所指定的值不相等,expected 中的內容就是原子對象的舊值。
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent
template (1)
template <class T>
bool atomic_compare_exchange_strong (volatile atomic<T>* obj, T* expected, T val) noexcept;
template <class T>
bool atomic_compare_exchange_strong (atomic<T>* obj, T* expected, T val) noexcept;
overloads (2)
bool atomic_compare_exchange_strong (volatile A* obj, T* expected, T val) noexcept;
bool atomic_compare_exchange_strong (A* obj, T* expected, T val) noexcept;
  • 相等,則用 val 替換原子對象的舊值。
  • 不相等,則用原子對象的舊值替換 expected ,因此調用該函數之後,如果被該原子對象封裝的值與參數 expected 所指定的值不相等,expected 中的內容就是原子對象的舊值。
template (1)
template <class T>
bool atomic_compare_exchange_strong_explicit (volatile atomic<T>* obj,
        T* expected, T val, memory_order success, memory_order failure) noexcept;
template <class T>
bool atomic_compare_exchange_strong_explicit (atomic<T>* obj,
        T* expected, T val, memory_order success, memory_order failure) noexcept;
overloads (2)
bool atomic_compare_exchange_strong_explicit (volatile A* obj,
        T* expected, T val, memory_order success, memory_order failure) noexcept;
bool atomic_compare_exchange_strong_explicit (A* obj,
        T* expected, T val, memory_order success, memory_order failure) noexcept;
  • 相等,則用 val 替換原子對象的舊值。
  • 不相等,則用原子對象的舊值替換 expected ,因此調用該函數之後,如果被該原子對象封裝的值與參數 expected 所指定的值不相等,expected 中的內容就是原子對象的舊值。
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent
template (integral) (1)
template <class T> T atomic_fetch_add (volatile atomic<T>* obj, T val) noexcept;
template <class T> T atomic_fetch_add (atomic<T>* obj, T val) noexcept;
template (pointer) (2)
template <class U> U* atomic_fetch_add (volatile atomic<U*>* obj, ptrdiff_t val) noexcept;
template <class U> U* atomic_fetch_add (atomic<U*>* obj, ptrdiff_t val) noexcept;
overloads (3)
T atomic_fetch_add (volatile A* obj, M val) noexcept;
T atomic_fetch_add (A* obj, M val) noexcept;
template (integral) (1)
template <class T>
T atomic_fetch_add_explicit (volatile atomic<T>* obj,
                             T val, memory_order sync) noexcept;
template <class T>
T atomic_fetch_add_explicit (atomic<T>* obj,
                             T val, memory_order sync) noexcept;
template (pointer) (2)
template <class U>
U* atomic_fetch_add_explicit (volatile atomic<U*>* obj,
                              ptrdiff_t val, memory_order sync) noexcept;
template <class U>
U* atomic_fetch_add_explicit (atomic<U*>* obj,
                              ptrdiff_t val, memory_order sync) noexcept;
overloads (3)
T atomic_fetch_add_explicit (volatile A* obj, M val, memory_order sync) noexcept;
T atomic_fetch_add_explicit (A* obj, M val, memory_order sync) noexcept;
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent
template (integral) (1)
template <class T> T atomic_fetch_sub (volatile atomic<T>* obj, T val) noexcept;
template <class T> T atomic_fetch_sub (atomic<T>* obj, T val) noexcept;
template (pointer) (2)
template <class U> U* atomic_fetch_sub (volatile atomic<U*>* obj, ptrdiff_t val) noexcept;
template <class U> U* atomic_fetch_sub (atomic<U*>* obj, ptrdiff_t val) noexcept;
overloads (3)
T atomic_fetch_sub (volatile A* obj, M val) noexcept;
T atomic_fetch_sub (A* obj, M val) noexcept;
template (integral) (1)
template <class T>
T atomic_fetch_sub_explicit (volatile atomic<T>* obj,
                             T val, memory_order sync) noexcept;
template <class T>
T atomic_fetch_sub_explicit (atomic<T>* obj,
                             T val, memory_order sync) noexcept;
template (pointer) (2)
template <class U>
U* atomic_fetch_sub_explicit (volatile atomic<U*>* obj,
                              ptrdiff_t val, memory_order sync) noexcept;
template <class U>
U* atomic_fetch_sub_explicit (atomic<U*>* obj,
                              ptrdiff_t val, memory_order sync) noexcept;
overloads (3)
T atomic_fetch_sub_explicit (volatile A* obj, M val, memory_order sync) noexcept;
T atomic_fetch_sub_explicit (A* obj, M val, memory_order sync) noexcept;
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent
emplate (integral) (1)
template <class T> T atomic_fetch_and (volatile atomic<T>* obj, T val) noexcept;
template <class T> T atomic_fetch_and (atomic<T>* obj, T val) noexcept;
overloads (2)
T atomic_fetch_and (volatile A* obj, T val) noexcept;
T atomic_fetch_and (A* obj, T val) noexcept;
template (integral) (1)
template <class T>
T atomic_fetch_and_explicit (volatile atomic<T>* obj,
                             T val, memory_order sync) noexcept;
template <class T>
T atomic_fetch_and_explicit (atomic<T>* obj,
                             T val, memory_order sync) noexcept;
overloads (2)
T atomic_fetch_and_explicit (volatile A* obj, T val, memory_order sync) noexcept;
T atomic_fetch_and_explicit (A* obj, T val, memory_order sync) noexcept;
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent
template (integral) (1)
template <class T> T atomic_fetch_or (volatile atomic<T>* obj, T val) noexcept;
template <class T> T atomic_fetch_or (atomic<T>* obj, T val) noexcept;
overloads (2)
T atomic_fetch_or (volatile A* obj, T val) noexcept;
T atomic_fetch_or (A* obj, T val) noexcept;
template (integral) (1)
template <class T>
T atomic_fetch_or_explicit (volatile atomic<T>* obj,
                             T val, memory_order sync) noexcept;
template <class T>
T atomic_fetch_or_explicit (atomic<T>* obj,
                             T val, memory_order sync) noexcept;
overloads (2)
T atomic_fetch_or_explicit (volatile A* obj, T val, memory_order sync) noexcept;
T atomic_fetch_or_explicit (A* obj, T val, memory_order sync) noexcept;
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent
template (integral) (1)
template <class T> T atomic_fetch_xor (volatile atomic<T>* obj, T val) noexcept;
template <class T> T atomic_fetch_xor (atomic<T>* obj, T val) noexcept;
overloads (2)
T atomic_fetch_xor (volatile A* obj, T val) noexcept;
T atomic_fetch_xor (A* obj, T val) noexcept;
template (integral) (1)
template <class T>
T atomic_fetch_xor_explicit (volatile atomic<T>* obj,
                             T val, memory_order sync) noexcept;
template <class T>
T atomic_fetch_xor_explicit (atomic<T>* obj,
                             T val, memory_order sync) noexcept;
overloads (2)
T atomic_fetch_xor_explicit (volatile A* obj, T val, memory_order sync) noexcept;
T atomic_fetch_xor_explicit (A* obj, T val, memory_order sync) noexcept;
Memory Order 值Memory Order 類型
memory_order_relaxedRelaxed
memory_order_consumeConsume
memory_order_acquireAcquire
memory_order_releaseRelease
memory_order_acq_relAcquire/Release
memory_order_seq_cstSequentially consistent

與原子對象初始化相關的宏



C++11 併發指南七(C++11 內存模型一:介紹)



第六章主要介紹了 C++11 中的原子類型及其相關的API,原子類型的大多數 API 都需要程序員提供一個 std::memory_order(可譯爲內存序,訪存順序) 的枚舉類型值作爲參數,比如:atomic_storeatomic_loadatomic_exchangeatomic_compare_exchange 等 API 的最後一個形參爲 std::memory_order order,默認值是 std::memory_order_seq_cst(順序一致性)。那麼究竟什麼是 std::memory_order 呢,爲了解答這個問題,我們先來討論 C++11 的內存模型。

一般來講,內存模型可分爲靜態內存模型和動態內存模型,靜態內存模型主要涉及類的對象在內存中是如何存放的,即從結構(structural)方面來看一個對象在內存中的佈局,以一個簡單的例子爲例(截圖參考《C++  Concurrency In Action》 P105 ):

上面是一個簡單的 C++ 類(又稱POD: Plain Old Data,它沒有虛函數,沒有繼承),它在內存中的佈局如圖右邊所示(對於複雜類對象的內存佈局,請參考《深度探索C++對象模型》一書)。

動態內存模型可理解爲存儲一致性模型,主要是從行爲(behavioral)方面來看多個線程對同一個對象同時(讀寫)操作時(concurrency)所做的約束,動態內存模型理解起來稍微複雜一些,涉及了內存,Cache,CPU 各個層次的交互,尤其是在共享存儲系統中,爲了保證程序執行的正確性,就需要對訪存事件施加嚴格的限制。

文獻中常見的存儲一致性模型包括順序一致性模型,處理器一致性模型,弱一致性模型,釋放一致性模型,急切更新釋放一致性模型、懶惰更新釋放一致性模型,域一致性模型以及單項一致性模型。不同的存儲一致性模型對訪存事件次序的限制不同,因而對程序員的要求和所得到的的性能也不一樣。存儲一致性模型對訪存事件次序施加的限制越弱,我們就越有利於提高程序的性能,但編程實現上更困難。

順序一致性模型由 Lamport 於 1979 年提出。順序一致性模型最好理解但代價太大,原文指出:

... the result of any execution is the same as if the operations of all the processors were executed in some sequential order, and the operations of each individual processor appear in this sequence in the order specified by its program.

該模型指出:如果在共享存儲系統中多機並行執行的結果等於把每一個處理器所執行的指令流按照某種方式順序地交織在一起在單機上執行的結果,則該共享存儲系統是順序一致性的。

順序一致性不僅在共享存儲系統上適用,在多處理器和多線程環境下也同樣適用。而在多處理器和多線程環境下理解順序一致性包括兩個方面,(1). 從多個線程平行角度來看,程序最終的執行結果相當於多個線程某種交織執行的結果,(2)從單個線程內部執行順序來看,該線程中的指令是按照程序事先已規定的順序執行的(即不考慮運行時 CPU 亂序執行和 Memory Reorder)。

我們以一個具體的例子來理解順序一致性:

假設存在兩個共享變量a, b,初始值均爲 0,兩個線程運行不同的指令,如下表格所示,線程 1 設置 a 的值爲 1,然後設置 R1 的值爲 b,線程 2 設置 b 的值爲 2,並設置 R2 的值爲 a,請問在不加任何鎖或者其他同步措施的情況下,R1,R2 的最終結果會是多少?

 

線程 1線程 2
a = 1;b = 2;
R1 = b;R2 = a;

 

由於沒有施加任何同步限制,兩個線程將會交織執行,但交織執行時指令不發生重排,即線程 1 中的 a = 1 始終在 R1 = b 之前執行,而線程 2 中的 b = 2 始終在 R2 = a 之前執行 ,因此可能的執行序列共有 4!/(2!*2!) = 6 種:

 

情況 1情況 2情況 3情況 4情況 5情況 6
a = 1;
b = 2;
a = 1;
a = 1;
b = 2;
b = 2;
R1 = b;
R2 = a;
b = 2;
b = 2;
a = 1;
a = 1;
b = 2;
a = 1;
R1 = b;
R2 = a;
R1 = b;
R2 = b;
R2 = a;
R1 = b;
R2 = a;
R1 = b;
R2 = a;
R1 = b;
R1 == 0, R2 == 1
R1 == 2, R2 == 0
R1 == 2, R2 == 1
R1 == 2, R2 == 1
R1 == 2, R2 == 1
R1 == 2, R2 == 1

 

上面的表格列舉了兩個線程交織執行時所有可能的執行序列,我們發現,R1,R2 最終結果只有 3 種情況,分別是 R1 == 0, R2 == 1(情況 1),R1 == 2, R2 == 0(情況2) 和 R1 == 2, R2 == 1(情況 3, 4, 5,6)。結合上面的例子,我想大家應該理解了什麼是順序一致性。

因此,多線程環境下順序一致性包括兩個方面,(1). 從多個線程平行角度來看,程序最終的執行結果相當於多個線程某種交織執行的結果,(2)從單個線程內部執行順序來看,該線程中的指令是按照程序事先已規定的順序執行的(即不考慮運行時 CPU 亂序執行和 Memory Reorder)。

當然,順序一致性代價太大,不利於程序的優化,現在的編譯器在編譯程序時通常將指令重新排序(當然前提是保證程序的執行結果是正確的),例如,如果兩個變量讀寫互不相關,編譯器有可能將讀操作提前(暫且稱爲預讀prefetch 吧),或者儘可能延遲寫操作,假設如下面的代碼段:

複製代碼
int a = 1, b = 2;

void func()
{
    a = b + 22;
    b = 22;
}
複製代碼

 在GCC 4.4 (X86-64)編譯條件下,優化選項爲 -O0 時,彙編後關鍵代碼如下:

movl    b(%rip), %eax ; 將 b 讀入 %eax
addl    $22, %eax ; %eax 加 22, 即 b + 22
movl    %eax, a(%rip) ; % 將 %eax 寫回至 a, 即 a = b + 22
movl    $22, b(%rip) ; 設置 b = 22

而在設置 -O2 選項時,彙編後的關鍵代碼如下:

movl    b(%rip), %eax ; 將 b 讀入 %eax
movl    $22, b(%rip) ; b = 22
addl    $22, %eax ; %eax 加 22
movl    %eax, a(%rip) ; 將 b + 22 的值寫入 a,即 a = b + 2

由上面的例子可以看出,編譯器在不同的優化級別下確實對指令進行了不同程度重排,在 -O0(不作優化)的情況下,彙編指令和 C 源代碼的邏輯相同,但是在 -O2 優化級別下,彙編指令和原始代碼的執行邏輯不同,由彙編代碼可以觀察出,b = 22 首先執行,最後纔是 a = b + 2, 由此看出,編譯器會根據不同的優化等級來適當地對指令進行重排。在單線程條件下上述指令重排不會對執行結果帶來任何影響,但是在多線程環境下就不一定了。如果另外一個線程依賴 a,b的值來選擇它的執行邏輯,那麼上述重排將會產生嚴重問題。編譯器優化是一門深奧的技術,但是無論編譯器怎麼優化,都需要對優化條件作出約束,尤其是在多線程條件下,不能無理由地優化,更不能錯誤地優化。

另外,現代的 CPU 大都支持多發射和亂序執行,在亂序執行時,指令被執行的邏輯可能和程序彙編指令的邏輯不一致,在單線程條件下,CPU 的亂序執行不會帶來大問題,但是在多核多線程時代,當多線程共享某一變量時,不同線程對共享變量的讀寫就應該格外小心,不適當的亂序執行可能導致程序運行錯誤。因此,CPU 的亂序執行也需要作出適當的約束。

綜上所述,我們必須對編譯器和 CPU 作出一定的約束才能合理正確地優化你的程序,那麼這個約束是什麼呢?答曰:內存模型。C++程序員要想寫出高性能的多線程程序必須理解內存模型,編譯器會給你的程序做優化(靜態),CPU爲了提升性能也有亂序執行(動態),總之,程序在最終執行時並不會按照你之前的原始代碼順序來執行,因此內存模型是程序員、編譯器,CPU 之間的契約,遵守契約後大家就各自做優化,從而儘可能提高程序的性能。

C++11 中規定了 6 中訪存次序(Memory Order),如下:

複製代碼
enum memory_order {
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
};
複製代碼

std::memory_order 規定了普通訪存操作和相鄰的原子訪存操作之間的次序是如何安排的,在多核系統中,當多個線程同時讀寫多個變量時,其中的某個線程所看到的變量值的改變順序可能和其他線程寫入變量值的次序不相同。同時,不同的線程所觀察到的某變量被修改次序也可能不相同。然而,如果保證所有對原子變量的操作都是順序的話,可能對程序的性能影響很大,因此,我們可以通過std::memory_order 來指定編譯器對訪存次序所做的限制。因此,在原子類型的 API 中,我們可以通過額外的參數指定該原子操作的訪存次序(內存序),默認的內存序是 std::memory_order_seq_cst

我們可以把上述 6 中訪存次序(內存序)分爲 3 類,順序一致性模型(std::memory_order_seq_cst),Acquire-Release 模型(std::memory_order_consume, std::memory_order_acquire, std::memory_order_release, std::memory_order_acq_rel,) 和 Relax 模型(std::memory_order_relaxed)。三種不同的內存模型在不同類型的 CPU上(如 X86,ARM,PowerPC等)所帶來的代價也不一樣。例如,在 X86 或者 X86-64平臺下,Acquire-Release 類型的訪存序不需要額外的指令來保證原子性,即使順序一致性類型操作也只需要在寫操作(Store)時施加少量的限制,而在讀操作(Load)則不需要花費額外的代價來保證原子性。

===================================== TL;DR =====================================

附:本文剩餘部分將介紹其他的存儲器一致模型中的其他幾種較常見的模型:處理器一致性(Processor Consistency)模型,弱一致性(Weak Consistency)模型,釋放一致性(Release Consistency)模型。[注:以下內容來自中國科學院計算技術研究所胡偉武老師寫的《計算機體系結構》(清華大學出版社),該書是胡偉武老師給研究生講課所用的教材,本文略有刪改]

處理器一致性(Processor Consistency)模型:處理器一致性(Processor Consistency)模型比順序一致性模型弱,因此對於某些在順序一致性模型下能夠正確執行的程序在處理器一致性條件下執行時可能會導致錯誤的結果,處理器一致性模型對訪存事件發生次序施加的限制是:(1). 在任意讀操作(Load)被允許執行之前,所有在同一處理器中先於這一 Load 的讀操作都已完成;(2). 在任意寫操作(Store)被允許執行之前,所有在同一處理器中先於這一 Store 的訪存操作(包括 Load 和 Store操作)都已完成。上述條件允許 Store 之後的 Load 越過 Store 操作而有限執行。

弱一致性(Weak Consistency)模型:弱一致性(Weak Consistency)模型的主要思想是將同步操作和普通的訪存操作區分開來,程序員必須用硬件可識別的同步操作把對可寫共享單元的訪存保護起來,以保證多個處理器對可寫單元的訪問是互斥的。弱一致性對訪存事件發生次序的限制如下:(1). 同步操作的執行滿足順序一致性條件; (2). 在任一普通訪存操作被允許執行之前,所有在同一處理器中先於這一訪存操作的同步操作都已完成; (3). 在任一同步操作被允許執行之前,所有在同一處理器中先於這一同步操作的普通操作都已完成。上述條件允許在同步操作之間的普通訪存操作執行時不用考慮進程之間的相關,雖然弱一致性增加了程序員的負擔,但是它能有效地提高系統的性能。

釋放一致性(Release Consistency)模型:釋放一致性(Release Consistency)模型是對弱一致性(Weak Consistency)模型的改進,它把同步操作進一步分成了獲取操作(Acquire)和釋放操作(Release)。Acquire 用於獲取對某些共享變量的獨佔訪問權,而 Release 則用於釋放這種訪問權,釋放一致性(Release Consistency)模型訪存事件發生次序的限制如下:(1). 同步操作的執行滿足順序一致性條件; (2). 在任一普通訪存操作被允許執行之前,所有在同一處理器中先於這一訪存操作的 Acquire 操作都已完成; (3). 在任一 Release 操作被允許執行之前,所有在同一處理器中先於這一 Release 操作的普通操作都已完成。

在硬件實現的釋放一致性模型中,對共享單元的訪存是及時進行的,並在執行獲取操作(Acquire)和釋放操作(Release)時對齊。在共享虛擬存儲系統或者在由軟件維護的數據一致性的共享存儲系統中,由於通信和數據交換的開銷很大,有必要減少通信和數據交換的次數。爲此,人們在釋放一致性(Release Consistency)模型的基礎上提出了急切更新釋放一致性模型(Eager Release Consistency)和懶惰更新釋放一致性模型(Lazy Release Consistency)。在急切更新釋放一致性模型中,在臨界區內的多個存數操作對共享內存的更新不是及時進行的,而是在執行 Release 操作之前(即退出臨界區之前)集中進行,把多個存數操作合併在一起統一執行,從而減少了通信次數。而在懶惰更新釋放一致性模型中,由一個處理器對某單元的存數操作並不是由此處理器主動傳播到所有共享該單元的其他處理器,而是在其他處理器要用到此處理器所寫的數據時(即其他處理器執行 Acquire 操作時)再向此處理器索取該單元的最新備份,這樣可以進一步減少通信量。

===============================================================================

好了,本文主要介紹了內存模型的相關概念,並重點介紹了順序一致性模型(附帶介紹了幾種常見的存儲一致性模型),並以一個實際的小例子向大家介紹了爲什麼程序員需要理解內存模型,總之,C++ 程序員要想寫出高性能的多線程程序必須理解內存模型,因爲編譯器會給你的程序做優化(如指令重排等),CPU 爲了提升性能也有多發射和亂序執行,因此程序在最終執行時並不會按照你之前的原始代碼順序來執行,所以內存模型是程序員、編譯器,CPU 之間的契約,遵守契約後大家就各自做優化,從而儘可能提高程序的性能。

下一節我將給大家介紹 C++11 內存模型中的 6 種訪存次序(或內存序)(std::memory_order_relaxed, std::memory_order_consume, std::memory_order_acquire, std::memory_order_release, std::memory_order_acq_rel, std::memory_order_seq_cst)各自的意義以及常見的用法,希望感興趣的同學繼續關注,如果您發現文中的錯誤,一定儘快告訴我 ;-)

另外,後續的幾篇博客我會給大家介紹更多的與內存模型相關的知識,我在 Github 上維護了一個頁面,主要是與內存模型相關資料的鏈接,感興趣的同學可以參考裏面的資料自己閱讀。



C++11 併發指南九(綜合運用: C++11 多線程下生產者消費者模型詳解)



前面八章介紹了 C++11 併發編程的基礎(抱歉哈,第五章-第八章還在草稿中),本文將綜合運用 C++11 中的新的基礎設施(主要是多線程、鎖、條件變量)來闡述一個經典問題——生產者消費者模型,並給出完整的解決方案。

生產者消費者問題是多線程併發中一個非常經典的問題,相信學過操作系統課程的同學都清楚這個問題的根源。本文將就四種情況分析並介紹生產者和消費者問題,它們分別是:單生產者-單消費者模型,單生產者-多消費者模型,多生產者-單消費者模型,多生產者-多消費者模型,我會給出四種情況下的 C++11 併發解決方案,如果文中出現了錯誤或者你對代碼有異議,歡迎交流 ;-)。

單生產者-單消費者模型

顧名思義,單生產者-單消費者模型中只有一個生產者和一個消費者,生產者不停地往產品庫中放入產品,消費者則從產品庫中取走產品,產品庫容積有限制,只能容納一定數目的產品,如果生產者生產產品的速度過快,則需要等待消費者取走產品之後,產品庫不爲空才能繼續往產品庫中放置新的產品,相反,如果消費者取走產品的速度過快,則可能面臨產品庫中沒有產品可使用的情況,此時需要等待生產者放入一個產品後,消費者才能繼續工作。C++11實現單生產者單消費者模型的代碼如下:

複製代碼
#include <unistd.h>

#include <cstdlib>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

static const int kItemRepositorySize  = 10; // Item buffer size.
static const int kItemsToProduce  = 1000;   // How many items we plan to produce.

struct ItemRepository {
    int item_buffer[kItemRepositorySize]; // 產品緩衝區, 配合 read_position 和 write_position 模型環形隊列.
    size_t read_position; // 消費者讀取產品位置.
    size_t write_position; // 生產者寫入產品位置.
    std::mutex mtx; // 互斥量,保護產品緩衝區
    std::condition_variable repo_not_full; // 條件變量, 指示產品緩衝區不爲滿.
    std::condition_variable repo_not_empty; // 條件變量, 指示產品緩衝區不爲空.
} gItemRepository; // 產品庫全局變量, 生產者和消費者操作該變量.

typedef struct ItemRepository ItemRepository;


void ProduceItem(ItemRepository *ir, int item)
{
    std::unique_lock<std::mutex> lock(ir->mtx);
    while(((ir->write_position + 1) % kItemRepositorySize)
        == ir->read_position) { // item buffer is full, just wait here.
        std::cout << "Producer is waiting for an empty slot...\n";
        (ir->repo_not_full).wait(lock); // 生產者等待"產品庫緩衝區不爲滿"這一條件發生.
    }

    (ir->item_buffer)[ir->write_position] = item; // 寫入產品.
    (ir->write_position)++; // 寫入位置後移.

    if (ir->write_position == kItemRepositorySize) // 寫入位置若是在隊列最後則重新設置爲初始位置.
        ir->write_position = 0;

    (ir->repo_not_empty).notify_all(); // 通知消費者產品庫不爲空.
    lock.unlock(); // 解鎖.
}

int ConsumeItem(ItemRepository *ir)
{
    int data;
    std::unique_lock<std::mutex> lock(ir->mtx);
    // item buffer is empty, just wait here.
    while(ir->write_position == ir->read_position) {
        std::cout << "Consumer is waiting for items...\n";
        (ir->repo_not_empty).wait(lock); // 消費者等待"產品庫緩衝區不爲空"這一條件發生.
    }

    data = (ir->item_buffer)[ir->read_position]; // 讀取某一產品
    (ir->read_position)++; // 讀取位置後移

    if (ir->read_position >= kItemRepositorySize) // 讀取位置若移到最後,則重新置位.
        ir->read_position = 0;

    (ir->repo_not_full).notify_all(); // 通知消費者產品庫不爲滿.
    lock.unlock(); // 解鎖.

    return data; // 返回產品.
}


void ProducerTask() // 生產者任務
{
    for (int i = 1; i <= kItemsToProduce; ++i) {
        // sleep(1);
        std::cout << "Produce the " << i << "^th item..." << std::endl;
        ProduceItem(&gItemRepository, i); // 循環生產 kItemsToProduce 個產品.
    }
}

void ConsumerTask() // 消費者任務
{
    static int cnt = 0;
    while(1) {
        sleep(1);
        int item = ConsumeItem(&gItemRepository); // 消費一個產品.
        std::cout << "Consume the " << item << "^th item" << std::endl;
        if (++cnt == kItemsToProduce) break; // 如果產品消費個數爲 kItemsToProduce, 則退出.
    }
}

void InitItemRepository(ItemRepository *ir)
{
    ir->write_position = 0; // 初始化產品寫入位置.
    ir->read_position = 0; // 初始化產品讀取位置.
}

int main()
{
    InitItemRepository(&gItemRepository);
    std::thread producer(ProducerTask); // 創建生產者線程.
    std::thread consumer(ConsumerTask); // 創建消費之線程.
    producer.join();
    consumer.join();
}
複製代碼

 單生產者-多消費者模型

與單生產者和單消費者模型不同的是,單生產者-多消費者模型中可以允許多個消費者同時從產品庫中取走產品。所以除了保護產品庫在多個讀寫線程下互斥之外,還需要維護消費者取走產品的計數器,代碼如下:

複製代碼
#include <unistd.h>

#include <cstdlib>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

static const int kItemRepositorySize  = 4; // Item buffer size.
static const int kItemsToProduce  = 10;   // How many items we plan to produce.

struct ItemRepository {
    int item_buffer[kItemRepositorySize];
    size_t read_position;
    size_t write_position;
    size_t item_counter;
    std::mutex mtx;
    std::mutex item_counter_mtx;
    std::condition_variable repo_not_full;
    std::condition_variable repo_not_empty;
} gItemRepository;

typedef struct ItemRepository ItemRepository;


void ProduceItem(ItemRepository *ir, int item)
{
    std::unique_lock<std::mutex> lock(ir->mtx);
    while(((ir->write_position + 1) % kItemRepositorySize)
        == ir->read_position) { // item buffer is full, just wait here.
        std::cout << "Producer is waiting for an empty slot...\n";
        (ir->repo_not_full).wait(lock);
    }

    (ir->item_buffer)[ir->write_position] = item;
    (ir->write_position)++;

    if (ir->write_position == kItemRepositorySize)
        ir->write_position = 0;

    (ir->repo_not_empty).notify_all();
    lock.unlock();
}

int ConsumeItem(ItemRepository *ir)
{
    int data;
    std::unique_lock<std::mutex> lock(ir->mtx);
    // item buffer is empty, just wait here.
    while(ir->write_position == ir->read_position) {
        std::cout << "Consumer is waiting for items...\n";
        (ir->repo_not_empty).wait(lock);
    }

    data = (ir->item_buffer)[ir->read_position];
    (ir->read_position)++;

    if (ir->read_position >= kItemRepositorySize)
        ir->read_position = 0;

    (ir->repo_not_full).notify_all();
    lock.unlock();

    return data;
}


void ProducerTask()
{
    for (int i = 1; i <= kItemsToProduce; ++i) {
        // sleep(1);
        std::cout << "Producer thread " << std::this_thread::get_id()
            << " producing the " << i << "^th item..." << std::endl;
        ProduceItem(&gItemRepository, i);
    }
    std::cout << "Producer thread " << std::this_thread::get_id()
                << " is exiting..." << std::endl;
}

void ConsumerTask()
{
    bool ready_to_exit = false;
    while(1) {
        sleep(1);
        std::unique_lock<std::mutex> lock(gItemRepository.item_counter_mtx);
        if (gItemRepository.item_counter < kItemsToProduce) {
            int item = ConsumeItem(&gItemRepository);
            ++(gItemRepository.item_counter);
            std::cout << "Consumer thread " << std::this_thread::get_id()
                << " is consuming the " << item << "^th item" << std::endl;
        } else ready_to_exit = true;
        lock.unlock();
        if (ready_to_exit == true) break;
    }
    std::cout << "Consumer thread " << std::this_thread::get_id()
                << " is exiting..." << std::endl;
}

void InitItemRepository(ItemRepository *ir)
{
    ir->write_position = 0;
    ir->read_position = 0;
    ir->item_counter = 0;
}

int main()
{
    InitItemRepository(&gItemRepository);
    std::thread producer(ProducerTask);
    std::thread consumer1(ConsumerTask);
    std::thread consumer2(ConsumerTask);
    std::thread consumer3(ConsumerTask);
    std::thread consumer4(ConsumerTask);

    producer.join();
    consumer1.join();
    consumer2.join();
    consumer3.join();
    consumer4.join();
}
複製代碼

 多生產者-單消費者模型

與單生產者和單消費者模型不同的是,多生產者-單消費者模型中可以允許多個生產者同時向產品庫中放入產品。所以除了保護產品庫在多個讀寫線程下互斥之外,還需要維護生產者放入產品的計數器,代碼如下:

複製代碼
#include <unistd.h>

#include <cstdlib>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

static const int kItemRepositorySize  = 4; // Item buffer size.
static const int kItemsToProduce  = 10;   // How many items we plan to produce.

struct ItemRepository {
    int item_buffer[kItemRepositorySize];
    size_t read_position;
    size_t write_position;
    size_t item_counter;
    std::mutex mtx;
    std::mutex item_counter_mtx;
    std::condition_variable repo_not_full;
    std::condition_variable repo_not_empty;
} gItemRepository;

typedef struct ItemRepository ItemRepository;


void ProduceItem(ItemRepository *ir, int item)
{
    std::unique_lock<std::mutex> lock(ir->mtx);
    while(((ir->write_position + 1) % kItemRepositorySize)
        == ir->read_position) { // item buffer is full, just wait here.
        std::cout << "Producer is waiting for an empty slot...\n";
        (ir->repo_not_full).wait(lock);
    }

    (ir->item_buffer)[ir->write_position] = item;
    (ir->write_position)++;

    if (ir->write_position == kItemRepositorySize)
        ir->write_position = 0;

    (ir->repo_not_empty).notify_all();
    lock.unlock();
}

int ConsumeItem(ItemRepository *ir)
{
    int data;
    std::unique_lock<std::mutex> lock(ir->mtx);
    // item buffer is empty, just wait here.
    while(ir->write_position == ir->read_position) {
        std::cout << "Consumer is waiting for items...\n";
        (ir->repo_not_empty).wait(lock);
    }

    data = (ir->item_buffer)[ir->read_position];
    (ir->read_position)++;

    if (ir->read_position >= kItemRepositorySize)
        ir->read_position = 0;

    (ir->repo_not_full).notify_all();
    lock.unlock();

    return data;
}

void ProducerTask()
{
    bool ready_to_exit = false;
    while(1) {
        sleep(1);
        std::unique_lock<std::mutex> lock(gItemRepository.item_counter_mtx);
        if (gItemRepository.item_counter < kItemsToProduce) {
            ++(gItemRepository.item_counter);
            ProduceItem(&gItemRepository, gItemRepository.item_counter);
            std::cout << "Producer thread " << std::this_thread::get_id()
                << " is producing the " << gItemRepository.item_counter
                << "^th item" << std::endl;
        } else ready_to_exit = true;
        lock.unlock();
        if (ready_to_exit == true) break;
    }
    std::cout << "Producer thread " << std::this_thread::get_id()
                << " is exiting..." << std::endl;
}

void ConsumerTask()
{
    static int item_consumed = 0;
    while(1) {
        sleep(1);
        ++item_consumed;
        if (item_consumed <= kItemsToProduce) {
            int item = ConsumeItem(&gItemRepository);
            std::cout << "Consumer thread " << std::this_thread::get_id()
                << " is consuming the " << item << "^th item" << std::endl;
        } else break;
    }
    std::cout << "Consumer thread " << std::this_thread::get_id()
                << " is exiting..." << std::endl;
}

void InitItemRepository(ItemRepository *ir)
{
    ir->write_position = 0;
    ir->read_position = 0;
    ir->item_counter = 0;
}

int main()
{
    InitItemRepository(&gItemRepository);
    std::thread producer1(ProducerTask);
    std::thread producer2(ProducerTask);
    std::thread producer3(ProducerTask);
    std::thread producer4(ProducerTask);
    std::thread consumer(ConsumerTask);

    producer1.join();
    producer2.join();
    producer3.join();
    producer4.join();
    consumer.join();
}
複製代碼

多生產者-多消費者模型

該模型可以說是前面兩種模型的綜合,程序需要維護兩個計數器,分別是生產者已生產產品的數目和消費者已取走產品的數目。另外也需要保護產品庫在多個生產者和多個消費者互斥地訪問。

代碼如下:

複製代碼
#include <unistd.h>

#include <cstdlib>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

static const int kItemRepositorySize  = 4; // Item buffer size.
static const int kItemsToProduce  = 10;   // How many items we plan to produce.

struct ItemRepository {
    int item_buffer[kItemRepositorySize];
    size_t read_position;
    size_t write_position;
    size_t produced_item_counter;
    size_t consumed_item_counter;
    std::mutex mtx;
    std::mutex produced_item_counter_mtx;
    std::mutex consumed_item_counter_mtx;
    std::condition_variable repo_not_full;
    std::condition_variable repo_not_empty;
} gItemRepository;

typedef struct ItemRepository ItemRepository;


void ProduceItem(ItemRepository *ir, int item)
{
    std::unique_lock<std::mutex> lock(ir->mtx);
    while(((ir->write_position + 1) % kItemRepositorySize)
        == ir->read_position) { // item buffer is full, just wait here.
        std::cout << "Producer is waiting for an empty slot...\n";
        (ir->repo_not_full).wait(lock);
    }

    (ir->item_buffer)[ir->write_position] = item;
    (ir->write_position)++;

    if (ir->write_position == kItemRepositorySize)
        ir->write_position = 0;

    (ir->repo_not_empty).notify_all();
    lock.unlock();
}

int ConsumeItem(ItemRepository *ir)
{
    int data;
    std::unique_lock<std::mutex> lock(ir->mtx);
    // item buffer is empty, just wait here.
    while(ir->write_position == ir->read_position) {
        std::cout << "Consumer is waiting for items...\n";
        (ir->repo_not_empty).wait(lock);
    }

    data = (ir->item_buffer)[ir->read_position];
    (ir->read_position)++;

    if (ir->read_position >= kItemRepositorySize)
        ir->read_position = 0;

    (ir->repo_not_full).notify_all();
    lock.unlock();

    return data;
}

void ProducerTask()
{
    bool ready_to_exit = false;
    while(1) {
        sleep(1);
        std::unique_lock<std::mutex> lock(gItemRepository.produced_item_counter_mtx);
        if (gItemRepository.produced_item_counter < kItemsToProduce) {
            ++(gItemRepository.produced_item_counter);
            ProduceItem(&gItemRepository, gItemRepository.produced_item_counter);
            std::cout << "Producer thread " << std::this_thread::get_id()
                << " is producing the " << gItemRepository.produced_item_counter
                << "^th item" << std::endl;
        } else ready_to_exit = true;
        lock.unlock();
        if (ready_to_exit == true) break;
    }
    std::cout << "Producer thread " << std::this_thread::get_id()
                << " is exiting..." << std::endl;
}

void ConsumerTask()
{
    bool ready_to_exit = false;
    while(1) {
        sleep(1);
        std::unique_lock<std::mutex> lock(gItemRepository.consumed_item_counter_mtx);
        if (gItemRepository.consumed_item_counter < kItemsToProduce) {
            int item = ConsumeItem(&gItemRepository);
            ++(gItemRepository.consumed_item_counter);
            std::cout << "Consumer thread " << std::this_thread::get_id()
                << " is consuming the " << item << "^th item" << std::endl;
        } else ready_to_exit = true;
        lock.unlock();
        if (ready_to_exit == true) break;
    }
    std::cout << "Consumer thread " << std::this_thread::get_id()
                << " is exiting..." << std::endl;
}

void InitItemRepository(ItemRepository *ir)
{
    ir->write_position = 0;
    ir->read_position = 0;
    ir->produced_item_counter = 0;
    ir->consumed_item_counter = 0;
}

int main()
{
    InitItemRepository(&gItemRepository);
    std::thread producer1(ProducerTask);
    std::thread producer2(ProducerTask);
    std::thread producer3(ProducerTask);
    std::thread producer4(ProducerTask);

    std::thread consumer1(ConsumerTask);
    std::thread consumer2(ConsumerTask);
    std::thread consumer3(ConsumerTask);
    std::thread consumer4(ConsumerTask);

    producer1.join();
    producer2.join();
    producer3.join();
    producer4.join();

    consumer1.join();
    consumer2.join();
    consumer3.join();
    consumer4.join();
}
複製代碼

 另外,所有例子的代碼(包括前面一些指南的代碼均放在github上),希望對大家學習 C++11 多線程併發有所幫助。



文章來源:http://www.cnblogs.com/haippy/p/3284540.html 





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章