├── 01-thread.MD ├── 02-mutex.MD ├── 03-mutex-2.MD ├── 04-condition_variable.MD ├── 05-async-1-promise.MD ├── 05-async-2-packaged_task.MD ├── 05-async-3-future.MD ├── 06-thread_local.MD ├── 07-atomic-1.MD ├── 07-atomic-2.MD ├── 08-memory_order.MD ├── 09-generic_function.MD ├── 10-generic_class.MD ├── README.md ├── code ├── chan.h └── chan.simple.h └── images ├── atomic_flag-test_and_set.png ├── cache_inconformity.png ├── compare_exchange.png ├── compiler_recursive.png ├── cpu_arch.png ├── cpu_inorder.png ├── synchronized_with.png ├── template_class_specify.png ├── template_function.png ├── template_function_specify.png ├── thread_local.png └── thread_seq.png /01-thread.MD: -------------------------------------------------------------------------------- 1 | # C++11多线程(简约但不简单) 2 | ## 一、简单使用 3 | C++11提供了一套精练的线程库,小巧且易用。运行一个线程,可以直接创建一个std::thread的实例,线程在实例成功构造成时启动。若有底层平台支持,成员函数std::thread ::native_handle()将可提供对原生线程对象运行平台特定的操作。 4 | ```c++ 5 | #include 6 | #include 7 | 8 | void foo() { 9 | std::cout << "Hello C++11" << std::endl; 10 | } 11 | 12 | int main() { 13 | std::thread thread(foo); // 启动线程foo 14 | thread.join(); // 等待线程执行完成 15 | 16 | return 0; 17 | } 18 | ``` 19 | 编译并运行,程序输出: 20 | > Hello C++11 21 | 22 | ### 1、线程参数 23 | 当需要向线程传递参数时,可以直接通过std::thread的构造函数参数进行,构造函数通过完美转发将参数传递给线程函数。 24 | ```c++ 25 | #include 26 | #include 27 | 28 | void hello(const char *name) { 29 | std::cout << "Hello " << name << std::endl; 30 | } 31 | 32 | int main() { 33 | std::thread thread(hello, "C++11"); 34 | thread.join(); 35 | 36 | return 0; 37 | } 38 | ``` 39 | 40 | ### 2. 类成员函数做为线程入口 41 | 类成员函数做为线程入口时,仍然十分简单: **把this做为第一个参数传递进去即可。** 42 | ```c++ 43 | #include 44 | #include 45 | 46 | class Greet 47 | { 48 | const char *owner = "Greet"; 49 | public: 50 | void SayHello(const char *name) { 51 | std::cout << "Hello " << name << " from " << this->owner << std::endl; 52 | } 53 | }; 54 | int main() { 55 | Greet greet; 56 | 57 | std::thread thread(&Greet::SayHello, &greet, "C++11"); 58 | thread.join(); 59 | 60 | return 0; 61 | } 62 | //输出:Hello C++11 from Greet 63 | ``` 64 | 65 | ### 3. join: 等待线程执行完成 66 | 线程如果像二哈似的撒手没,则程序铁定悲剧。因此std::thread提供了几个线程管理的工具,其中join就是很重要的一个:等待线程执行完成。即使当线程函数已经执行完成后,调用join仍然是有效的。 67 | ### 4. 线程暂停 68 | 从外部让线程暂停,会引发很多并发问题。大家可以百度一下,此处不做引申。这大概也是std::thread并没有直接提供pause函数的原因。但有时线程在运行时,确实需要“停顿”一段时间怎么办呢?可以使用std::this_thread::sleep_for或std::this_thread::sleep_until 69 | ```c++ 70 | #include 71 | #include 72 | #include 73 | 74 | using namespace std::chrono; 75 | 76 | void pausable() { 77 | // sleep 500毫秒 78 | std::this_thread::sleep_for(milliseconds(500)); 79 | // sleep 到指定时间点 80 | std::this_thread::sleep_until(system_clock::now() + milliseconds(500)); 81 | } 82 | 83 | int main() { 84 | std::thread thread(pausable); 85 | thread.join(); 86 | 87 | return 0; 88 | } 89 | ``` 90 | ### 5. 线程停止 91 | 一般情况下当线程函数执行完成后,线程“自然”停止。但在std::thread中有一种情况会造成线程**异常终止**,那就是:**析构**。当std::thread实例析构时,如果线程还在运行,则线程会被强行终止掉,这可能会造成资源的泄漏,因此尽量在析构前join一下,以确保线程成功结束。 92 | 如果确实想提前让线程结束怎么办呢?一个简单的方法是使用“共享变量”,线程定期地去检测该量,如果需要退出,则停止执行,退出线程函数。使用“共享变量”需要注意,在多核、多CPU的情况下需要使用“原子”操作,关于原子操作后面会有专题讲述。 93 | 94 | ## 二、进阶(更多你可能需要知道的) 95 | ### 1. 拷贝 96 | ```c++ 97 | std::thread a(foo); 98 | std::thread b; 99 | b = a; 100 | ``` 101 | 当执行以上代码时,会发生什么?最终foo线程是由a管理,还是b来管理?答案是由b来管理。std::thread被设计为只能由一个实例来维护线程状态,以及对线程进行操作。因此当发生赋值操作时,会发生线程所有权转移。在macos下std::thread的赋值函数原型为: 102 | ```c++ 103 | thread& operator=(thread&& a); 104 | ``` 105 | 赋完值后,原来由a管理的线程改为由b管理,a不再指向任何线程(相当于执行了detach操作)。如果b原本指向了一个线程,那么这个线程会被终止掉。 106 | ### 2. detach/joinable 107 | detach是std::thread的成员函数,函数原型为: 108 | ```c++ 109 | void detach(); 110 | bool joinable() const; 111 | ``` 112 | detach以后就失去了对线程的所有权,不能再调用join了,因为线程已经分离出去了,不再归该实例管了。判断线程是否还有对线程的所有权的一个简单方式是调用joinable函数,返回true则有,否则为无。 113 | ### 3. 线程内部调用自身的join 114 | 自己等待自己执行结束?如果程序员真这么干,那这个程序员一定是脑子短路了。对于这种行为C++11只能抛异常了。 115 | 116 | ## 三、其它 117 | ### 1. get_id 118 | 每个线程都有一个id,但此处的get_id与系统分配给线程的ID并不一是同一个东东。如果想取得系统分配的线程ID,可以调用native_handle函数。 119 | ### 2. 逻辑运算? 120 | 有些平台下std::thread还支持若干逻辑运算,比如Visual C++, 但这并不是标准库的行为,不要在跨平台的场景中使用。 121 | 122 | -------------------------------------------------------------------------------- /02-mutex.MD: -------------------------------------------------------------------------------- 1 | # C++11多线程-mutex(1) 2 | mutex又称互斥量,用于提供对共享变量的互斥访问。C++11中mutex相关的类都在\头文件中。共四种互斥类: 3 | 4 | 5 | 6 | 7 | 8 | 9 |
序号名称用途
1std::mutex最基本也是最常用的互斥类
2std::recursive_mutex同一线程内可递归(重入)的互斥类
3std::timed_mutex除具备mutex功能外,还提供了带时限请求锁定的能力
4std::recursive_timed_mutex同一线程内可递归(重入)的timed_mutex
10 | 与std::thread一样,mutex相关类不支持拷贝构造、不支持赋值。同时mutex类也不支持move语义(move构造、move赋值)。不用担心会误用这些操作,真要这么做了的话,编译器会阻止你的。 11 | 12 | ## 一、lock, try_lock, unlock 13 | mutex的标准操作,四个mutex类都支持这些操作,但是不同类在行为上有些微的差异。 14 | ### lock 15 | 锁住互斥量。调用lock时有三种情况: 16 | 1. 如果互斥量没有被锁住,则调用线程将该mutex锁住,直到调用线程调用unlock释放。 17 | 2. 如果mutex已被其它线程lock,则调用线程将被阻塞,直到其它线程unlock该mutex。 18 | 3. 如果当前mutex已经被调用者线程锁住,则std::mutex死锁,而recursive系列则成功返回。 19 | ### try_lock 20 | 尝试锁住mutex,调用该函数同样也有三种情况: 21 | 1. 如果互斥量没有被锁住,则调用线程将该mutex锁住(返回true),直到调用线程调用unlock释放。 22 | 2. 如果mutex已被其它线程lock,则调用线程将失败,并返回false。 23 | 3. 如果当前mutex已经被调用者线程锁住,则std::mutex死锁,而recursive系列则成功返回true。 24 | ### unlock 25 | 解锁mutex,释放对mutex的所有权。值得一提的时,对于recursive系列mutex,unlock次数需要与lock次数相同才可以完全解锁。
26 | 下面给出一个mutex小例子 27 | ```c++ 28 | #include 29 | #include 30 | #include 31 | 32 | void inc(std::mutex &mutex, int loop, int &counter) { 33 | for (int i = 0; i < loop; i++) { 34 | mutex.lock(); 35 | ++counter; 36 | mutex.unlock(); 37 | } 38 | } 39 | int main() { 40 | std::thread threads[5]; 41 | std::mutex mutex; 42 | int counter = 0; 43 | 44 | for (std::thread &thr: threads) { 45 | thr = std::thread(inc, std::ref(mutex), 1000, std::ref(counter)); 46 | } 47 | for (std::thread &thr: threads) { 48 | thr.join(); 49 | } 50 | 51 | // 输出:5000,如果inc中调用的是try_lock,则此处可能会<5000 52 | std::cout << counter << std::endl; 53 | 54 | return 0; 55 | } 56 | //: g++ -std=c++11 main.cpp 57 | ``` 58 | 59 | ## 二、try_lock_for, try_lock_until 60 | 这两个函数仅用于timed系列的mutex(std::timed_mutex, std::recursive_timed_mutex),函数最多会等待指定的时间,如果仍未获得锁,则返回false。除超时设定外,这两个函数与try_lock行为一致。 61 | ```c++ 62 | // 等待指定时长 63 | template 64 | try_lock_for(const chrono::duration& rel_time); 65 | // 等待到指定时间 66 | template 67 | try_lock_until(const chrono::time_point& abs_time); 68 | ``` 69 | try_lock_for相关代码 70 | ```c++ 71 | #include 72 | #include 73 | #include 74 | #include 75 | 76 | void run500ms(std::timed_mutex &mutex) { 77 | auto _500ms = std::chrono::milliseconds(500); 78 | if (mutex.try_lock_for(_500ms)) { 79 | std::cout << "获得了锁" << std::endl; 80 | } else { 81 | std::cout << "未获得锁" << std::endl; 82 | } 83 | } 84 | int main() { 85 | std::timed_mutex mutex; 86 | 87 | mutex.lock(); 88 | std::thread thread(run500ms, std::ref(mutex)); 89 | thread.join(); 90 | mutex.unlock(); 91 | 92 | return 0; 93 | } 94 | //输出:未获得锁 95 | ``` 96 | ### 其它 97 | mutex文件中还提供了lock_guard, unique_lock,std::call_once, std::try_lock, std::lock(批量上锁)操作,由于篇幅关系,我们下次再讲。 98 | -------------------------------------------------------------------------------- /03-mutex-2.MD: -------------------------------------------------------------------------------- 1 | # C++11多线程-mutex(2) 2 | C++11在提供了常规mutex的基础上,还提供了一些易用性的类,本节我们将一起看一下这些类。 3 | ## 1. lock_guard 4 | lock_guard利用了C++ RAII的特性,在构造函数中上锁,析构函数中解锁。lock_guard是一个模板类,其原型为 5 | ```c++ 6 | template class lock_guard 7 | ``` 8 | 模板参数Mutex代表互斥量,可以是上一篇介绍的std::mutex, std::timed_mutex, std::recursive_mutex, std::recursive_timed_mutex中的任何一个,也可以是std::unique_lock(下面即将介绍),这些都提供了lock和unlock的能力。
9 | lock_guard仅用于上锁、解锁,不对mutex承担供任何生周期的管理,因此在使用的时候,请确保lock_guard管理的mutex一直有效。 10 | 同其它mutex类一样,locak_guard不允许拷贝,即拷贝构造和赋值函数被声明为delete。 11 | ```c++ 12 | lock_guard(lock_guard const&) = delete; 13 | lock_guard& operator=(lock_guard const&) = delete; 14 | ``` 15 | lock_guard的设计保证了即使程序在锁定期间发生了异常,也会安全的释放锁,不会发生死锁。 16 | ```c++ 17 | #include 18 | #include 19 | 20 | std::mutex mutex; 21 | 22 | void safe_thread() { 23 | try { 24 | std::lock_guard _guard(mutex); 25 | throw std::logic_error("logic error"); 26 | } catch (std::exception &ex) { 27 | std::cerr << "[caught] " << ex.what() << std::endl; 28 | } 29 | } 30 | int main() { 31 | safe_thread(); 32 | // 此处仍能上锁 33 | mutex.lock(); 34 | std::cout << "OK, still locked" << std::endl; 35 | mutex.unlock(); 36 | 37 | return 0; 38 | } 39 | ``` 40 | 程序输出 41 | ```console 42 | [caught] logic error 43 | OK, still locked 44 | ``` 45 | ## 2. unique_lock 46 | lock_guard提供了简单上锁、解锁操作,但当我们需要更灵活的操作时便无能为力了。这些就需要unique_lock上场了。unique_lock拥有对Mutex的**所有权**,一但初始化了unique_lock,其就接管了该mutex, 在unique_lock结束生命周期前(析构前),其它地方就不要再直接使用该mutex了。unique_lock提供的功能较多,此处不一一列举,下面列出unique_lock的类声明,及部分注释,供大家参考 47 | ```c++ 48 | template 49 | class unique_lock 50 | { 51 | public: 52 | typedef Mutex mutex_type; 53 | // 空unique_lock对象 54 | unique_lock() noexcept; 55 | // 管理m, 并调用m.lock进行上锁,如果m已被其它线程锁定,由该构造了函数会阻塞。 56 | explicit unique_lock(mutex_type& m); 57 | // 仅管理m,构造函数中不对m上锁。可以在初始化后调用lock, try_lock, try_lock_xxx系列进行上锁。 58 | unique_lock(mutex_type& m, defer_lock_t) noexcept; 59 | // 管理m, 并调用m.try_lock,上锁不成功不会阻塞当前线程 60 | unique_lock(mutex_type& m, try_to_lock_t); 61 | // 管理m, 该函数假设m已经被当前线程锁定,不再尝试上锁。 62 | unique_lock(mutex_type& m, adopt_lock_t); 63 | // 管理m, 并调用m.try_lock_unitil函数进行加锁 64 | template 65 | unique_lock(mutex_type& m, const chrono::time_point& abs_time); 66 | // 管理m,并调用m.try_lock_for函数进行加锁 67 | template 68 | unique_lock(mutex_type& m, const chrono::duration& rel_time); 69 | // 析构,如果此前成功加锁(或通过adopt_lock_t进行构造),并且对mutex拥有所有权,则解锁mutex 70 | ~unique_lock(); 71 | 72 | // 禁止拷贝操作 73 | unique_lock(unique_lock const&) = delete; 74 | unique_lock& operator=(unique_lock const&) = delete; 75 | 76 | // 禁止move语义 77 | unique_lock(unique_lock&& u) noexcept; 78 | unique_lock& operator=(unique_lock&& u) noexcept; 79 | 80 | void lock(); 81 | bool try_lock(); 82 | 83 | template 84 | bool try_lock_for(const chrono::duration& rel_time); 85 | template 86 | bool try_lock_until(const chrono::time_point& abs_time); 87 | 88 | // 显示式解锁,该函数调用后,除非再次调用lock系列函数进行上锁,否则析构中不再进行解锁 89 | void unlock(); 90 | 91 | // 与另一个unique_lock交换所有权 92 | void swap(unique_lock& u) noexcept; 93 | // 返回当前管理的mutex对象的指针,并释放所有权 94 | mutex_type* release() noexcept; 95 | 96 | // 当前实例是否获得了锁 97 | bool owns_lock() const noexcept; 98 | // 同owns_lock 99 | explicit operator bool () const noexcept; 100 | // 返回mutex指针,便于开发人员进行更灵活的操作 101 | // 注意:此时mutex的所有权仍归unique_lock所有,因此不要对mutex进行加锁、解锁操作 102 | mutex_type* mutex() const noexcept; 103 | }; 104 | ``` 105 | ## 3. std::call_once 106 | 该函数的作用顾名思义:保证call_once调用的函数只被执行一次。该函数需要与std::once_flag配合使用。std::once_flag被设计为对外封闭的,即外部没有任何渠道可以改变once_flag的值,仅可以通过std::call_once函数修改。一般情况下我们在自己实现call_once效果时,往往使用一个全局变量,以及双重检查锁(DCL)来实现,即便这样该实现仍然会有很多坑(多核环境下)。有兴趣的读者可以搜索一下DCL来看,此处不再赘述。
107 | C++11为我们提供了简便的解决方案,所需做的仅仅像下面这样使用即可。 108 | ```c++ 109 | #include 110 | #include 111 | #include 112 | 113 | void initialize() { 114 | std::cout << __FUNCTION__ << std::endl; 115 | } 116 | 117 | std::once_flag of; 118 | void my_thread() { 119 | std::call_once(of, initialize); 120 | } 121 | 122 | int main() { 123 | std::thread threads[10]; 124 | for (std::thread &thr: threads) { 125 | thr = std::thread(my_thread); 126 | } 127 | for (std::thread &thr: threads) { 128 | thr.join(); 129 | } 130 | return 0; 131 | } 132 | // 仅输出一次:initialize 133 | ``` 134 | ## 4. std::try_lock 135 | 当有多个mutex需要执行try_lock时,该函数提供了简便的操作。try_lock会按参数从左到右的顺序,对mutex**顺次执行**try_lock操作。当其中某个mutex.try_lock失败(返回false或抛出异常)时,已成功锁定的mutex都将被解锁。
136 | 需要注意的是,该函数成功时返回-1, 否则返回失败mutex的索引,索引从0开始计数。 137 | ```c++ 138 | template 139 | int try_lock(L1&, L2&, L3&...); 140 | ``` 141 | ## 5. std::lock 142 | std::lock是较智能的上批量上锁方式,采用死锁算法来锁定给定的mutex列表,避免死锁。该函数对mutex列表的上锁顺序是不确定的。该函数保证: 如果成功,则所有mutex全部上锁,如果失败,则全部解锁。 143 | ```c++ 144 | template 145 | void lock(L1&, L2&, L3&...); 146 | ``` 147 | -------------------------------------------------------------------------------- /04-condition_variable.MD: -------------------------------------------------------------------------------- 1 | # C++11多线程-条件变量(std::condition_variable) 2 | 前面我们介绍了线程(std::thread)和互斥量(std::mutex),互斥量是多线程间同时访问某一共享变量时,保证变量可被安全访问的手段。在多线程编程中,还有另一种十分常见的行为:线程同步。线程同步是指线程间需要按照预定的先后次序顺序进行的行为。C++11对这种行为也提供了有力的支持,这就是条件变量。条件变量位于头文件condition_variable下。本章我们将简要介绍一下该类,在文章的最后我们会综合运用std::mutex和std::condition_variable,实现一个chan类,该类可在多线程间安全的通信,具有广泛的应用场景。 3 | ## 1. std::condition_variable 4 | 条件变量提供了两类操作:wait和notify。这两类操作构成了多线程同步的基础。 5 | ### 1.1 wait 6 | wait是线程的等待动作,直到其它线程将其唤醒后,才会继续往下执行。下面通过伪代码来说明其用法: 7 | ```c++ 8 | std::mutex mutex; 9 | std::condition_variable cv; 10 | 11 | // 条件变量与临界区有关,用来获取和释放一个锁,因此通常会和mutex联用。 12 | std::unique_lock lock(mutex); 13 | // 此处会释放lock,然后在cv上等待,直到其它线程通过cv.notify_xxx来唤醒当前线程,cv被唤醒后会再次对lock进行上锁,然后wait函数才会返回。 14 | // wait返回后可以安全的使用mutex保护的临界区内的数据。此时mutex仍为上锁状态 15 | cv.wait(lock) 16 | ``` 17 | 需要注意的一点是, wait有时会在没有任何线程调用notify的情况下返回,这种情况就是有名的[**spurious wakeup**](https://docs.microsoft.com/zh-cn/windows/desktop/api/synchapi/nf-synchapi-sleepconditionvariablecs)。因此当wait返回时,你需要再次检查wait的前置条件是否满足,如果不满足则需要再次wait。wait提供了重载的版本,用于提供前置检查。 18 | ```c++ 19 | template 20 | void wait(unique_lock &lock, Predicate pred) { 21 | while(!pred()) { 22 | wait(lock); 23 | } 24 | } 25 | ``` 26 | 除wait外, 条件变量还提供了wait_for和wait_until,这两个名称是不是看着有点儿眼熟,std::mutex也提供了_for和_until操作。在C++11多线程编程中,需要等待一段时间的操作,一般情况下都会有xxx_for和xxx_until版本。前者用于等待指定时长,后者用于等待到指定的时间。 27 | ### 1.2 notify 28 | 了解了wait,notify就简单多了:唤醒wait在该条件变量上的线程。notify有两个版本:notify_one和notify_all。 29 | * notify_one 唤醒等待的一个线程,注意只唤醒一个。 30 | * notify_all 唤醒所有等待的线程。使用该函数时应避免出现[惊群效应](https://blog.csdn.net/lyztyycode/article/details/78648798?locationNum=6&fps=1)。 31 | 32 | 其使用方式见下例: 33 | ```c++ 34 | std::mutex mutex; 35 | std::condition_variable cv; 36 | 37 | std::unique_lock lock(mutex); 38 | // 所有等待在cv变量上的线程都会被唤醒。但直到lock释放了mutex,被唤醒的线程才会从wait返回。 39 | cv.notify_all(lock) 40 | ``` 41 | ## 2. 线程间通信 - chan的实现 42 | 有了上面的基础我们就可以设计我们的线程间通讯工具"chan"了。我们的设计目标: 43 | * 在线程间安全的传递数据。golang社区有一句经典的话:不要通过共享内存来通信,要通过通信来共享内存。 44 | * 消除线程线程同步带来的复杂性。 45 | 46 | 我们先来看一下chan的实际使用效果, 生产者-消费者(一个生产者,多个消费者) 47 | ```c++ 48 | #include 49 | #include 50 | #include "chan.h" // chan的头文件 51 | 52 | using namespace std::chrono; 53 | 54 | // 消费数据 55 | void consume(chan ch, int thread_id) { 56 | int n; 57 | while(ch >> n) { 58 | printf("[%d] %d\n", thread_id, n); 59 | std::this_thread::sleep_for(milliseconds(100)); 60 | } 61 | } 62 | 63 | int main() { 64 | chan chInt(3); 65 | 66 | // 消费者 67 | std::thread consumers[5]; 68 | for (int i = 0; i < 5; i++) { 69 | consumers[i] = std::thread(consume, chInt, i+1); 70 | } 71 | 72 | // 生产数据 73 | for (int i = 0; i < 16; i++) { 74 | chInt << i; 75 | } 76 | chInt.close(); // 数据生产完毕 77 | 78 | for (std::thread &thr: consumers) { 79 | thr.join(); 80 | } 81 | 82 | return 0; 83 | } 84 | ``` 85 | ## 源码: 86 | [chan.h](./code/chan.h)
87 | chan.h的简化版,方便理解std::mutex, std::condition_variable: [chan.simple.h](./code/chan.simple.h) 88 | -------------------------------------------------------------------------------- /05-async-1-promise.MD: -------------------------------------------------------------------------------- 1 | # C++11多线程-异步运行之std::promise 2 | 前面介绍了C++11的std::thread、std::mutex以及std::condition_variable,并实现了一个多线程通信的chan类,虽然由于篇幅的限制,该实现有些简陋,甚至有些缺陷,但对于一般情况应该还是够用了。在C++11多线程系列的最后会献上chan的最终版本,敬请期待。

