[转] 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=().
}