二十四、 Thread框架
24.1 join 与 detach
t.join 和 t.detach 标志着,线程对象和线程的关系。t.join 表识,线程与线程对象 的同步关系。而 t.detach 表识,线程与线程对象的异步关系。
join 是阻塞的。
注意:主线程结束后 detach() 可能会还没运行就销毁了
24.2 传参方式
线程,有自己独立的栈。可以共享全局的变量。在线程启动的时候可以传入启动参数。
1.传值
std::thread threadTest(func,arg1,arg2,…);
2.传引用
std::thread threadTest(func,std::ref(arg1),std::ref(arg2));
24.3 常用函数
join() //阻塞运行线程 joinable() // 线程是否阻塞的 detach() // 异步运行 sdt::ref() //引用化
24.4 同步之mutex
24.5 volatile
修饰变量,此变量可能被多线程访问和修改,加上此关键字,可以避免编译器优化,不使用存储在寄存器中的值,而是每次都去内存里去读。
24.6 lock(),unlock()
对某作用域加锁,解锁,但如果作用域抛异常可能会导致解锁失败,产生死锁。
可以控制加锁粒度。
24.7 try_lock(),unlock()
尝试加锁,加锁失败会返回false,
try_lock()
1.如果互斥锁当前未被任何线程锁定,则调用线程将其锁定(从此点开始,直到调用其成员解锁,该线程拥有互斥锁)。
2.如果互斥锁当前被另一个线程锁定,则该函数将失败并返回false,而不会阻塞(调用线程继续执行)。
3.如果互斥锁当前被调用此函数的同一线程锁定,则会产生死锁(具有未定义的行为)。 请参阅recursive_mutex以获取允许来自同一线程的多个锁的互斥锁类型。
24.8 std::lock_guard()
自动锁,声明范围内进行自动加锁解锁操作
在 lock_guard 对象构造时,传入的 Mutex 对象(即它所管理的 Mutex 对象) 会被当前线程锁住。在 lock_guard
对象被析构时,它所管理的 Mutex 对象会自动解 锁,由于不需要程序员手动调用 lock 和 unlock 对 Mutex
进行上锁和解锁操作, 因此这也是最简单安全的上锁和解锁方式,尤其是在程序抛出异常后先前已被上锁的 Mutex
对象可以正确进行解锁操作,极大地简化了程序员编写与 Mutex 相关的异常 处理代码。
#include <iostream> #include <thread> #include <mutex> using namespace std; mutex mtx; void printEven(int i) { if( i%2 == 0) cout<< i <<" is even"<<endl; else throw logic_error("not even"); } void printThreadId(int id) { try{ lock_guard<mutex> lck(mtx); //栈自旋 抛出异常时栈对象自我析构。 printEven(id); // mtx.lock(); //printEven(id); //mtx.unlock(); }catch(logic_error & ){ cout<<"exception caught"<<endl; } } int main() { thread ths[10]; //spawn 10 threads for(int i=0; i<10; i++) { ths[i] = thread(printThreadId,i+1); } for(auto & th: ths) th.join(); return 0; }
24.9 死锁
死锁的原因是,container 试图多次去获取锁己获得的锁。
std::recursive_mutex 允 许多次获取相同的 mutex。
C++中 STL 中的容器,是非线程安全的。
24.10 std::recursive_mutex()
递归锁
- 调用线程从成功调用 lock 或 try_lock 开始占有recursive_mutex, 期间线程可以进行对 lock 或 try_lock的附加调用,所有权在线程调用 unlock 匹配次数时结束。
- 线程占有recursive_mutex时,若其他线程要求recursive_mutex所有权,调用lock将被阻塞,调用try_lock将返回false.
- 可锁定recursive_mutex的最大次数未指定的,但到达该数后,对 lock 的调用将抛出 std::system_error 而对 try_lock 的调用返回false;
- 若recursive_mutex在仍被线程占有时被销毁,则程序行为未定义。recursive_mutex满足 mutex 和 标准布局类型的所有要求。
24.11 同步之std::condition_variable
条件变量,多线程中对变量操作时对变量进行条件判断 从而当前线程需要是否阻塞或者被唤醒
条件变量(condition variable)是利用线程间共享的全局变量进行同步的一种机制,
主要包括两个动作:
一个线程等待某个条件为真,而将自己挂起;
另一个线程使的条件成立,并通知等待的线程继续。
为了防止竞争,条件变量的使用总是和一个互斥锁 结合在一起
C++11 中引入了条件变量,其相关内容均在<condition_variable>中。
这里主要 介绍 std::condition_variable 类。 条件变量 std::condition_variable 用于多线程之间的通信,它可以阻塞一个或同时阻塞多个线程。std::condition_variable 需要与 std::unique_lock 配合使用。 std::condition_variable 效果上相当于包装了 pthread 库中的 pthread_cond_*()系列 的函数。
当 std::condition_variable 对 象 的 某 个 wait 函 数 被 调 用 的 时 候 , 它 使 用 std::unique_lock(通过 std::mutex)来锁住当前线程。当前线程会一直被阻塞,直到另 外一个线程在相同的 std::condition_variable 对象上调用了 notification 函数来唤醒 当前线程。
24.11.1 成员函数
(1)、构造函数: 仅支持默认构造函数,拷贝、赋值和移动(move)均是被禁用的。
(2)、wait: 当前线程调用 wait()后将被阻塞,直到另外某个线程调用 notify_*唤 醒当前线程;当线程被阻塞时,该函数会自动调用 std::mutex 的 unlock()释放锁,使
得其它被阻塞在锁竞争上的线程得以继续执行。一旦当前线程获得通知(notify,通常 是另外某个线程调用
notify_*唤醒了当前线程),wait()函数也是自动调用 std::mutex 的 lock()。wait
分为无条件被阻塞和带条件的被阻塞两种。 无条件被阻塞:调用该函数前,当前线程应该已经对 unique_lock lck
完成了加锁。所有使用同一个条件变量的线程必须在 wait 函数中使用同一个 unique_lock。该 wait
函数内部会自动调用 lck.unlock()对互斥锁解锁, 使得其他被阻塞在互斥锁上的线程恢复执行。使用本函数被阻塞的当前线程在获得通知
(notified,通过别的线程调用 notify_*系列的函数)而被唤醒后,wait()函数恢复执行 并自动调用
lck.lock()对互斥锁加锁。
带条件的被阻塞: wait 函数设置了谓词(Predicate),只有当 pred 条件为 false 时 调用该 wait 函数才会阻塞当前线程,并且在收到其它线程的通知后只有当 pred 为 true 时才会被解除阻塞。因此,等效于 while
(!pred()) wait(lck)
(3)、wait_for: 与 wait()类似,只是 wait_for 可以指定一个时间段,在当前线程 收到通知或者指定的时间超时之前,该线程都会处于阻塞状态。而一旦超时或者收到了 其它线程的通知,wait_for 返回,剩下的步骤和 wait
类似。
(4)、wait_until: 与 wait_for 类似,只是 wait_until 可以指定一个时间点,在当 前线程收到通知或者指定的时间点超时之前,该线程都会处于阻塞状态。而一旦超时或 者收到了其它线程的通知,wait_until
返回,剩下的处理步骤和 wait 类似。
(5)、notify_all: 唤醒所有的 wait 线程,如果当前没有等待线程,则该函数什么也 不做。
(6)、notify_one: 唤醒某个 wait 线程,如果当前没有等待线程,则该函数什么也 不做;如果同时存在多个等待线程,则唤醒某个线程是不确定的(unspecified)。
24.12 std::unique_lock()
独占锁,当前锁作用域结束之前,其余线程无法使用mutex。
std::unique_lock对象以独占所有权的方式(uniqueowership)管理mutex对象的上锁和解锁操作,即在unique_lock对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而unique_lock的生命周期结束之后,它所管理的锁对象会被解锁。
unique_lock具有lock_guard的所有功能,而且更为灵活。虽然二者的对象都不能复制,但是unique_lock可以移动(movable),因此用unique_lock管理互斥对象,可以作为函数的返回值,也可以放到STL的容器中。
24.13 std::atomic
c++11提供了原子类型std::atomic,理论上这个T可以是任意类型。
整形有这种原子变量已经足够方便,就不需要使用std::mutex来保护该变量啦。
struct OriginCounter { // 普通的计数器 int count = 0; std::mutex mutex_; void add() { std::lock_guard<std::mutex> lock(mutex_); ++count; } void sub() { std::lock_guard<std::mutex> lock(mutex_); --count; } int get() { std::lock_guard<std::mutex> lock(mutex_); return count; } }; struct NewCounter { // 使用原子变量的计数器 std::atomic<int> count = 0; void add() { ++count; // count.store(++count);这种方式也可以 } void sub() { --count; // count.store(--count); } int get() { return count.load(); } }; void main() { NewCounter counter; std::thread thread1([&counter]() { int nTime = 10; while (nTime--) { counter.add(); printf("thread1:%d\n", counter.get()); } }); thread1.detach(); std::thread thread2([&counter]() { int nTime = 10; while (nTime--) { counter.add(); printf("thread2:%d\n", counter.get()); } }); thread2.join(); return; }
thread1:1 thread1:2 thread1:4 thread1:5 thread1:6 thread1:7 thread1:8 thread1:9 thread1:10 thread1:11 thread2:3 thread2:12 thread2:13 thread2:14 thread2:15 thread2:16 thread2:17 thread2:18 thread2:19 thread2:20
24.14 std::call_once
c++11提供了std::call_once来保证某一函数在多线程环境中只调用一次,它需要配合std::once_flag使用。
类 std::once_flag 是 std::call_once 的辅助类。
传递给多个 std::call_once 调用的 std::once_flag 对象允许那些调用彼此协调,从而只令调用之一实际运行完成。
std::once_flag 既不可复制亦不可移动。
总之避免,某函数被多个对象调用,多个对象调用std::call_once时传入std::once_flag来判断该对象是否能调用目标函数。
#include <thread> #include <mutex> #include <iostream> std::once_flag onceflag; void CallOnce() { std::call_once(onceflag, []() { std::cout << "call once" << std::endl; }); } int main() { std::thread threads[5]; for (int i = 0; i < 5; ++i) { threads[i] = std::thread(CallOnce); } for (auto& th : threads) { th.join(); } return 0; }
call once
24.15 volatile相关
volatile修饰过的变量,编译器对访问该变量的代码通常不再进行优化。
24.16 异步相关
std::future
std::future用于访问异步操作的结果,从翻译上来讲就是将来的值,future.get()时会阻塞,如果有值,就立刻返回值;如果没有,则阻塞当前线程当前位置,直到有值,也就是所谓的异步;
std::promise
std::promise内部有个future 类似
class Promise{ public: Future* m_pFuture; }
std::promise 主要用来传递future包含的信息的 包括信息的set_value()、get_future()
当需要获取线程中的某个值,可以使用std::promise
std::packaged_task
当需要获取线程函数返回值,可以使用std::packaged_task。
std::packaged_task: (1).禁用拷贝赋值。(2).支持移动赋值。
std::promise与std::future配合使用
#include <functional> #include <future> #include <iostream> #include <thread> #include <windows.h> int main() { std::promise<int> prom; std::future<int> fut = prom.get_future(); std::thread thread1([&fut]() { int startCount = GetTickCount(); printf("thread1 start :%d\n", startCount); int x = fut.get(); std::cout << "value: " << x << std::endl; int endCount = GetTickCount(); printf("thread1 end :%d\n", endCount); }); thread1.detach(); std::thread thread2([&prom]() { int value = 144; printf("thread2 set_value :%d\n", value); prom.set_value(value); }); thread2.join(); system("pause"); return 0; }
thread1 start :32191656 thread2 set_value :144 value: 144 thread1 end :32191656
留意输出结果会发现 有一个输出顺序,thread1 先等待一会,等待thread2set_value之后thread1才继续输出。
就是get时没有获得到值所以阻塞中的过程。
std::packaged_task与std::future配合使用
#include <functional> #include <future> #include <iostream> #include <thread> using namespace std; int main() { std::packaged_task<int(int)> task([](int in)->int { return in + 1; }); std::future<int> fut = task.get_future(); std::thread(std::move(task), 5).detach();//move是因为std::packaged_task不支持拷贝构造,支持移动构造。 cout << "result " << fut.get() << endl; return 0; }
result 6
std::future用于访问异步操作的结果,
而std::promise和std::packaged_task在future高一层,它们内部都有一个future,promise包装的是一个值,packaged_task包装的是一个函数,当需要获取线程中的某个值,可以使用std::promise,当需要获取线程函数返回值,可以使用std::packaged_task。
24.17 std::async
async是比future,packaged_task,promise更高级的东西,
它是基于任务的异步操作,通过async可以直接创建异步的任务,返回的结果会保存在future中,
不需要像packaged_task和promise那么麻烦,关于线程操作应该优先使用async。
#include <functional> #include <future> #include <iostream> #include <thread> #include <Windows.h> using namespace std; int main() { auto res = std::async(std::launch::async,[](int in)->int { int start = GetTickCount(); printf("func start %d\n", start); Sleep(2000); int end = GetTickCount(); int n = in + 1; printf("func value %d\n", n); printf("func end %d\n", end); return n; }, 5); // res.wait(); int beforeTime1 = GetTickCount(); printf("Main Thread beforeTime %d\n", beforeTime1); cout << "Get Value:"<<res.get() << endl; // 阻塞直到函数返回 int afterTime1 = GetTickCount(); printf("Main Thread afterTime1 %d\n", afterTime1); return 0; }
Main Thread beforeTime 34517000 func start 34517000 func value 6 func end 34519015 Get Value:6 Main Thread afterTime1 34519015
语法
std::future<T> std::async(std::launch::async | std::launch::deferred, func, args...);
std::future< T >:返回参数
参数1 执行策略:
- std::launch::async表示任务执行在另一线程
- std::launch::deferred表示延迟执行任务,调用get或者wait时才会执行,不会创建线程,惰性执行在当前线程。
如果不明确指定创建策略,以上两个都不是async的默认策略,而是未定义,它是一个基于任务的程序设计,内部有一个调度器(线程池),会根据实际情况决定采用哪种策略。
参数2 func:即将执行的函数
参数args…: 函数实参