3 | 本文将介绍C++11的另一大特性:异步运行(std::async)。async顾名思义是将一个函数A移至另一线程中去运行。A可以是静态函数、全局函数,甚至类成员函数。在异步运行的过程中,如果A需要向调用者输出结果怎么办呢?std::async完美解决了这一问题。在了解async的解决之道前,我们需要一些知识储备,那就是:std::promise、std::packaged_task和std::future。异步运行涉及的内容较多,我们会分几节来讲。 4 | ## 1. std::promise 5 | std::promise是一个模板类: ```template class promise```。其泛型参数R为std::promise对象保存的值的类型,R可以是void类型。std::promise保存的值可被与之关联的std::future读取,读取操作可以发生在其它线程。std::promise**允许move**语义(右值构造,右值赋值),但**不允许拷贝**(拷贝构造、赋值),std::future亦然。std::promise和std::future合作共同实现了多线程间通信。 6 | ### 1.1 设置std::promise的值 7 | 通过成员函数set_value可以设置std::promise中保存的值,该值最终会被与之关联的std::future::get读取到。**需要注意的是:set_value只能被调用一次,多次调用会抛出std::future_error异常**。事实上std::promise::set_xxx函数会改变std::promise的状态为ready,再次调用时发现状态已要是reday了,则抛出异常。 8 | ```c++ 9 | #include // std::cout, std::endl 10 | #include // std::thread 11 | #include // std::string 12 | #include // std::promise, std::future 13 | #include // seconds 14 | using namespace std::chrono; 15 | 16 | void read(std::future *future) { 17 | // future会一直阻塞,直到有值到来 18 | std::cout << future->get() << std::endl; 19 | } 20 | 21 | int main() { 22 | // promise 相当于生产者 23 | std::promise promise; 24 | // future 相当于消费者, 右值构造 25 | std::future future = promise.get_future(); 26 | // 另一线程中通过future来读取promise的值 27 | std::thread thread(read, &future); 28 | // 让read等一会儿:) 29 | std::this_thread::sleep_for(seconds(1)); 30 | // 31 | promise.set_value("hello future"); 32 | // 等待线程执行完成 33 | thread.join(); 34 | 35 | return 0; 36 | } 37 | // 控制台输: hello future 38 | ``` 39 | 与std::promise关联的std::future是通过std::promise::get_future获取到的,自己构造出来的无效。**一个std::promise实例只能与一个std::future关联共享状态**,当在同一个std::promise上反复调用get_future会抛出future_error异常。

40 | 共享状态。在std::promise构造时,std::promise对象会与共享状态关联起来,这个共享状态可以存储一个R类型的值或者一个由std::exception派生出来的异常值。通过std::promise::get_future调用获得的std::future与std::promise共享相同的共享状态。 41 | ### 1.2 当std::promise不设置值时 42 | 如果promise直到销毁时,都未设置过任何值,则promise会在析构时自动设置为std::future_error,这会造成std::future.get抛出std::future_error异常。 43 | ```c++ 44 | #include // std::cout, std::endl 45 | #include // std::thread 46 | #include // std::promise, std::future 47 | #include // seconds 48 | using namespace std::chrono; 49 | 50 | void read(std::future future) { 51 | try { 52 | future.get(); 53 | } catch(std::future_error &e) { 54 | std::cerr << e.code() << "\n" << e.what() << std::endl; 55 | } 56 | } 57 | 58 | int main() { 59 | std::thread thread; 60 | { 61 | // 如果promise不设置任何值 62 | // 则在promise析构时会自动设置为future_error 63 | // 这会造成future.get抛出该异常 64 | std::promise promise; 65 | thread = std::thread(read, promise.get_future()); 66 | } 67 | thread.join(); 68 | 69 | return 0; 70 | } 71 | ``` 72 | 上面的程序在Clang下输出: 73 | ```console 74 | future:4 75 | The associated promise has been destructed prior to the associated state becoming ready. 76 | ``` 77 | ### 1.3 通过std::promise让std::future抛出异常 78 | 通过std::promise.set_exception函数可以设置自定义异常,该异常最终会被传递到std::future,并在其get函数中被抛出。 79 | ```c++ 80 | #include 81 | #include 82 | #include 83 | #include // std::make_exception_ptr 84 | #include // std::logic_error 85 | 86 | void catch_error(std::future &future) { 87 | try { 88 | future.get(); 89 | } catch (std::logic_error &e) { 90 | std::cerr << "logic_error: " << e.what() << std::endl; 91 | } 92 | } 93 | 94 | int main() { 95 | std::promise promise; 96 | std::future future = promise.get_future(); 97 | 98 | std::thread thread(catch_error, std::ref(future)); 99 | // 自定义异常需要使用make_exception_ptr转换一下 100 | promise.set_exception( 101 | std::make_exception_ptr(std::logic_error("caught"))); 102 | 103 | thread.join(); 104 | return 0; 105 | } 106 | // 输出:logic_error: caught 107 | ``` 108 | std::promise虽然支持自定义异常,但它并不直接接受异常对象: 109 | ```c++ 110 | // std::promise::set_exception函数原型 111 | void set_exception(std::exception_ptr p); 112 | ``` 113 | 自定义异常可以通过位于头文件exception下的std::make_exception_ptr函数转化为std::exception_ptr。 114 | ### 1.4 std::promise\ 115 | 通过上面的例子,我们看到```std::promise```是合法的。此时std::promise.set_value不接受任何参数,仅用于通知关联的std::future.get()解除阻塞。 116 | 117 | ### 1.5 std::promise所在线程退出时 118 | std::async(异步运行)时,开发人员有时会对std::promise所在线程退出时间比较关注。std::promise支持定制线程退出时的行为: 119 | * std::promise.set_value_at_thread_exit 线程退出时,std::future收到通过该函数设置的值 120 | * std::promise.set_exception_at_thread_exit 线程退出时,std::future则抛出该函数指定的异常。 121 | 122 | 关于std::promise就是这些,本文从使用角度介绍了std::promise的能力以及边界,读者如果想更深入了解该类,可以直接阅读一下源码。 123 | -------------------------------------------------------------------------------- /05-async-2-packaged_task.MD: -------------------------------------------------------------------------------- 1 | # C++11多线程-异步运行之std::packaged_task 2 | 上一篇介绍的std::promise通过set_value可以使得与之关联的std::future获取数据。本篇介绍的std::packaged_task则更为强大,它允许传入一个函数,并将函数计算的结果传递给std::future,包括函数运行时产生的异常。下面我们就来详细介绍一下它。 3 | ## 2. std::package_task 4 | 在开始std::packaged_task之前我们先看一段代码,对std::packaged_task有个直观的印象,然后我们再进一步介绍。 5 | ```c++ 6 | #include // std::thread 7 | #include // std::packaged_task, std::future 8 | #include // std::cout 9 | 10 | int sum(int a, int b) { 11 | return a + b; 12 | } 13 | 14 | int main() { 15 | std::packaged_task task(sum); 16 | std::future future = task.get_future(); 17 | 18 | // std::promise一样,std::packaged_task支持move,但不支持拷贝 19 | // std::thread的第一个参数不止是函数,还可以是一个可调用对象,即支持operator()(Args...)操作 20 | std::thread t(std::move(task), 1, 2); 21 | // 等待异步计算结果 22 | std::cout << "1 + 2 => " << future.get() << std::endl; 23 | 24 | t.join(); 25 | return 0; 26 | } 27 | /// 输出: 1 + 2 => 3 28 | ``` 29 | std::packaged_task位于头文件```#include ```中,是一个模板类 30 | ```c++ 31 | template 32 | class packaged_task 33 | ``` 34 | 其中R是一个函数或可调用对象,ArgTypes是R的形参。与std::promise一样,std::packaged_task**支持move,但不支持拷贝(copy)**。std::packaged_task封装的函数的计算结果会通过与之联系的std::future::get获取(当然,可以在其它线程中异步获取)。关联的std::future可以通过std::packaged_task::get_future获取到,get_future仅能调用一次,多次调用会触发std::future_error异常。
35 | std::package_task除了可以通过可调用对象构造外,还支持缺省构造(无参构造)。但此时构造的对象不能直接使用,需通过右值赋值操作设置了可调用对象或函数后才可使用。判断一个std::packaged_task是否可使用,可通过其成员函数valid来判断。 36 | ### 2.1 std::packaged_task::valid 37 | 该函数用于判断std::packaged_task对象是否是有效状态。当通过缺省构造初始化时,由于其未设置任何可调用对象或函数,valid会返回false。只有当std::packaged_task设置了有效的函数或可调用对象,valid才返回true 38 | ```c++ 39 | #include // std::packaged_task, std::future 40 | #include // std::cout 41 | 42 | int main() { 43 | std::packaged_task task; // 缺省构造、默认构造 44 | std::cout << std::boolalpha << task.valid() << std::endl; // false 45 | 46 | std::packaged_task task2(std::move(task)); // 右值构造 47 | std::cout << std::boolalpha << task.valid() << std::endl; // false 48 | 49 | task = std::packaged_task([](){}); // 右值赋值, 可调用对象 50 | std::cout << std::boolalpha << task.valid() << std::endl; // true 51 | 52 | return 0; 53 | } 54 | ``` 55 | 上面的示例演示了几种valid为false的情况,程序输出如下 56 | ```console 57 | false 58 | false 59 | true 60 | ``` 61 | 62 | ### 2.2 std::packaged_task::operator()(ArgTypes...) 63 | 该函数会调用std::packaged_task对象所封装可调用对象R,但其函数原型与R稍有不同: 64 | ```c++ 65 | void operator()(ArgTypes... ); 66 | ``` 67 | operator()的返回值是void,即无返回值。因为std::packaged_task的设计主要是用来进行异步调用,因此R(ArgTypes...)的计算结果是通过std::future::get来获取的。该函数会忠实地将R的计算结果反馈给std::future,即使R抛出异常(此时std::future::get也会抛出同样的异常) 68 | ```c++ 69 | #include // std::packaged_task, std::future 70 | #include // std::cout 71 | 72 | int main() { 73 | std::packaged_task convert([](){ 74 | throw std::logic_error("will catch in future"); 75 | }); 76 | std::future future = convert.get_future(); 77 | 78 | convert(); // 异常不会在此处抛出 79 | 80 | try { 81 | future.get(); 82 | } catch(std::logic_error &e) { 83 | std::cerr << typeid(e).name() << ": " << e.what() << std::endl; 84 | } 85 | 86 | return 0; 87 | } 88 | /// Clang下输出: St11logic_error: will catch in future 89 | ``` 90 | 为了帮忙大家更好的了解该函数,下面将Clang下精简过的operator()(Args...)的实现贴出,以便于更好理解该函数的边界,明确什么可以做,什么不可以做。 91 | ```c++ 92 | template 93 | class packaged_task<_Rp(_ArgTypes...)> { 94 | __packaged_task_function<_Rp_(_ArgTypes...)> __f_; 95 | promise<_Rp> __p_; // 内部采用了promise实现 96 | 97 | public: 98 | // 构造、析构以及其它函数... 99 | 100 | void packaged_task<_Rp(_ArgTypes...)>::operator()(_ArgTypes... __args) { 101 | if (__p_.__state_ == nullptr) 102 | __throw_future_error(future_errc::no_state); 103 | if (__p_.__state_->__has_value()) // __f_不可重复调用 104 | __throw_future_error(future_errc::promise_already_satisfied); 105 | 106 | try { 107 | __p_.set_value(__f_(std::forward<_ArgTypes>(__args)...)); 108 | } catch (...) { 109 | __p_.set_exception(current_exception()); 110 | } 111 | } 112 | }; 113 | ``` 114 | ### 2.3 让std::packaged_task在线程退出时再将结果反馈给std::future 115 | std::packaged_task::make_ready_at_thread_exit函数接收的参数与operator()(_ArgTypes...)一样,行为也一样。只有一点差别,那就是不会将计算结果立刻反馈给std::future,而是在其执行时所在的线程结束后,std::future::get才会取得结果。 116 | ### 2.4 std::packaged_task::reset 117 | 与std::promise不一样, std::promise仅可以执行一次set_value或set_exception函数,但std::packagged_task可以执行多次,其奥秘就是reset函数 118 | ```c++ 119 | template 120 | void packaged_task<_Rp(_ArgTypes...)>::reset() 121 | { 122 | if (!valid()) 123 | __throw_future_error(future_errc::no_state); 124 | __p_ = promise(); 125 | } 126 | ``` 127 | 通过重新构造一个promise来达到多次调用的目的。显然调用reset后,需要重新get_future,以便获取下次operator()执行的结果。由于是重新构造了promise,因此reset操作并不会影响之前调用的make_ready_at_thread_exit结果,也即之前的定制的行为在线程退出时仍会发生。 128 | 129 | std::packaged_task就介绍到这里,下一篇将会完成本次异步运行的整体脉络,将std::async和std::future一起介绍结大家。 130 | 131 | ## 附: C++11多线程中的样例代码的编译及运行 132 | ```console 133 | g++ -std=c++11 134 | ./a.out 135 | ``` 136 | -------------------------------------------------------------------------------- /05-async-3-future.MD: -------------------------------------------------------------------------------- 1 | # C++11多线程-异步运行之最终篇(future+async) 2 | 前面两章多次使用到std::future,本章我们就来揭开std::future庐山真面目。最后我们会引出std::async,该函数使得我们的并发调用变得简单,优雅。 3 | ## 3. std::future 4 | 前面我们多次使用std::future的get方法来获取其它线程的结果,那么除这个方法外,std::future还有哪些方法呢 5 | ```c++ 6 | enum class future_status 7 | { 8 | ready, 9 | timeout, 10 | deferred 11 | }; 12 | template 13 | class future 14 | { 15 | public: 16 | // retrieving the value 17 | R get(); 18 | // functions to check state 19 | bool valid() const noexcept; 20 | 21 | void wait() const; 22 | template 23 | future_status wait_for(const chrono::duration& rel_time) const; 24 | template 25 | future_status wait_until(const chrono::time_point& abs_time) const; 26 | 27 | shared_future share() noexcept; 28 | }; 29 | ``` 30 | 以上代码去掉了std::future构造、析构、赋值相关的代码,这些约束我们之前都讲过了。下面我们来逐一了解上面这些函数。 31 | ### 3.1 get 32 | 这个函数我们之前一直使用,该函数会一直阻塞,直到获取到结果或异步任务抛出异常。 33 | ### 3.2 share 34 | std::future允许move,但是不允许拷贝。如果用户确有这种需求,需要同时持有多个实例,怎么办呢? 这就是share发挥作用的时候了。std::shared_future通过引用计数的方式实现了多实例[共享同一状态](#assoc_state),但有计数就伴随着同步开销(无锁的原子操作也是有开销的),性能会稍有下降。因此C++11要求程序员显式调用该函数,以表明用户对由此带来的开销负责。std::shared_future允许move,允许拷贝,并且具有和std::future同样的成员函数,此处就不一一介绍了。当调用share后,std::future对象就不再和任何[共享状态](#assoc_state)关联,其[valid](#valid)函数也会变为false。 35 | ### 3.3 wait 36 | 等待,直到数据就绪。数据就绪时,通过get函数,无等待即可获得数据。 37 | ### 3.4 wait_for和wait_until 38 | wait_for、wait_until主要是用来进行超时等待的。wait_for等待指定时长,wait_until则等待到指定的时间点。返回值有3种状态: 39 | 1. ready - 数据已就绪,可以通过get获取了 40 | 2. timeout - 超时,数据还未准备好 41 | 3. deferred - 这个和std::async相关,表明无需wait,异步函数将在get时执行 42 | 43 | ### 3.5 valid 44 | 判断当前std::future实例是否有效。std::future主要是用来获取异步任务结果的,作为消费方出现,单独构建出来的实例没意义,因此其valid为false。当与其它生产方(Provider)关联后,valid就会变得有效,std::future才会发挥实际的作用。C++11中有下面几种Provider,可这些Provider获得有效的std::future实例: 45 | 1. [std::async](#async) 46 | 2. std::promise::get_future 47 | 3. std::packaged_task::get_future 48 | Provider与std::future通过共享状态进行关联,从而实现与std::future的通信。既然std::future的各种行为都依赖共享状态,那么什么是共享状态呢? 49 | ## 4. 共享状态 50 | 共享状态其本质就是单生产者-单消费者的多线程并发模型。无论是std::promise还是std::packaged_task都是通过共享状态,实现与std::future通信的。还记得我们在std::condition_variable一节给出的chan类么。共享状态与其类似,通过std::mutex、std::condition_variable实现了多线程间通信。共享状态并非C++11的标准,只是对std::promise、std::future的实现手段。回想我们之前的使用场景,共享状态可能具有如下形式(c++11伪代码): 51 | ```c++ 52 | template 53 | class assoc_state { 54 | protected: 55 | mutable mutex mut_; 56 | mutable condition_variable cv_; 57 | unsigned state_ = 0; 58 | // std::shared_future中拷贝动作会发生引用计数的变化 59 | // 当引用计数降到0时,实例被delete 60 | int share_count_ = 0; 61 | exception_ptr exception_; // 执行异常 62 | T value_; // 执行结果 63 | 64 | public: 65 | enum { 66 | ready = 4, // 异步动作执行完,数据就绪 67 | // 异步动作将延迟到future.get()时调用 68 | // (实际上非异步,只不过是延迟执行) 69 | deferred = 8, 70 | }; 71 | 72 | assoc_state() {} 73 | // 禁止拷贝 74 | assoc_state(const assoc_state &) = delete; 75 | assoc_state &operator=(const assoc_state &) = delete; 76 | // 禁止move 77 | assoc_state(assoc_state &&) = delete; 78 | assoc_state &operator=(assoc_state &&) = delete; 79 | 80 | void set_value(const T &); 81 | void set_exception(exception_ptr p); 82 | // 需要用到线程局变存储 83 | void set_value_at_thread_exit(const T &); 84 | void set_exception_at_thread_exit(exception_ptr p); 85 | 86 | void wait(); 87 | future_status wait_for(const duration &) const; 88 | future_status wait_until(const time_point &) const; 89 | 90 | T &get() { 91 | unique_lock lock(this->mut_); 92 | // 当_state为deferred时,std::async中 93 | // 的函数将在sub_wait中调用 94 | this->sub_wait(lock); 95 | if (this->_exception != nullptr) 96 | rethrow_exception(this->_exception); 97 | return _value; 98 | } 99 | private: 100 | void sub_wait(unique_lock &lk) { 101 | if (state_ != ready) { 102 | if (state_ & static_cast(deferred)) { 103 | state_ &= ~static_cast(deferred); 104 | lk.unlock(); 105 | __execute(); // 此处执行实际的函数调用 106 | } else { 107 | cv_.wait(lk, [this](){return state == ready;}) 108 | } 109 | } 110 | } 111 | }; 112 | ``` 113 | 以上给出了get的实现(伪代码),其它部分虽然没实现,但assoc_state应该具有的功能,以及对std::promise、std::packaged_task、std::future、std::shared_future的支撑应该能够表达清楚了。未实现部分还请读者自行补充一下,权当是练手了。
114 | 有兴趣的读者可以阅读[llvm-libxx](https://github.com/llvm-mirror/libcxx)(https://github.com/llvm-mirror/libcxx) 的源码,以了解更多细节,对共享状态有更深掌握。 115 | ## 5. std::async 116 | std::async可以看作是对std::packaged_task的封装(虽然实际并一定如此,取决于编译器的实现,但共享状态的思想是不变的),有两种重载形式: 117 | ```c++ 118 | #define FR typename result_of::type(typename decay::type...)>::type 119 | 120 | // 不含执行策略 121 | template 122 | future async(F&& f, Args&&... args); 123 | // 含执行策略 124 | template 125 | future async(launch policy, F&& f, Args&&... args); 126 | ``` 127 | define部分是用来推断函数F的返回值类型,我们先忽略它,以后有机再讲。两个重载形式的差别是一个含执行策略,而另一个不含。那么什么是执行策略呢?执行策略定义了async执行F(函数或可调用求对象)的方式,是一个枚举值: 128 | ```c++ 129 | enum class launch { 130 | // 保证异步行为,F将在单独的线程中执行 131 | async = 1, 132 | // 当其它线程调用std::future::get时, 133 | // 将调用非异步形式, 即F在get函数内执行 134 | deferred = 2, 135 | // F的执行时机由std::async来决定 136 | any = async | deferred 137 | }; 138 | ``` 139 | 不含加载策略的版本,使用的是std::launch::any,也即由std::async函数自行决定F的执行策略。那么C++11如何确定std::any下的具体执行策略呢,一种可能的办法是:优先使用async策略,如果创建线程失败,则使用deferred策略。实际上这也是Clang的any实现方式。std::async的出现大大减轻了异步的工作量。使得一个异步调用可以像执行普通函数一样简单。 140 | ```c++ 141 | #include // std::cout, std::endl 142 | #include // std::async, std::future 143 | #include // seconds 144 | using namespace std::chrono; 145 | 146 | int main() { 147 | auto print = [](char c) { 148 | for (int i = 0; i < 10; i++) { 149 | std::cout << c; 150 | std::cout.flush(); 151 | std::this_thread::sleep_for(milliseconds(1)); 152 | } 153 | }; 154 | // 不同launch策略的效果 155 | std::launch policies[] = {std::launch::async, std::launch::deferred}; 156 | const char *names[] = {"async ", "deferred"}; 157 | for (int i = 0; i < sizeof(policies)/sizeof(policies[0]); i++) { 158 | std::cout << names[i] << ": "; 159 | std::cout.flush(); 160 | auto f1 = std::async(policies[i], print, '+'); 161 | auto f2 = std::async(policies[i], print, '-'); 162 | f1.get(); 163 | f2.get(); 164 | std::cout << std::endl; 165 | } 166 | 167 | return 0; 168 | } 169 | ``` 170 | 以上代码输出如下: 171 | ```commandline 172 | async : +-+-+-+--+-++-+--+-+ 173 | deferred: ++++++++++---------- 174 | ``` 175 | 进行到现在,C++11的async算是结束了,尽管还留了一些疑问,比如共享状态如何实现set_value_at_thread_exit效果。我们将会在下一章节介绍C++11的线程局部存储,顺便也解答下该疑问。 176 | -------------------------------------------------------------------------------- /06-thread_local.MD: -------------------------------------------------------------------------------- 1 | # C++11多线程-线程局部存储(thread_local) 2 | 线程局部存储在其它语言中都是以库的形式提供的(库函数或类)。但在C++11中以关键字的形式,做为一种存储类型出现,由此可见C++11对线程局部存储的重视。C++11中有如下几种存储类型: 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
序号类型备注
1auto(C++11)该关键字用于两种情况:
1. 声明变量时: 根据初始化表达式自动推断变量类型。
2. 声明函数作为函数返回值的占位符。
2staticstatic本质上是创建“全局变量”,该变量只初始化一次,除此之外它还会控制变量的可见性:
1. static修饰函数内的“局部”变量时,表明它不需要在进入或离开函数时创建或销毁。且仅在函数内可见。
2. static修饰全局变量时,表明该变量仅在当前(声明它的)文件内可见。
3. static修饰类的成员变量时,则该变量被该类的所有实例共享。
3register寄存器变量。该变量存储在CPU寄存器中,而不是RAM(栈或堆)中。该变量的最大尺寸等于寄存器的大小。由于是存储于寄存器中,因此不能对该变量进行取地址操作。
4extern引用一个全局变量。当在一个文件中定义了一个全局变量时,就可以在其它文件中使用extern来声明并引用该变量。
5mutable仅适用于类成员变量。以mutable修饰的成员变量可以在const成员函数中修改。参见上一章chan.simple.h中对mutex的使用。
6thread_local(C++11)线程周期
12 | thread_local修饰的变量具有线程周期。变量在线程创建时生成(不同编译器实现略有差异,但在线程内变量第一次使用前必然已构造完毕),线程结束时被销毁(析构)。每个线程都拥有其自己的变量副本。thread_local可以和static或extern联合使用,这将会影响变量的链接属性。 13 | 下面代码演示了thread_local变量在线程中的生命周期 14 | 15 | ```c++ 16 | // thread_local.cpp 17 | #include 18 | #include 19 | 20 | class A { 21 | public: 22 | A() { 23 | std::cout << std::this_thread::get_id() 24 | << " " << __FUNCTION__ 25 | << "(" << (void *)this << ")" 26 | << std::endl; 27 | } 28 | ~A() { 29 | std::cout << std::this_thread::get_id() 30 | << " " << __FUNCTION__ 31 | << "(" << (void *)this << ")" 32 | << std::endl; 33 | } 34 | 35 | // 线程中,第一次使用前初始化 36 | void doSth() { 37 | } 38 | }; 39 | 40 | thread_local A a; 41 | int main() { 42 | a.doSth(); 43 | 44 | std::thread t([]() { 45 | std::cout << "Thread: " 46 | << std::this_thread::get_id() 47 | << " entered" << std::endl; 48 | a.doSth(); 49 | }); 50 | 51 | t.join(); 52 | 53 | return 0; 54 | } 55 | ``` 56 | 运行该程序 57 | 58 | ```commandline 59 | $> g++ -std=c++11 -o debug/tls.out ./thread_local.cpp 60 | $> ./debug/tls.out 61 | 01 A(0xc00720) 62 | Thread: 02 entered 63 | 02 A(0xc02ee0) 64 | 02 ~A(0xc02ee0) 65 | 01 ~A(0xc00720) 66 | $> 67 | ``` 68 | a在main线程和t线程中分别保留了一份副本,以下时序图表明了两份副本的生命周期。
69 | ![](./images/thread_local.png) 70 | 71 | ## 附:std::future中共享状态set_value_at_thread_exit函数的实现 72 | 有了thread_local,则共享状态的set_value_at_thread_exit函数实现起来就容易多了: 只要将异步结果推迟到线程退出时,再调用assoc_state::set_value时就可以了,下面是其伪代码: 73 | 74 | ```c++ 75 | class assoc_state_tls { 76 | // 线程析构时要执行的函数列表 77 | std::list> funcs_; 78 | public: 79 | ~assoc_state_tls() { 80 | for (auto f: funcs_) { 81 | f(); 82 | } 83 | } 84 | 85 | void add(std::function f) { 86 | funcs_.push_back(f); 87 | } 88 | }; 89 | thread_local assoc_state_tls assoc_state_tls_; 90 | 91 | // 实现set_value_at_thread_exit 92 | template 93 | void assoc_state::set_value_at_thread_exit(const T &v) { 94 | // 当前所在线程结束时,再调用set_value 95 | assoc_state_tls_.add([this](){ 96 | this->set_value(v); 97 | }); 98 | } 99 | ``` 100 | -------------------------------------------------------------------------------- /07-atomic-1.MD: -------------------------------------------------------------------------------- 1 | # C++11多线程-原子操作(1) 2 | 前面我们讲了C++11下的多线程及相关操作,这些操作在绝大多数情况下应该够用了。但在某些极端场合,如需要高性能的情况下,我们还需要一些更高效的同步手段。本节介绍的原子操作是一种lock free的操作,不需要同步锁,具有很高的性能。在化学中原子不是可分割的最小单位,引申到编程中,原子操作是不可打断的最低粒度操作,是线程安全的。**C++11中原子类提供的成员函数都是原子的,是线程安全的。** 3 | 原子操作中最简单的莫过于atomic_flag,只有两种操作:test and set、clear。我们的原子操作就从这种类型开始。 4 | ## 1. std::atomic_flag 5 | C++11中所有的原子类都是**不允许拷贝、不允许Move**的,atomic_flag也不例外。atomic_flag顾名思议,提供了标志的管理,标志有三种状态:clear、set和未初始化状态。 6 | ### 1.1 atomic_flag实例化 7 | 缺省情况下atomic_flag处于未初始化状态。除非初始化时使用了`ATOMIC_FLAG_INIT`宏,则此时atomic_flag处于clear状态。 8 | ### 1.2 std::atomic_flag::clear 9 | 调用该函数将会把atomic_flag置为clear状态。clear状态您可以理解为bool类型的false,set状态可理解为true状态。clear函数没有任何返回值: 10 | ```c++ 11 | void clear(memory_order m = memory_order_seq_cst) volatile noexcept; 12 | void clear(memory_order m = memory_order_seq_cst) noexcept; 13 | ``` 14 | 对于memory_order我们会在后面的章节中详细介绍它,现在先列出其取值及简单释义 15 | 16 | |序号|值|意义 17 | |:--:|:--:|:--- 18 | |1|memory_order_relaxed|宽松模型,不对执行顺序做保证 19 | |2|memory_order_consume|当前线程中,满足happens-before原则。
当前线程中该原子的所有后续操作,必须在本条操作完成之后执行 20 | |3|memory_order_acquire|当前线程中,**读**操作满足happens-before原则。
所有后续的**读**操作必须在本操作完成后执行 21 | |4|memory_order_release|当前线程中,**写**操作满足happens-before原则。
所有后续的**写**操作必须在本操作完成后执行 22 | |5|memory_order_acq_rel|当前线程中,同时满足memory_order_acquire和memory_order_release 23 | |6|memory_order_seq_cst|最强约束。全部读写都按顺序执行 24 | 25 | ### 1.3 test_and_set 26 | 该函数会检测flag是否处于set状态,如果不是,则将其设置为set状态,并返回false;否则返回true。![](https://upload-images.jianshu.io/upload_images/6687014-40e5b28ef720dad9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 27 | 28 | [test_and_set](https://en.wikipedia.org/wiki/Test-and-set)是典型的*read-modify-write(RMW)*模型,保证多线程环境下只被设置一次。下面代码通过10个线程,模拟了一个计数程序,第一个完成计数的会打印"win"。 29 | ```c++ 30 | #include // atomic_flag 31 | #include // std::cout, std::endl 32 | #include // std::list 33 | #include // std::thread 34 | 35 | void race(std::atomic_flag &af, int id, int n) { 36 | for (int i = 0; i < n; i++) { 37 | } 38 | // 第一个完成计数的打印:Win 39 | if (!af.test_and_set()) { 40 | printf("%s[%d] win!!!\n", __FUNCTION__, id); 41 | } 42 | } 43 | 44 | int main() { 45 | std::atomic_flag af = ATOMIC_FLAG_INIT; 46 | 47 | std::list lstThread; 48 | for (int i = 0; i < 10; i++) { 49 | lstThread.emplace_back(race, std::ref(af), i + 1, 5000 * 10000); 50 | } 51 | 52 | for (std::thread &thr : lstThread) { 53 | thr.join(); 54 | } 55 | 56 | return 0; 57 | } 58 | ``` 59 | 程序输出如下(每次运行,可能率先完成的thread不同): 60 | ```console 61 | race[7] win!!! 62 | ``` 63 | 64 | [](https://www.cnblogs.com/zifeiye/p/8194949.html) 65 | -------------------------------------------------------------------------------- /07-atomic-2.MD: -------------------------------------------------------------------------------- 1 | # C++11多线程-原子操作(2) 2 | 上一篇我们介绍了原子操作中最简单的std::atomic_flag,今天我们看一下std::atomic\类。 3 | ## 2. std::atomic\ 4 | std::atomic是一个模板类,它定义了一些atomic应该具有的通用操作,我们一起来看一下: 5 | ### 2.1 is_lock_free 6 | ```c++ 7 | bool is_lock_free() const noexcept; 8 | bool is_lock_free() const volatile noexcept; 9 | ``` 10 | atomic是否无锁操作。如果是,则在多个线程访问该对象时不会导致线程阻塞(可能使用某种事务内存transactional memory方法实现lock-free的特性)。 11 | 事实上该函数可以做为一个静态函数。所有指定相同类型T的atomic实例的is_lock_free函数都会返回相同值。 12 | ### 2.2 store 13 | ```c++ 14 | void store(T desr, memory_order m = memory_order_seq_cst) noexcept; 15 | void store(T desr, memory_order m = memory_order_seq_cst) volatile noexcept; 16 | T operator=(T d) noexcept; 17 | T operator=(T d) volatile noexcept; 18 | ``` 19 | 赋值操作。operator=实际上内部调用了store,并返回d。 20 | ```c++ 21 | T operator=(T d) volatile noexpect { 22 | store(d); 23 | return d; 24 | } 25 | ``` 26 | **注**:有些编译器,在实现store时限定m只能取以下三个值:memory_order_consume,memory_order_acquire,memory_order_acq_rel。 27 | ### 2.3 load 28 | ```c++ 29 | T load(memory_order m = memory_order_seq_cst) const volatile noexcept; 30 | T load(memory_order m = memory_order_seq_cst) const noexcept; 31 | operator T() const volatile noexcept; 32 | operator T() const noexcept; 33 | ``` 34 | 读取,加载并返回变量的值。operator T是load的简化版,内部调用的是load(memory_order_seq_cst)形式。 35 | ### 2.4 exchange 36 | ```c++ 37 | T exchange(T desr, memory_order m = memory_order_seq_cst) volatile noexcept; 38 | T exchange(T desr, memory_order m = memory_order_seq_cst) noexcept; 39 | ``` 40 | 交换,赋值后返回变量赋值前的值。exchange也称为read-modify-write操作。 41 | ### 2.5 compare_exchange_weak 42 | ```c++ 43 | bool compare_exchange_weak(T& expect, T desr, memory_order s, memory_order f) volatile noexcept; 44 | bool compare_exchange_weak(T& expect, T desr, memory_order s, memory_order f) noexcept; 45 | bool compare_exchange_weak(T& expect, T desr, memory_order m = memory_order_seq_cst) volatile noexcept; 46 | bool compare_exchange_weak(T& expect, T desr, memory_order m = memory_order_seq_cst) noexcept; 47 | ``` 48 | 这就是有名的[CAS(Compare And Swap: 比较并交换)](https://en.wikipedia.org/wiki/Compare-and-swap)。但C++11针对该操作提供了更多的细节,其操作流程如下:![](./images/compare_exchange.png) 49 | 50 | 以上只是个示意图,compare_exchange_weak操作是原子的,排它的。其它线程如果想要读取或修改该原子对象时,会等待先该操作完成。 51 | 该函数**直接比较原子对象所封装的值与expect的物理内容**,在某些情况下,对象的比较操作在使用 operator==() 判断时相等,但 compare_exchange_weak 判断时却可能失败,因为对象底层的物理内容中可能存在位对齐或其他逻辑表示相同但是物理表示不同的值(比如 true 和 5,它们在逻辑上都表示"真",但在物理上两者的表示并不相同)。 52 | 与strong版本不同,weak版允许返回**伪false**,即使原子对象所封装的值与expect的物理内容相同,也仍然返回false。但它在某些平台下会取得更好的性能,在某些循环算法中这种行为也是可接受的。对于非循环算法建议使用compare_exchange_strong。 53 | ### 2.6 compare_exchange_strong 54 | ```c++ 55 | bool compare_exchange_strong(T& expect, T desr, memory_order s, memory_order f) volatile noexcept; 56 | bool compare_exchange_strong(T& expect, T desr, memory_order s, memory_order f) noexcept; 57 | bool compare_exchange_strong(T& expect, T desr, memory_order m = memory_order_seq_cst) volatile noexcept; 58 | bool compare_exchange_strong(T& expc, T desr, memory_order m = memory_order_seq_cst) noexcept; 59 | ``` 60 | compare_exchange的strong版本,进行compare时,与weak版一样,都是比较的物理内容。与weak版不同的是,strong版本不会返回伪false。即:原子对象所封装的值如果与expect在物理内容上相同,strong版本一定会返回true。其所付出的代价是:在某些需要循环检测的算法,或某些平台下,其性能较compare_exchange_weak要差。但对于某些不需要采用循环检测的算法而言, 通常采用compare_exchange_strong 更好。 61 | 62 | ## 3. std::atomic特化 63 | 我知道计算擅长处理整数以及指针,并且X86架构的CPU还提供了指令级的CAS操作。C++11为了充分发挥计算的特长,针对数值(std::atmoic\)及指针(std::atomic\)进行了特化,以提高原子操作的性能。特化后的atomic在通用操作的基础上,还提供了更丰富的功能。 64 | ### 3.1 fetch_add 65 | ```c++ 66 | // T is integral 67 | T fetch_add(T v, memory_order m = memory_order_seq_cst) volatile noexcept; 68 | T fetch_add(T v, memory_order m = memory_order_seq_cst) noexcept; 69 | // T is pointer 70 | T fetch_add(ptrdiff_t v, memory_order m = memory_order_seq_cst) volatile noexcept; 71 | T fetch_add(ptrdiff_t v, memory_order m = memory_order_seq_cst) noexcept; 72 | ``` 73 | 该函数将原子对象封装的值加上v,同时返回原子对象的旧值。其功能用伪代码表示为: 74 | ```c++ 75 | auto old = contained 76 | contained += v 77 | return old 78 | ``` 79 | 其中contained为原子对象封装值,本文后面均使用contained代表该值。**注:** 以上是为了便于理解的伪代码,实际实现是原子的不可拆分的。 80 | ### 3.2 fetch_sub 81 | ```c++ 82 | // T is integral 83 | T fetch_sub(T v, memory_order m = memory_order_seq_cst) volatile noexcept; 84 | T fetch_sub(T v, memory_order m = memory_order_seq_cst) noexcept; 85 | // T is pointer 86 | T fetch_sub(ptrdiff_t v, memory_order m = memory_order_seq_cst) volatile noexcept; 87 | T fetch_sub(ptrdiff_t v, memory_order m = memory_order_seq_cst) noexcept; 88 | ``` 89 | 该函数将原子对象封装的值减去v,同时返回原子对象的旧值。其功能用伪代码表示为: 90 | ```c++ 91 | auto old = contained 92 | contained -= v 93 | return old 94 | ``` 95 | ### 3.3 ++, --, +=, -= 96 | 不管是基于整数的特化,还是指针特化,atomic均支持这四种操作。其用法与未封装时一样,此处就不一一列举其函数原型了。 97 | ## 4 独属于数值型特化的原子操作 - 位操作 98 | ### 4.1 fetch_and,fetch_or,fetch_xor 99 | 位操作,将contained按指定方式进行位操作,并返回contained的旧值。 100 | ```c++ 101 | integral fetch_and(integral v, memory_order m = memory_order_seq_cst) volatile noexcept; 102 | integral fetch_and(integral v, memory_order m = memory_order_seq_cst) noexcept; 103 | integral fetch_or(integral v, memory_order m = memory_order_seq_cst) volatile noexcept; 104 | integral fetch_or(integral v, memory_order m = memory_order_seq_cst) noexcept; 105 | integral fetch_xor(integral v, memory_order m = memory_order_seq_cst) volatile noexcept; 106 | integral fetch_xor(integral v, memory_order m = memory_order_seq_cst) noexcept; 107 | ``` 108 | 以xor为例,其操作相当于 109 | ```c++ 110 | auto old = contained 111 | contained ^= v 112 | return old 113 | ``` 114 | ### 4.2 operator &=,operator |=,operator ^= 115 | 与相应的fetch_*操作不同的是,operator操作返回的是新值: 116 | ```c++ 117 | T operator &=(T v) volatile noexcept {return fetch_and(v) & v;} 118 | T operator &=(T v) noexcept {return fetch_and(v) & v;} 119 | T operator |=(T v) volatile noexcept {return fetch_or(v) | v;} 120 | T operator |=(T v) noexcept {return fetch_or(v) | v;} 121 | T operator ^=(T v) volatile noexcept {return fetch_xor(v) ^ v;} 122 | T operator ^=(T v) noexcept {return fetch_xor(v) ^ v;} 123 | ``` 124 | ## 5. std::atomic的限制: trivial copyable 125 | 上面我们提到std::atomic提供了通用操作,其实这些操作可以应用到所有**trivially copyable**的类型。从字面意义理解,一个类型如果是拷贝不变的(**trivially copyable**),则使用memcpy这种方式把它的数据从一个地方拷贝出来会得到相同的结果。编译器如何判断一个类型是否**trivially copyable**呢?C++标准把trivial类型定义如下,一个拷贝不变(**trivially copyable**)类型是指: 126 | 1. 没有non-trivial 的拷贝构造函数 127 | 2. 没有non-trivial的move构造函数 128 | 3. 没有non-trivial的赋值操作符 129 | 4. 没有non-trivial的move赋值操作符 130 | 5. 有一个trivial的析构函数 131 | 132 | 一个trivial class类型是指有一个trivial类型的默认构造函数,而且是拷贝不变的(**trivially copyable**)的class。**特别注意,拷贝不变类型和trivial类型都不能有虚机制**。那么trivial和non-trivial类型到底是什么呢?这里给出一个非官方、不严谨的判断方式,方便大家对**trivially copyable**有一个直观的认识。一个**trivial copyable**类在四个点上没有自定义动作,也没有编译器加入的额外动作(如虚指针初始化就属额外动作),这四个点是: 133 | * 缺省构造。*类必须支持缺省构造*,同时类的非静态成员也不能有自定义或编译器加入的额外动作,否则编译器势必会隐式插入额外动作来初始化非静态成员。 134 | * 拷贝构造、拷贝赋值 135 | * move构造、move赋值 136 | * 析构 137 | 138 | 为了加深理解,我们来看一下下面的例子(所有的类都是trivial的): 139 | ```c++ 140 | // 空类 141 | struct A1 {}; 142 | 143 | // 成员变量是trivial的 144 | struct A2 { 145 | int x; 146 | }; 147 | 148 | // 基类是trivial的 149 | struct A3 : A2 { 150 | // 非用户自定义的构造函数(使用编译器提供的default构造) 151 | A3() = default; 152 | int y; 153 | }; 154 | 155 | struct A4 { 156 | int a; 157 | private: // 对防问限定符没有要求,A4仍然是trivial的 158 | int b; 159 | }; 160 | 161 | struct A5 { 162 | A1 a; 163 | A2 b; 164 | A3 c; 165 | A4 d; 166 | }; 167 | 168 | struct A6 { 169 | A2 a[16]; 170 | }; 171 | 172 | struct A7 { 173 | A6 c; 174 | void f(); // 普通成员函数是允许的 175 | }; 176 | 177 | struct A8 { 178 | int x; 179 | // 对静态成员无要求(std::string是non-trivial的) 180 | static std::string y; 181 | }; 182 | 183 | struct A9 { 184 | // 非用户自定义 185 | A9() = default; 186 | // 普通构造函数是可以的(前提是我们已经有了非定义的缺省构造函数) 187 | A9(int x) : x(x) {}; 188 | int x; 189 | }; 190 | ``` 191 | 而下面这些类型都是non-trivial的 192 | ```c++ 193 | struct B { 194 | // 有虚函数(编译器会隐式生成缺省构造,同时会初始化虚函数指针) 195 | virtual f(); 196 | }; 197 | 198 | struct B2 { 199 | // 用户自定义缺省构造函数 200 | B2() : z(42) {} 201 | int z; 202 | }; 203 | 204 | struct B3 { 205 | B3(); 206 | int w; 207 | }; 208 | // 虽然使用了default,但在缺省构造声明处未指定,因此被判断为non-trivial的 209 | NonTrivial3::NonTrivial3() = default; 210 | 211 | struct B4 { 212 | // 虚析构是non-trivial的 213 | virtual ~B4(); 214 | }; 215 | ``` 216 | STL在其头文件中定义了对**trivially copyable**类型的检测: 217 | ```c++ 218 | template 219 | struct std::is_trivially_copyable; 220 | ``` 221 | 判断类A是否trivially_copyable:```std::is_trivially_copyable::value```,该值是一个const bool类型,如果为true则是**trivially_copyable**的,否则不是。 222 | -------------------------------------------------------------------------------- /08-memory_order.MD: -------------------------------------------------------------------------------- 1 | # C++11多线程-内存模型 2 | 我们在前面讲atomic时,每一个原子操作都有一个std::memory_order参数。这个参数就是C++11的内存模型,用于确定该原子操作以什么样的方式进行读取。在atomic_flag中我们简单的对std::memory_order模型做了解释,本节我们再来深入了解一下。 3 | # 一、内存模型分类 4 | 一般来说,内存模型可分为静态内存模型和动态内存模型。 5 | 1. **静态内存模型**主要是类(或结构)对象在内存中的布局。也就是类(或结构)成员在内存中是如何存放的。类(或结构)对象的内存布局请参考[Stanley B.Lippman](https://en.wikipedia.org/wiki/Stanley_B._Lippman)的《深度探索C++对象模型》。 6 | 2. **动态内存模型**是从行为方面来看,多个线程对同一个对象同时读写时所做的约束,该模型理解起来要复杂一些,涉及了内存、Cache、CPU各个层次的交互,尤其是在多核系统中为了保证多线程环境下执行的正确性,需要对读写事件加以严格限制。std::memory_order就是这用来做这事的,它实际上是程序员、编译器以及CPU之间的契约,遵守契约后大家各自优化,从而可能提高程序性能。 7 | # 二、为什么需要内存模型 8 | 理解动态内存模型需要对现代处理器架构有一定的了解,因为它表示机器指令是以什么样的顺序被处理器执行的 (现代的处理器不是逐条处理机器指令的)。 9 | 10 | ![CPU架构](./images/cpu_arch.png) 11 | 12 | 上图是一个典型的多核CPU系统架构,具有双CPU核,每个核有一个私有的64K的一级缓存,两核共享4MB的二级缓存以及8G内存。该架构下数据并不是CPU<-->RAM直接读写,而是要经过L1和L2。写时CPU写入L1 Cache中,再从L1存入RAM中。读时也是,先从L1中读,读不到再从RAM中读。上图中离CPU越近的Cache读写性能越高,这可以有效提高数据的存取效率,但在一些特殊情况下会导致程序出错,看下面的例子(初值:x=y=0): 13 | 14 | |线程1|线程2 15 | |---|--- 16 | |x = 1;
r1 = y;|y=2;
r2 = x; 17 | 18 | 在编译器、CPU不对指令进行重排,且两个线程交织执行(假设以上四条语句都是原子操作)时共有4!/(2!*2!)=6种情况: 19 | 20 | ![](./images/thread_seq.png) 21 | 22 | r1和r2的最终结果共有3种,分别是: 23 | * r1==0,r2==1: 情况1 24 | * r1==2,r2==0: 情况2 25 | * r1==2,r2==1: 情况3、4、5、6 26 | 27 | 表面上看,r1== r2 == 0的情况不可能出现。但是当四条语句不是原子操作时。有一种可能是Core1的指令预处理单元看到线程1的两条语句没有依赖性(不管哪条语句先执行,在两条指令语句完成后都会得到一样的结果),会先执行r1=y再执行x=1,或者两条指令同时执行,这就是CPU的多发射和乱序执行。Core2也一样。这样一来就有可能出来r1==r2==0的结果。 28 | 29 | ![](./images/cpu_inorder.png) 30 | 31 | 另一种可能是缓存不一致。当Core1和Core2都将x,y更新到L1 Cache中,而还未来得及更新到RAM时,两个线程都已经执行完了第二条语句,此时r1==r2==0。 32 | 33 | ![](./images/cache_inconformity.png) 34 | 35 | 从编译器层面也一样,为了获取更高的性能,也可能会对语句进行执行顺序上的优化(类似CPU乱序)。 36 | 37 | 在编译器优化+CPU乱序+缓存不一致的多重组合下,情况不止以上三种。但不管哪一种,都不是我们希望看到的。那么如何避免呢?最简单的,也是首选的,方案当然是std::mutex。大牛陈硕曾说过: 38 | > 一个使用普通mutex的多线程程序,如果写错了,一般程序员,比如我,很容易分析出错误在哪——无非是漏了加锁,或者加锁次序错乱——并加以改正。如果用原子操作,除了最简单的 atomic counter,如果写错了,你能通过读代码找出错误吗?反正我是不能。换言之,你如何证明这一段代码是正确的?reasoning 难多了。 39 | 40 | 但是当程序对代码执行效率要求很高,std::mutex不满足时,就需要std::atomic上场了。 41 | # 三、C++11的内存模型 42 | 在正式介绍memory_order之前,我们先来看两个概念:synchronized-with和happends-before。 43 | * 行为:**synchronized-with**。 44 | 这是std::atomic生效的前提之一。假设X是一个atomic变量。如果线程A写了变量X, 线程B读了变量X,那么我们就说线程A、B间存在synchronized-with关系。C++11默认的原子操作(memory_order_seq_cst)就是synchronized-with的,保证了对X的读和写是互斥的,不会同时发生。 45 | 46 | ![具有synchronized-with特性的变量保证了操作的互斥性](./images/synchronized_with.png) 47 | 48 | * 结果:**happens-before**。 49 | happens-before指明了后发生的动作会看到先发生的动作的结果。还是上图,当线程B读取X时,读到的一定是写入后的X值,而不会是其它情况。happends-before具有*传递性*。如果A happens-before B,B happens-before C,那么A happends-before C。 50 | 51 | 概念介绍完了,下面来看看C++11为std::atomic提供的memory order: 52 | ```c++ 53 | enum class memory_order { 54 | memory_order_relaxed, 55 | memory_order_consume, // since C++20, load-consume 56 | memory_order_acquire, // load-acquire 57 | memory_order_release, // store-release 58 | memory_order_acq_rel, // store-release load-acquire 59 | memory_order_seq_cst // store-release load-acquire 60 | }; 61 | ``` 62 | 虽然枚举定义了6个,但它们表示的是4种内存模型: 63 | 64 | |序号|内存模型|memory_order值|备注 65 | |:-:|:-:|----|-- 66 | |1|宽松|memory_order_relaxed| 67 | |2|释放-获取|memory_order_acquire
memory_order_release
memory_order_acq_rel| 68 | |3|释放-消费|memory_order_consume|C++20起 69 | |4|顺序一致|memory_order_seq_cst|默认内存序 70 | 71 | 这些不同的内存序模型在不同的CPU架构下会有不同的代价。这允许专家通过采用更合理的内存序获得更大的性能升;同时允许在对性能要求不是那么严格的环境中采用默认的内存序,使得程序更容易理解。 72 | 73 | ## 3.1 宽松次序(relaxed ordering) 74 | 在原子变量上采用relaxed ordering的操作不参与synchronized-with关系,无同步操作,它们不会在内存并发访问时强加顺序。Relaxed order只是保证了原子性和修改顺序的一致性。在同一线程内对**同一原子变量**的操作不可以被重排,仍保持happens-before关系,但这与别的线程无关。尽管如此,relaxed ordering操作仍然是原子的,其值不会因为多线程而被破坏。先看看一个简单的例子: 75 | ```c++ 76 | std::atomic x{0}, y{0}; 77 | 78 | void thread_1() { 79 | auto r1 = y.load(std::memory_order_relaxed); // A 80 | x.store(r1, std::memory_order_relaxed); // B 81 | } 82 | void thread_2() { 83 | auto r2 = x.load(std::memory_order_relaxed); // C 84 | y.store(42, std::memory_order_relaxed); // D 85 | } 86 | ``` 87 | 执行完上面的程序,可能出现r1 == r2 == 42。理解这一点并不难,因为编译器允许调整C和D的执行顺序。如果程序的执行顺序是 D -> A -> B -> C,那么就会出现r1 == r2 == 42。 88 | 89 | Relaxed ordering适用于**只要求原子操作,不需要其它同步保障**的情况。该操作典型的应用场景是程序计数器: 90 | ```c++ 91 | #include 92 | #include 93 | #include 94 | 95 | std::atomic cnt{0}; 96 | void f() 97 | { 98 | for (int n = 0; n < 1000; ++n) { 99 | cnt.fetch_add(1, std::memory_order_relaxed); 100 | } 101 | } 102 | int main() 103 | { 104 | std::thread threads[10]; 105 | for (std::thread &thr: threads) { 106 | thr = std::thread(f); 107 | } 108 | for (auto &thr : v) { 109 | thr.join(); 110 | } 111 | assert(cnt == 10000); // 永远不会失败 112 | return 0; 113 | } 114 | ``` 115 | ## 3.2 释放-获取次序(release-acquire ordering) 116 | Release-acquire中没有全序关系,但它提供了一些同步方法。在这种序列模型下,原子操作对应的内存序为: 117 | 118 | |序号|原子操作|对应的内存操作|memory_order枚举值 119 | |:--:|:--:|---|---- 120 | |1|load|acquire|memory_order_acquire 121 | |2|store|release|memory_order_release 122 | |3|fetch_add
exchange|acquire
或 release
或 两者都是|memory_order_acquire
memory_order_release
memory_order_acq_rel 123 | |...|...|...|... 124 | 125 | Release-acquire中同步是成对出现的,仅建立在释放和获取同一原子对象的线程之间。其它线程有可能看到不一样的内存访问顺序。在我们常用的x86系统(强顺序系统)上,释放-获取顺序对于多数操作是自动进行的,无需为此同步模式添加额外的CPU指令。但在弱顺序系统(如ARM)上,必须使用特别的CPU加载或内存栅栏指令。 126 | 127 | Release-acquire有一个特点:线程A中所有发生在release x之前的写操作(包括非原子或宽松原子),对在线程B acquire x之后都可见。本来A、B间读写操作顺序不定。这么一同步,在x这个点前后,A、B线程之间有了个顺序关系,称作inter-thread happens-before。 128 | 129 | 一个释放-获取同步的例子是std::mutex:线程A释放锁而线程B获得它时,发生于线程A环境的临界区(释放之前)中的所有内存写入操作,对于线程B(获得之后)均可见。下面我们来看一个释放-获取的例子: 130 | ```c++ 131 | #include 132 | #include 133 | #include 134 | #include 135 | 136 | std::atomic ptr; 137 | int data; 138 | 139 | void producer() 140 | { 141 | std::string* p = new std::string("Hello"); 142 | data = 42; 143 | // 这句要放在最后,目的是为了在consumer中看到data的副带效应 144 | ptr.store(p, std::memory_order_release); 145 | } 146 | 147 | void consumer() 148 | { 149 | std::string* p2; 150 | while (!(p2 = ptr.load(std::memory_order_acquire))) { 151 | ; 152 | } 153 | // 下面的两个断言永远为真 154 | assert(*p2 == "Hello"); 155 | // producer中执行store之前的操作,在这里也可以看到 156 | assert(data == 42); 157 | } 158 | 159 | int main() 160 | { 161 | std::thread t1(producer); 162 | std::thread t2(consumer); 163 | t1.join(); 164 | t2.join(); 165 | } 166 | ``` 167 | 根据happens-before,可以知道释放-获取次序是可以传递的,在更多线程下仍然有效。 168 | ## 3.3 释放-消费次序(release-consume ordering) 169 | 释放-消费顺序的规范正在修订中,C++标准暂不鼓励使用memory_order_consume。此处不过多介绍。 170 | ## 3.4 顺序一致性次序(sequential-consisten ordering) 171 | 顺序一致性原子操作可以看作是释放-获取操作的加强版,它与释放-获取顺序相同的方式排序内存(在一个线程中先发生于存储的任何结果都变成进行加载的线程中的可见副效应)的同时,还对所有内存操作建立单独全序。 172 | 173 | 换个说法。Release-acquire只针对一个变量x的原子操作进行同步,而Sequential-consisten则是对所有使用memory_order_seq_cst的原子操作进行同步。这么一来所有的使用memory_order_seq_cst的原子操作就跟由一个线程顺序执行似的。 174 | 175 | 顺序一致性次序是std::atomic的默认内存序,它意味着将程序看做是一个简单的序列。如果对于一个原子变量的所有操作都是顺序一致的,那么多线程程序的行为就像是这些操作都以一种特定顺序被单线程程序执行。从同步的角度来看,一个顺序一致的store操作会与load操作同步。顺序模型还保证了在load之后执行的顺序一致原子操作都得表现得在store之后完成。 176 | 177 | 顺序一致性模型在所有多核系统上要求完全的内存栅栏CPU指令。这可能成为性能瓶颈,因为它强制受影响的内存访问传播到每个核心。 178 | 179 | 此示例演示顺序一致次序为必要的场合。任何其他次序都可能触发assert,因为可能令线程c和d观测到原子对象x和y以相反顺序更改。 180 | ```c++ 181 | #include 182 | #include 183 | #include 184 | 185 | std::atomic x = {false}; 186 | std::atomic y = {false}; 187 | std::atomic z = {0}; 188 | 189 | void write_x() 190 | { 191 | x.store(true, std::memory_order_seq_cst); 192 | } 193 | 194 | void write_y() 195 | { 196 | y.store(true, std::memory_order_seq_cst); 197 | } 198 | 199 | void read_x_then_y() 200 | { 201 | while (!x.load(std::memory_order_seq_cst)) 202 | ; 203 | if (y.load(std::memory_order_seq_cst)) { 204 | ++z; 205 | } 206 | } 207 | 208 | void read_y_then_x() 209 | { 210 | while (!y.load(std::memory_order_seq_cst)) 211 | ; 212 | if (x.load(std::memory_order_seq_cst)) { 213 | ++z; 214 | } 215 | } 216 | 217 | int main() 218 | { 219 | std::thread a(write_x); 220 | std::thread b(write_y); 221 | std::thread c(read_x_then_y); 222 | std::thread d(read_y_then_x); 223 | a.join(); b.join(); c.join(); d.join(); 224 | // 如果不使用顺序一致模型的话,则此处就可能触发断言 225 | assert(z.load() != 0); 226 | } 227 | ``` 228 | 229 | # 4. 写在最后的话 230 | 通过本文,想必对C++的memory order有了一定程度的了解。需要再次强调的是,当程序对性能没有特殊要求时,首选std::mutex,其次使用memory_order_seq_cst。只有当对cpu架构了解较深,且对性能要求苛刻的场合下才考虑使用其它内存序。 231 | -------------------------------------------------------------------------------- /09-generic_function.MD: -------------------------------------------------------------------------------- 1 | # 函数模板 2 | 3 | # 一、为什么要有函数模板 4 | 在泛型编程出现前,我们要实现一个swap函数得这样写: 5 | ```c++ 6 | void swap(int &a, int &b) { 7 | int tmp{a}; 8 | a = b; 9 | b = tmp; 10 | } 11 | ``` 12 | 但这个函数只支持int型的变量交换,如果我们要做float, long, double, std::string等等类型的交换时,只能不断加入新的重载函数。这样做不但代码冗余,容易出错,还不易维护。C++函数模板有效解决了这个问题。函数模板摆脱了类型的限制,提供了通用的处理过程,极大提升了代码的重用性。 13 | # 二、什么是函数模板 14 | [cppreference](https://zh.cppreference.com/w/cpp/language/function_template)中给出的定义是"**函数模板定义一族函数**",怎么理解呢?我们先来看一段简单的代码 15 | ```c++ 16 | #include 17 | 18 | template 19 | void swap(T &a, T &b) { 20 | T tmp{a}; 21 | a = b; 22 | b = tmp; 23 | } 24 | 25 | int main() { 26 | int a = 2, b = 3; 27 | swap(a, b); // 使用函数模板 28 | std::cout << "a=" << a << ", b=" << b << std::endl; 29 | } 30 | ``` 31 | swap支持多种类型的通用交换逻辑。它跟普通C++函数的区别在于其函数声明(declaration)前面加了个**template\**,这句话告诉编译器,swap中(函数参数、返回值、函数体中)出现类型T时,不要报错,T是一个通用类型。 32 | 函数模板的格式: 33 | ```c++ 34 | template function-declaration 35 | ``` 36 | 函数模板在形式上分为两部分:模板、函数。在函数前面加上template<...>就成为函数模板,因此对函数的各种修饰(inline、constexpr等)需要加在function-declaration上,而不是template前。如 37 | ```c++ 38 | template 39 | inline T min(const T &, const T &); 40 | ``` 41 | **parameter-list**是由英文逗号(,)分隔的列表,每项可以是下列之一: 42 | 43 | |序号|名称|说明 44 | |:--:|:--:|-- 45 | |1|非类型形参|已知的数据类型,如整数、指针等,C++11中有三种形式:
int N
int N = 1: 带默认值,该值必须是一个常量或常量表达式
int ...N: [模板参数包(可变参数模板)](https://zh.cppreference.com/w/cpp/language/parameter_pack) 46 | |2|类型形参|swap值用的形式,格式为:
typename name[ = default]
typename ... name: [模板参数包](https://zh.cppreference.com/w/cpp/language/parameter_pack) 47 | |3|模板模板形参|没错有两个"模板",这个比较复杂,有兴趣的同学可以参考
[cppreference之模板形参与模板实参](https://zh.cppreference.com/w/cpp/language/template_parameters) 48 | 49 | 上面swap函数模板,使用了类型形参。函数模板就像是一种契约,任何满足该契约的类型都可以做为模板实参。而契约就是函数实现中,模板实参需要支持的各种操作。上面swap中T需要满足的契约为:支持拷贝构造和赋值。 50 | ```c++ 51 | template 52 | void swap(T &a, T &b) { 53 | T tmp{a}; // 契约一:T需要支持拷贝构造 54 | a = b; // 契约二:T需要支持赋值操作 55 | b = tmp; 56 | } 57 | ``` 58 | # 三、函数模板不是函数 59 | 刚才我们提到函数模板用来定义**一族函数**,而不是一个函数。C++是一种强类型的语言,在不知道T的具体类型前,无法确定swap需要占用的栈大小(参数栈,局部变量),同时也不知道函数体中T的各种操作如何实现,无法生成具体的函数。只有当用具体类型去替换T时,才会生成具体函数,该过程叫做**函数模板的实例化**。当在main函数中调用`swap(a,b)`时,编译器推断出此时`T`为`int`,然后编译器会生成int版的swap函数供调用。所以相较普通函数,函数模板多了生成具体函数这一步。如果我们只是编写了函数模板,但不在任何地方使用它(也不显式实例化),则编译器不会为该函数模板生成任何代码。 60 | 61 | ![函数模板实例化](./images/template_function.png) 62 | 63 | 函数模板实例化分为隐式实例化和显式实例化。 64 | ## 3.1 隐式实例化 65 | 仍以swap为例,我们在main中调用`swap(a,b)`时,就发生了隐式实例化。当函数模板被调用,且在之前没有显式实例化时,即发生函数模板的隐式实例化。如果模板实参能从调用的语境中推导,则不需要提供。 66 | ```c++ 67 | #include 68 | 69 | template 70 | void print(const T &r) { 71 | std::cout << r << std::endl; 72 | } 73 | int main() { 74 | // 隐式实例化print(int) 75 | print(1); 76 | // 实例化print(char) 77 | print<>('c'); 78 | // 仍然是隐式实例化,我们希望编译器生成print(double) 79 | print(1); 80 | } 81 | ``` 82 | 83 | ## 3.2 显式实例化 84 | 在**函数模板定义后**,我们可以通过显式实例化的方式告诉编译器生成指定实参的函数。显式实例化声明会阻止隐式实例化。 85 | ```c++ 86 | template 87 | R add(T1 a, T2 b) { 88 | return static_cast(a + b); 89 | } 90 | // 显式实例化 91 | template double add(int, double); 92 | // 显式实例化, 推导出第三个模板实参 93 | template int add(int, int); 94 | // 全部由编译器推导 95 | template double add(double, double); 96 | ``` 97 | 如果我们在显式实例化时,只指定**部分模板实参**,则指定顺序必须自左至右依次指定,不能越过前参模板形参,直接指定后面的。 98 | 99 | ![函数模板显式实例化](./images/template_function_specify.png) 100 | 101 | # 四、函数模板的使用 102 | ## 4.1 使用非类型形参 103 | ```c++ 104 | #include 105 | 106 | // N必须是编译时的常量表达式 107 | template 108 | void printArray(const T (&a)[N]) { 109 | std::cout << "["; 110 | const char *sep = ""; 111 | for (int i = 0; i < N; i++, (sep = ", ")) { 112 | std::cout << sep << a[i]; 113 | } 114 | std::cout << "]" << std::endl; 115 | } 116 | 117 | int main() { 118 | // T: int, N: 3 119 | printArray({1, 2, 3}); 120 | } 121 | //输出:[1, 2, 3] 122 | ``` 123 | 124 | ## 4.2 返回值为auto 125 | 有些时候我们会碰到这样一种情况,函数的返回值类型取决于函数参数某种运算后的类型。对于这种情况可以采用**auto**关键字作为返回值占位符。 126 | ```c++ 127 | template 128 | auto multi(T a, T b) -> decltype(a * b) { 129 | return a * b; 130 | } 131 | ``` 132 | **decltype**操作符用于查询表达式的数据类型,也是C++11标准引入的新的运算符,其目的是解决泛型编程中有些类型由模板参数决定,而难以表示的问题。为何要将返回值后置呢? 133 | ```c++ 134 | // 这样是编译不过去的,因为decltype(a*b)中,a和b还未声明,编译器不知道a和b是什么。 135 | template 136 | decltype(a*b) multi(T a, T b) { 137 | return a*+ b; 138 | } 139 | //编译时会产生如下错误:error: use of undeclared identifier 'a' 140 | ``` 141 | 142 | ## 4.3 类成员函数模板 143 | 函数模板可以做为类的成员函数。 144 | ```c++ 145 | #include 146 | 147 | class object { 148 | public: 149 | template 150 | void print(const char *name, const T &v) { 151 | std::cout << name << ": " << v << std::endl; 152 | } 153 | }; 154 | 155 | int main() { 156 | object o; 157 | o.print("name", "Crystal"); 158 | o.print("age", 18); 159 | } 160 | ``` 161 | 输出: 162 | ``` 163 | name: Crystal 164 | age: 18 165 | ``` 166 | 需要注意的是:*函数模板不能用作虚函数*。这是因为C++编译器在解析类的时候就要确定虚函数表(vtable)的大小,如果允许一个虚函数是函数模板,那么就需要在解析这个类之前扫描所有的代码,找出这个模板成员函数的调用或显式实例化操作,然后才能确定虚函数表的大小,而显然这是不可行的。 167 | ## 4.4 函数模板重载 168 | 函数模板之间、普通函数和模板函数之间可以重载。编译器会根据调用时提供的函数参数,调用能够处理这一类型的最佳匹配版本。在匹配度上,一般按照如下顺序考虑: 169 | |顺序|行为 170 | |:--:|-- 171 | |1|最符合函数名和参数类型的**普通函数** 172 | |2|特殊模板(具有非类型形参的模板,即对T有类型限制) 173 | |3|普通模板(对T没有任何限制的) 174 | |4|通过类型转换进行参数匹配的重载函数 175 | ```c++ 176 | #include 177 | 178 | template 179 | const T &max(const T &a, const T &b) { 180 | std::cout << "max(&, &) = "; 181 | return a > b ? a : b; 182 | } 183 | 184 | // 函数模板重载 185 | template 186 | const T *max(T *a, T *b) { 187 | std::cout << "max(*, *) = "; 188 | return *a > *b ? a : b; 189 | } 190 | 191 | // 函数模板重载 192 | template 193 | const T &max(const T &a, const T &b, const T &c) { 194 | std::cout << "max(&, &, &) = "; 195 | const T &t = (a > b ? a : b); 196 | return t > c ? t : c; 197 | } 198 | 199 | // 普通函数 200 | const char *max(const char *a, const char *b) { 201 | std::cout << "max(const char *, const char *) = "; 202 | return strcmp(a, b) > 0 ? a : b; 203 | } 204 | 205 | int main() { 206 | int a = 1, b = 2; 207 | std::cout << max(a, b) << std::endl; 208 | std::cout << *max(&a, &b) << std::endl; 209 | std::cout << max(a, b, 3) << std::endl; 210 | std::cout << max("en", "ch") << std::endl; 211 | // 可以通过空模板实参列表来限定编译器只匹配函数模板 212 | std::cout << max<>("en", "ch") << std::endl; 213 | } 214 | ``` 215 | 输出 216 | ```commandline 217 | max(&, &) = 2 218 | max(*, *) = 2 219 | max(&, &, &) = 3 220 | max(const char *, const char *) = en 221 | max(*, *) = en 222 | ``` 223 | 可以通过空模板实参列表来限定编译器只匹配函数模板,比如main函数中的最后一条语句。 224 | 225 | ## 4.5 函数模板特化 226 | 当函数模板需要对某些类型进行特别处理,这称为函数模板的特化。当我们定义一个特化版本时,函数参数类型必须与一个先前声明的模板中对应的类型匹配。函数模板特化的本质是实例化一个模板,而非重载它。因此,特化不影响编译器函数匹配。 227 | ```c++ 228 | template 229 | int compare(const T1 &a, const T2 b) { 230 | return a - b; 231 | } 232 | // 对const char *进行特化 233 | template<> 234 | int compare(const char * const &a, const char * const &b) { 235 | return strcmp(a, b); 236 | } 237 | ``` 238 | 上面的例子中针对const char \*的特化,我们其实可以通过函数重载达到相同效果。因此对于函数模板特化,目前公认的观点是**没什么用,并且最好别用**。[Why Not Specialize Function Templates?](http://www.gotw.ca/publications/mill17.htm) 239 | 240 | 但函数模板特化和重载在[重载决议](https://zh.cppreference.com/w/cpp/language/overload_resolution)时有些细微的差别。这些差别中比较有用的一个是阻止某些隐式转换。如当你只有void foo(int)时,以浮点类型调用会发生隐式转换,这可以通过特化来阻止: 241 | ```c++ 242 | template void foo(T); 243 | template <> void foo(int) {} 244 | foo(3.0); // link error,阻止float隐式转换为int 245 | ``` 246 | 虽然模板配重载也可以达到同样的效果,但特化版的意图更加明确。 247 | > 函数模板及其特化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特化版本。 248 | 249 | # 五、变参函数模板(模板参数包) 250 | 这是C++11引入的新特性,用来表示任意数量的模板形参。其语法样式如下: 251 | ```c++ 252 | template // Args: 模板参数包 253 | void foo(Args ... args); // args: 函数参数包 254 | ``` 255 | 在模板形参Args的左边出现三个英文点号"...",表示Args是零个或多个类型的列表,是一个模板参数包(template parameter pack)。正如其名称一样,编译器会将Args所表示的类型列表打成一个包,将其当做一个特殊类型处理。相应的函数参数列表中也有一个*函数参数包*。与普通模板函数一样,编译器从函数的实参推断模板参数类型,与此同时还会推断包中参数的数量。 256 | ```c++ 257 | // sizeof...() 是C++11引入的参数包的操作函数,用来取参数的数量 258 | template 259 | int length(Args ... args) { 260 | return sizeof...(Args); 261 | } 262 | 263 | // 以下语句将在屏幕打印出:2 264 | std::cout << length(1, "hello") << std::endl; 265 | ``` 266 | 变参函数模板主要用来处理既不知道要处理的实参的数目也不知道它们的类型时的场景。既然我们对实参数量以及类型都一无所知,那么我们怎么使用它呢?最常用的方法是*递归*。 267 | ## 5.1 递归 268 | 通过递归来遍历所有的实参,这需要一点点的技巧,需要给出终止递归的条件,否则递归将无限进行。 269 | ```c++ 270 | #include 271 | 272 | // 递归终止 273 | void print() { /// 1 274 | std::cout << std::endl; 275 | } 276 | 277 | // 打印绑定到t的实参 278 | template 279 | void print(const T &t, const Args &... args) { /// 2 280 | std::cout << t << (sizeof...(args) > 0 ? ", " : ""); 281 | // 编译时展开:通过在args右边添加省略号(...)进行展开,打印参数包中剩余的参数 282 | print(args...); 283 | } 284 | 285 | int main() { 286 | print(1, "hello", "C++", 11); 287 | return 0; 288 | } 289 | //输出: 1, hello, C++, 11 290 | ``` 291 | 该例子的技巧在于,函数2提供了const T &t参数,保证至少有一个参数,避免了与函数1在args为0时的冲突。需要注意的是,递归是指**编译器递归**,不是运行过程时的递归调用。实际上编译器为函数2生成了4个重载版本,并依次调用。下图是在运行时的调用栈,可以看到共有5个重载版本的print函数,4个递归展开的函数2,外加函数1。递归最终结束在函数1处。 292 | 293 | ![compiler_recursive.png](./images/compiler_recursive.png) 294 | 295 | ## 5.2 包扩展 296 | 对于一个参数包,不管是模板参数包还是函数参数包,我们对它能做的只有两件事:**sizeof...()**和**包扩展**。前面我们说过编译器将参数包当作**一个**类型来处理,因此使用的时候需要将其展开,展开时我们需要提供用于每个元素的处理模式(pattern)。包扩展就是**对参数包中的每一个元素应用模式,获取得扩展后的列表**。最简单的包扩展方式就是我们在上节中看到的`const Args &...`和`args...`,该扩展是将其扩展为构成元素。C++11还支持更复杂的扩展模式,如: 297 | ```c++ 298 | #include 299 | #include 300 | #include 301 | #include 302 | 303 | template 304 | std::string to_str(const T &r) { 305 | std::stringstream ss; 306 | ss << "\"" << r << "\""; 307 | return ss.str(); 308 | } 309 | 310 | template 311 | void init_vector(std::vector &vec, const Args &...args) { 312 | // 复杂的包扩展方式 313 | vec.assign({to_str(args)...}); 314 | } 315 | 316 | int main() { 317 | std::vector vec; 318 | init_vector(vec, 1, "hello", "world"); 319 | std::cout << "vec.size => " << vec.size() << std::endl; 320 | for (auto r: vec) { 321 | std::cout << r << std::endl; 322 | } 323 | } 324 | ``` 325 | 运行程序将产生如下输出: 326 | ``` 327 | vec.size => 3 328 | "1" 329 | "hello" 330 | "world" 331 | ``` 332 | 扩展过程中模式(pattern)会独立地应用于包中的每一个元素。同时pattern也可以接受多个参数,并非仅仅只能接受参数包。 333 | ## 5.3 参数包的转发 334 | C++11中,我们可以同时使用变参函数模板和std::forward机制来编写函数,将实参原封不动地传递给其它函数。其中典型的应用是std::vector::emplace_back操作: 335 | ```c++ 336 | template 337 | template 338 | void vector::emplace_back(_Args&&... __args) { 339 | push_back (T(forward<_Args>(__args)... )); 340 | } 341 | ``` 342 | # 六、其它 343 | ## 6.1 函数模板 .vs. 模板函数 344 | 函数模板重点在*模板*。表示这是一个模板,用来生成函数。
模板函数重点在*函数*。表示的是由一个模板生成而来的函数。 345 | ## 6.2 [cv限定](https://zh.cppreference.com/w/cpp/language/cv) 346 | cv限定是指函数参数中有const、volatile或mutable限定。已指定、推导出或从默认模板实参获得所有模板实参时,函数参数列表中每次模板形参的使用都会被替换成对应的模板实参。替换后: 347 | * 所有数组类型和函数类型参数被调整成为指针 348 | * 所有顶层cv限定符从函数参数被丢弃,如在普通函数声明中。 349 | 350 | 顶层cv限定符的去除不影响参数类型的使用,因为它出现于函数中: 351 | ```c++ 352 | template void f(T t); 353 | template void g(const X x); 354 | template void h(Z z, Z *zp); 355 | 356 | // 两个不同函数有同一类型,但在函数中, t有不同的cv限定 357 | f(1); // 函数类型是 void(int) , t 为 int 358 | f(1); // 函数类型是 void(int) , t 为 const int 359 | 360 | // 二个不同函数拥有同一类型和同一 x 361 | // (指向此二函数的指针不相等,且函数局域的静态变量可以拥有不同地址) 362 | g(1); // 函数类型是 void(int) , x 为 const int 363 | g(1); // 函数类型是 void(int) , x 为 const int 364 | 365 | // 仅丢弃顶层 cv 限定符: 366 | h(1, NULL); // 函数类型是 void(int, const int*) 367 | // z 为 const int , zp 为 int* 368 | ``` 369 | -------------------------------------------------------------------------------- /10-generic_class.MD: -------------------------------------------------------------------------------- 1 | # C++11泛型 - 类模板 2 | 前面我们介绍了[函数模板](09-generic_function.MD)。今天我们来看看C++的另一种泛型:类模板。C++中类模板通常是容器(如std::vector)或行为的封装(如之前我们实现的chan类)。类模板语法: 3 | ```c++ 4 | template < parameter-list > class-declaration 5 | ``` 6 | 构成类模板的形参(parameter-list)约束与函数模板相同,此处就不赘述了。与函数模板的类型自动推演不同,类模板实例化时,需要显式指定: 7 | ```c++ 8 | std::vector intArr; 9 | ``` 10 | # 一、成员函数 11 | 与函数模板一样,类模板只是定义了一组通用的操作,在具体实例化前是不占用程序空间的。这种Lazy特性在类模板中得到了进一步地加强:**成员函数(含成员函数模板)只有在使用时才生成**。 12 | ```c++ 13 | template 14 | class A { 15 | T a_; 16 | public: 17 | void add(int n) { 18 | a_ += n; 19 | } 20 | }; 21 | 22 | class M{}; 23 | 24 | int main() { 25 | A s; // s未用到add函数,因此整个程序得以成功编译 26 | // s.add(1); // 如果有这句话,则会编译失败 27 | } 28 | ``` 29 | 本例中,A::add在变量s中未使用到,因此虽然a_ += n不合法,但整个程序仍然通过了编译。 30 | 31 | ## 1.1 虚函数 32 | 在函数模板中我们提到*虚函数*不能是函数模板,那么在类模板中可以有虚函数吗?答案是肯定的,类模板中可以有虚函数,虚函数在类模板实例化为模板类时由编译器生成,因此其实现必须是合法的,否则即使未被使用到,也会编译失败。类模板的虚函数可以访问模板类中的泛型成员(变量、成员函数模板都可以访问)。 33 | ```c++ 34 | #include 35 | template 36 | class A { 37 | T a_; 38 | public: 39 | virtual void say() { 40 | std::cout << "a -> " << a_ << std::endl; 41 | } 42 | }; 43 | 44 | class M{}; 45 | 46 | int main() { 47 | // 尽管say函数未被使用,此处会编译仍会失败,因为std::cout << m.a_操作是非法的 48 | A m; 49 | } 50 | ``` 51 | ## 1.2 成员函数模板 52 | 类模板和函数模板结合就是成员函数模板。 53 | ```c++ 54 | #include 55 | template 56 | class Printer { 57 | T prefix_; 58 | public: 59 | explicit Printer(const T &prefix):prefix_(prefix){ 60 | } 61 | // 成员函数模板 62 | template void print(const U &u, Args... args); 63 | void print() { 64 | std::cout << std::endl; 65 | } 66 | }; 67 | 68 | template template 69 | void Printer::print(const U &u, Args... args) { 70 | std::cout << this->prefix_ << u << std::endl; 71 | print(args...); 72 | } 73 | ``` 74 | # 二、类模板特化与偏特化 75 | 模板特化是指定类模板的**特定实现**。是针对某类型参数的特殊化处理。假设我们有一个类模板Stack,它有一个功能:min(取Stack的最小值),则该类模板的典型实现如下: 76 | ```c++ 77 | template 78 | struct StackItem { 79 | StackItem *next; 80 | T item; 81 | }; 82 | template 83 | class Stack { 84 | StackItem *front = nullptr; 85 | public: 86 | T min() const { 87 | assert(front != nullptr); 88 | T min = front->item; 89 | for (StackItem *it = front->next; it != nullptr; it = it->next) { 90 | if (it->item < min) { 91 | min = it->item; 92 | } 93 | } 94 | return min; 95 | } 96 | }; 97 | ``` 98 | Stack::min所需满足的契约是:T需支持小于操作(operator <)。但有些类型无法满足该要求,如const char *。如果Stack要支持const char *的话,则需要特化。 99 | ```c++ 100 | template<> 101 | class Stack // 类名后面,跟上<...>,则表明是特化 102 | { 103 | StackItem *front = nullptr; 104 | public: 105 | const char * min() const { 106 | assert(front != nullptr); 107 | const char * min = front->item; 108 | for (StackItem *it = front->next; it != nullptr; it = it->next) { 109 | if (strcmp(it->item, min) < 0) { 110 | min = it->item; 111 | } 112 | } 113 | return min; 114 | } 115 | }; 116 | ``` 117 | ## 2.1 偏特化 118 | 偏特化也叫部分特化,指的是当类模板有一个以上模板参数时,我们希望能对某个或某几个模板实参进行特化。类模板的特化(或偏特化)只需要*模板名称相同*并且*特化列表<>中的参数个数与原始模板对应*上即可,模板参数列表不必与原始模板相同模板名称相同。一个类模板可以有多个特化,与函数模板相同,编译器会自动实例化那个最特殊的版本。 119 | 120 | ![类模板特化和偏特化](./images/template_class_specify.png) 121 | 122 | 完全特化的结果是一个实际的class,而偏特化的结果是另外一个同名的模板。 123 | 124 | # 三、类模板中的static成员 125 | 类模板中可以声明static成员。但需要注意的是**每个不同模板实例都会有一个独立的static成员变量**。 126 | ```c++ 127 | template 128 | class A { 129 | public: 130 | static int count; 131 | }; 132 | template int A::count = 1; 133 | ``` 134 | 则`A::count`与`A::count`是不同的两个变量。 135 | ## 3.1 类模板中static成员的特化 136 | static成员也可以进行特化 137 | ```c++ 138 | // template A {...}; // 见上面的定义 139 | template<> int A::count = 100; 140 | ``` 141 | 则A::count的值被值始化为100,而以其它类型进行实例化时则初始化为1。 142 | 143 | # 四、友元 144 | 友元在C++中做为一个**BUG**式的存在,可以授权“好友”访问其隐私数据。 145 | ```c++ 146 | template class A; // 前置声明,在B中声明友元需要的 147 | 148 | template 149 | class B { 150 | // 每个B的实例将授权相同类型实例化的A 151 | friend class A; 152 | // C的所有实例都是B的友元,该种情况下C无需前置声明 153 | template fiend class C; 154 | // 普通类 155 | friend class D; 156 | // 模板自己的类型参数成为友元 157 | friend U; 158 | }; 159 | ``` 160 | # 五、总结 161 | 本节简单介绍了类模板,由于篇幅限制不能一一展开,如有疏漏欢迎批评指正。 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cpp11misc 2 | 3 | ## 1. [C++11多线程(简约但不简单)](./01-thread.MD) 4 | ## 2. [C++11多线程-mutex](./02-mutex.MD) 5 | ## 3. [C++11多线程-mutex(2)](./03-mutex-2.MD) 6 | ## 4. [C++11多线程-条件变量(condition_variable)](./04-condition_variable.MD) 7 | ## 5. [C++11多线程-异步运行之promise](./05-async-1-promise.MD) 8 | ## 6. [C++11多线程-异步运行之packaged_task](./05-async-2-packaged_task.MD) 9 | ## 7. [C++11多线程-异步运行之最终篇(future+async)](./05-async-3-future.MD) 10 | ## 8. [C++11多线程-线程局部存储](./06-thread_local.MD) 11 | ## 9. [C++11多线程-原子操作(1)](./07-atomic-1.MD) 12 | ## 10. [C++11多线程-原子操作(2)](./07-atomic-2.MD) 13 | ## 11. [C++11多线程-内存模型](./08-memory_order.MD) 14 | ## 12. [C++11-函数模板](09-generic_function.MD) 15 | 16 | ## 17 | ## VSCode下环境搭建相关: 18 | 1. mac在vscode下搭建C/C++环境 https://www.jianshu.com/p/050fa455bc74 19 | 2. vscode下c++代码格式化 20 | * https://www.jianshu.com/p/542e535117eb 21 | * https://blog.csdn.net/softimite_zifeng/article/details/78357898 22 | 3. mac下代码覆盖率工具 https://www.cnblogs.com/fnlingnzb-learner/p/6943512.html 23 | 4. 本文使用的格式化参数
24 | {BasedOnStyle: Google, IndentWidth: 4, AccessModifierOffset: -4, AlignTrailingComments: true, AllowShortFunctionsOnASingleLine: false, ColumnLimit: 120, ConstructorInitializerAllOnOneLineOrOnePerLine: true} 25 | -------------------------------------------------------------------------------- /code/chan.h: -------------------------------------------------------------------------------- 1 | /** 2 | * chan的使用: 3 | * 1. 阻塞队列: 4 | * 1.1 chan ch(0): 无缓冲队列,push阻塞直到pop取走数据,或ch关闭。 5 | * 1.2 chan ch(N): N>0, 具有N个缓冲区的队列,只有当队列满时push才会阻塞。 6 | * 2. 非阻塞队列 7 | * 2.1 chan ch(N, discard_old): 具有N个缓冲区的实时队列, 当N==0时队列长度为1。 8 | * push不阻塞,当队列满时,新push的数据会替换掉最老的数据。 9 | * 2.2 chan ch(N, discard): 具有N个缓冲区的队列,当N==0时队列长度为1。 10 | * push不阻塞,当队列满时,新push的数据会失败。 11 | */ 12 | #pragma once 13 | #ifdef _MSC_VER 14 | #pragma warning(push) 15 | #pragma warning(disable : 4200) 16 | #endif 17 | 18 | #include // std::condition_variable 19 | #include // unique_ptr 20 | #include // std::mutex 21 | #include 22 | #ifndef CHAN_MAX_COUNTER 23 | # include // std::numeric_limits 24 | // VC下max问题: https://www.cnblogs.com/cvbnm/articles/1947743.html 25 | # define CHAN_MAX_COUNTER (std::numeric_limits::max)() 26 | #endif 27 | 28 | // 队列满时的push策略 29 | enum class push_policy : unsigned char { 30 | blocking, // 阻塞, 直到队列腾出空间 31 | discard_old, // 丢弃队列中最老数据,非阻塞 32 | discard, // 丢弃当前入chan的值,并返回false。非阻塞 33 | }; 34 | 35 | namespace ns_chan { 36 | // 避免惊群 37 | class cv_t { 38 | std::condition_variable cv_; 39 | uint32_t thread_count_ = 0; 40 | uint32_t wait_count_ = 0; 41 | 42 | public: 43 | template 44 | void wait(std::unique_lock &lock, Predicate pred) { 45 | if (!pred()) { 46 | ++thread_count_; 47 | do { 48 | ++wait_count_; 49 | cv_.wait(lock); 50 | } while (!pred()); 51 | --thread_count_; 52 | } 53 | } 54 | 55 | void notify_one() { 56 | if (wait_count_ > 0) { 57 | wait_count_ = (wait_count_ > thread_count_ ? thread_count_ : wait_count_) - 1; 58 | cv_.notify_one(); 59 | } 60 | } 61 | void notify_all() { 62 | wait_count_ = 0; 63 | cv_.notify_all(); 64 | } 65 | }; 66 | 67 | template 68 | class queue_t { 69 | mutable std::mutex mutex_; 70 | cv_t cv_push_; 71 | cv_t cv_pop_; 72 | std::condition_variable *const cv_overflow_; 73 | const size_t capacity_; // _data容量 74 | const push_policy policy_; // 队列满时的push策略 75 | bool closed_ = false; // 队列是否已关闭 76 | size_t first_ = 0; // 队列中的第一条数据 77 | size_t new_ = 0; // 新数据的插入位置,first_==new_队列为空 78 | T data_[0]; // T data_[capacity_] 79 | private: 80 | queue_t(size_t capacity, push_policy policy) 81 | : capacity_(capacity == 0 ? 1 : capacity), 82 | policy_(policy), 83 | cv_overflow_(capacity == 0 ? new std::condition_variable() : nullptr) { 84 | } 85 | public: 86 | queue_t(const queue_t &) = delete; 87 | queue_t(queue_t &&) = delete; 88 | queue_t &operator=(const queue_t &) = delete; 89 | queue_t &operator=(queue_t &&) = delete; 90 | 91 | ~queue_t() { 92 | for (; first_ < new_; first_++) { 93 | data(first_).~T(); 94 | } 95 | delete cv_overflow_; 96 | } 97 | 98 | // close以后的入chan操作会返回false, 而出chan则在队列为空后,才返回false 99 | void close() { 100 | std::unique_lock lock(mutex_); 101 | closed_ = true; 102 | if (cv_overflow_ != nullptr && !is_empty()) { 103 | // 消除溢出 104 | data(--new_).~T(); 105 | cv_overflow_->notify_all(); 106 | } 107 | cv_push_.notify_all(); 108 | cv_pop_.notify_all(); 109 | } 110 | 111 | bool is_closed() const { 112 | std::unique_lock lock(mutex_); 113 | return closed_; 114 | } 115 | 116 | // 入chan,支持move语义 117 | template 118 | bool push(TR &&data) { 119 | std::unique_lock lock(mutex_); 120 | cv_push_.wait(lock, [&]() { return policy_ != push_policy::blocking || free_count() > 0 || closed_; }); 121 | if (closed_) { 122 | return false; 123 | } 124 | 125 | if (!push_thread_unsafe(std::forward(data))) { 126 | return false; 127 | } 128 | 129 | cv_pop_.notify_one(); 130 | if (cv_overflow_ != nullptr) { 131 | const size_t old = first_; 132 | cv_overflow_->wait(lock, [&]() { return old != first_ || closed_; }); 133 | } 134 | 135 | return !closed_; 136 | } 137 | 138 | bool pop(std::function consume) { 139 | std::unique_lock lock(mutex_); 140 | cv_pop_.wait(lock, [&]() { return !is_empty() || closed_; }); 141 | if (is_empty()) { 142 | return false; // 已关闭 143 | } 144 | 145 | T &target = data(first_++); 146 | consume(std::move(target)); 147 | target.~T(); 148 | 149 | if (cv_overflow_ != nullptr) { 150 | cv_overflow_->notify_one(); 151 | } 152 | cv_push_.notify_one(); 153 | 154 | return true; 155 | } 156 | 157 | static std::shared_ptr make_queue(size_t capacity, push_policy policy) { 158 | if (policy != push_policy::blocking && capacity == 0) { 159 | capacity = 1; 160 | } 161 | const size_t size = sizeof(queue_t) + sizeof(T) * (capacity == 0 ? 1 : capacity); 162 | // 只有阻塞模式下才存在“溢出”等待区 163 | return std::shared_ptr(new (new char[size]) queue_t(capacity, policy), [](queue_t *q) { 164 | q->~queue_t(); 165 | delete[](char *) q; 166 | }); 167 | } 168 | 169 | private: 170 | template 171 | bool push_thread_unsafe(TR &&d) { 172 | if (free_count() > 0) { 173 | new (&data(new_++)) T(std::forward(d)); 174 | } else if (policy_ == push_policy::discard_old) { 175 | first_++; // 替换掉最老的 176 | data(new_++) = std::forward(d); 177 | } else { 178 | assert(policy_ == push_policy::discard); 179 | return false; // 取消此次操作, 需结合is_closed来判断是否已关闭 180 | } 181 | // 防_first和_new溢出 182 | if (new_ >= CHAN_MAX_COUNTER) { 183 | reset_pos(); 184 | } 185 | 186 | return true; 187 | } 188 | size_t free_count() const { 189 | return first_ + capacity_ - new_; 190 | } 191 | bool is_empty() const { 192 | return first_ >= new_; 193 | } 194 | T &data(size_t pos) { 195 | return data_[pos % capacity_]; 196 | } 197 | 198 | void reset_pos() { 199 | const size_t new_first = (this->first_ % this->capacity_); 200 | this->new_ -= (this->first_ - new_first); 201 | this->first_ = new_first; 202 | } 203 | 204 | }; 205 | } 206 | 207 | template 208 | class chan { 209 | struct data_t{ 210 | std::vector > > queue_; 211 | std::atomic push_{0}, pop_{0}; 212 | }; 213 | 214 | std::shared_ptr data_; 215 | 216 | public: 217 | //sizeof(queue__t) = 216, sizeof(cv) = 48/56, sizeof(mutex) = 64 218 | // 选取适当的concurrent_shift和capacity,chan的吞吐理可达千万/秒 219 | explicit chan(size_t concurrent_shift, size_t capacity, push_policy policy = push_policy::blocking) { 220 | data_ = std::make_shared(); 221 | data_->queue_.resize(1 << concurrent_shift); 222 | for (auto &r: data_->queue_) { 223 | r = ns_chan::queue_t::make_queue(capacity, policy); 224 | } 225 | } 226 | 227 | explicit chan(size_t capacity = 0, push_policy policy = push_policy::blocking) 228 | : chan(0, capacity, policy) { 229 | } 230 | 231 | // 支持拷贝 232 | chan(const chan &) = default; 233 | chan &operator=(const chan &) = default; 234 | // 支持move 235 | chan(chan &&) = default; 236 | chan &operator=(chan &&) = default; 237 | 238 | // 入chan,支持move语义 239 | template 240 | bool operator<<(TR &&data) { 241 | unsigned int index = data_->push_.fetch_add(1, std::memory_order_acq_rel); 242 | return data_->queue_[index % length()]->push(std::forward(data)); 243 | } 244 | template 245 | bool push(TR &&data) { 246 | unsigned int index = data_->push_.fetch_add(1, std::memory_order_acq_rel); 247 | return data_->queue_[index % length()]->push(std::forward(data)); 248 | } 249 | 250 | void close() { 251 | for (size_t i = 0; i < length(); i++) { 252 | data_->queue_[i]->close(); 253 | } 254 | } 255 | 256 | bool is_closed() const { 257 | return data_->queue_[0]->is_closed(); 258 | } 259 | 260 | // 出chan 261 | template 262 | bool operator>>(TR &d) { 263 | unsigned int index = data_->pop_.fetch_add(1, std::memory_order_acq_rel); 264 | return data_->queue_[index % length()]->pop([&d](T &&target) { d = std::forward(target); }); 265 | } 266 | 267 | // 性能较operator>>稍差,但外部用起来更方便 268 | // 当返回false时表明chan已关闭: while(d = ch.pop()){} 269 | std::unique_ptr pop() { 270 | unsigned int index = data_->pop_.fetch_add(1, std::memory_order_acq_rel); 271 | std::unique_ptr d; 272 | data_->queue_[index % length()]->pop([&d](T &&target) { d.reset(new T(std::forward(target))); }); 273 | return d; 274 | } 275 | private: 276 | size_t length() const { 277 | return data_->queue_.size(); 278 | } 279 | }; 280 | 281 | #ifdef _MSC_VER 282 | #pragma warning(pop) 283 | #endif 284 | -------------------------------------------------------------------------------- /code/chan.simple.h: -------------------------------------------------------------------------------- 1 | // chan.simple.h 2 | #pragma once 3 | #include // std::condition_variable 4 | #include // std::list 5 | #include // std::mutex 6 | 7 | template 8 | class chan { 9 | class queue_t { 10 | mutable std::mutex mutex_; 11 | std::condition_variable cv_; 12 | std::list data_; 13 | const size_t capacity_; // data_容量 14 | const bool enable_overflow_; 15 | bool closed_ = false; // 队列是否已关闭 16 | size_t pop_count_ = 0; // 计数,累计pop的数量 17 | public: 18 | queue_t(size_t capacity) : capacity_(capacity == 0 ? 1 : capacity), enable_overflow_(capacity == 0) { 19 | } 20 | 21 | bool is_empty() const { 22 | return data_.empty(); 23 | } 24 | size_t free_count() const { 25 | // capacity_为0时,允许放入一个,但_queue会处于overflow状态 26 | return capacity_ - data_.size(); 27 | } 28 | bool is_overflow() const { 29 | return enable_overflow_ && data_.size() >= capacity_; 30 | } 31 | 32 | bool is_closed() const { 33 | std::unique_lock lock(this->mutex_); 34 | return this->closed_; 35 | } 36 | 37 | // close以后的入chan操作会返回false, 而出chan则在队列为空后才返回false 38 | void close() { 39 | std::unique_lock lock(this->mutex_); 40 | this->closed_ = true; 41 | if (this->is_overflow()) { 42 | // 消除溢出 43 | this->data_.pop_back(); 44 | } 45 | this->cv_.notify_all(); 46 | } 47 | 48 | template 49 | bool pop(TR &data) { 50 | std::unique_lock lock(this->mutex_); 51 | this->cv_.wait(lock, [&]() { return !is_empty() || closed_; }); 52 | if (this->is_empty()) { 53 | return false; // 已关闭 54 | } 55 | 56 | data = this->data_.front(); 57 | this->data_.pop_front(); 58 | this->pop_count_++; 59 | 60 | if (this->free_count() == 1) { 61 | // 说明以前是full或溢出状态 62 | this->cv_.notify_all(); 63 | } 64 | 65 | return true; 66 | } 67 | 68 | template 69 | bool push(TR &&data) { 70 | std::unique_lock lock(mutex_); 71 | cv_.wait(lock, [this]() { return free_count() > 0 || closed_; }); 72 | if (closed_) { 73 | return false; 74 | } 75 | 76 | data_.push_back(std::forward(data)); 77 | if (data_.size() == 1) { 78 | cv_.notify_all(); 79 | } 80 | 81 | // 当queue溢出,需等待queue回复正常 82 | if (is_overflow()) { 83 | const size_t old = this->pop_count_; 84 | cv_.wait(lock, [&]() { return old != pop_count_ || closed_; }); 85 | } 86 | 87 | return !this->closed_; 88 | } 89 | }; 90 | std::shared_ptr queue_; 91 | 92 | public: 93 | explicit chan(size_t capacity = 0) { 94 | queue_ = std::make_shared(capacity); 95 | } 96 | 97 | // 支持拷贝 98 | chan(const chan &) = default; 99 | chan &operator=(const chan &) = default; 100 | // 支持move 101 | chan(chan &&) = default; 102 | chan &operator=(chan &&) = default; 103 | 104 | // 入chan,支持move语义 105 | template 106 | bool operator<<(TR &&data) { 107 | return queue_->push(std::forward(data)); 108 | } 109 | 110 | // 出chan(支持兼容类型的出chan) 111 | template 112 | bool operator>>(TR &data) { 113 | return queue_->pop(data); 114 | } 115 | 116 | // close以后的入chan操作返回false, 而出chan则在队列为空后才返回false 117 | void close() { 118 | queue_->close(); 119 | } 120 | 121 | bool is_closed() const { 122 | return queue_->is_closed(); 123 | } 124 | }; 125 | -------------------------------------------------------------------------------- /images/atomic_flag-test_and_set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcj116/cpp11thread/75621c07ef21f21ce1600a8a895dfd0987f4a1f9/images/atomic_flag-test_and_set.png -------------------------------------------------------------------------------- /images/cache_inconformity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcj116/cpp11thread/75621c07ef21f21ce1600a8a895dfd0987f4a1f9/images/cache_inconformity.png -------------------------------------------------------------------------------- /images/compare_exchange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcj116/cpp11thread/75621c07ef21f21ce1600a8a895dfd0987f4a1f9/images/compare_exchange.png -------------------------------------------------------------------------------- /images/compiler_recursive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcj116/cpp11thread/75621c07ef21f21ce1600a8a895dfd0987f4a1f9/images/compiler_recursive.png -------------------------------------------------------------------------------- /images/cpu_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcj116/cpp11thread/75621c07ef21f21ce1600a8a895dfd0987f4a1f9/images/cpu_arch.png -------------------------------------------------------------------------------- /images/cpu_inorder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcj116/cpp11thread/75621c07ef21f21ce1600a8a895dfd0987f4a1f9/images/cpu_inorder.png -------------------------------------------------------------------------------- /images/synchronized_with.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcj116/cpp11thread/75621c07ef21f21ce1600a8a895dfd0987f4a1f9/images/synchronized_with.png -------------------------------------------------------------------------------- /images/template_class_specify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcj116/cpp11thread/75621c07ef21f21ce1600a8a895dfd0987f4a1f9/images/template_class_specify.png -------------------------------------------------------------------------------- /images/template_function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcj116/cpp11thread/75621c07ef21f21ce1600a8a895dfd0987f4a1f9/images/template_function.png -------------------------------------------------------------------------------- /images/template_function_specify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcj116/cpp11thread/75621c07ef21f21ce1600a8a895dfd0987f4a1f9/images/template_function_specify.png -------------------------------------------------------------------------------- /images/thread_local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcj116/cpp11thread/75621c07ef21f21ce1600a8a895dfd0987f4a1f9/images/thread_local.png -------------------------------------------------------------------------------- /images/thread_seq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcj116/cpp11thread/75621c07ef21f21ce1600a8a895dfd0987f4a1f9/images/thread_seq.png --------------------------------------------------------------------------------