并行编程
第一部分:thread类
c++
头文件<thread>
主要包含了std::thread
类,此外,命名空间std::this_thread
也封装在这一头文件中。
构造
thread
类的构造函数为thread (Fn&& fn, Args&&... args);
,线程会调用fn
函数,函数fn
有需要传递的参数在args
里给出。 构造函数结束的同时函数fn将被调用**。
方法
join
方法:- 声明:
void join();
- 只有当这个线程结束并返回后主线程才会继续执行。
- 声明:
detach
方法:- 声明:
void detach();
- 把这个线程与主线程分离运行。
- 声明:
一个例程:
1 |
|
以下是程序中的部分输出:
1 | thread a: 61856 |
可以看到thread_a
和thread_b
两个线程在同时向命令行输出信息。
第二部分:mutex类
多线程很方便,许多任务都可以通过并行式运算加快计算速度,但是同时也带来了一些问题,例如当多个线程同时竞争一个内存区域时,就会产生线程安全问题,导致程序运行出现一些意想不到的问题。
这样的现象就是多个进程同时竞争一个内存资源导致的。
为了解决这种问题,我们引入了一种工具——锁
。所谓锁
,顾名思义,是用来保护内存的工具,在多线程开发中,必须合理地使用锁才能保证程序的安全。
在c++11
中提供了mutex(互斥类)
,它们封装在头文件<mutex>
中。通过mutex
可以比较方便的在多线程中使用锁
。
std::mutex也被称为互斥锁,同一时间只能被一个线程上锁,其他线程想上锁时,必须等待当前锁被释放后,才能成功上锁。也就是说,std::mutex可以保证,处于同一个mutex对象下的一对lock()和unlock()间的代码,不会有多个线程在同时运行。
下面先介绍mutex
类。
构造
mutex
的构造不需要传递参数,直接std::mutex mtx
即可。
方法
lock()
方法- 如果
mutex
现在没有被任何线程在占有,那么调用mtx.lock()
函数可以锁住mutex
,即mutex
被调用mtx.lock
的线程占有。 - 如果
mutex
已经被其他线程占有,那么调用mtx.lock()
函数的线程将会被阻滞,直到mutex
被占有它的进程释放(执行mtx.unlock()
)且本线程成功锁住mutex
,线程才会恢复运行。 - 如果在
mutex
已经被本线程锁定的情况下执行mtx.lock()
函数,那么将会造成死锁。由于本教程为速成教程,故不在此解释死锁概念和其解决方法。但死锁概念在多线程编程中十分重要,建议读者百度自学。在此,你可以将死锁
理解为一把会将进程彻底锁死的锁,产生死锁
后线程将被阻滞且不会被唤醒。
- 如果
unlock()
方法- 解锁本线程锁定的
mutex
,以便其他线程可以占有mutex
- 解锁本线程锁定的
下面使用mutex
解决一个程序中多线程竞争内存导致的错误:
1 |
|
1 | result: 200000 |
mutex
已经十分方便了,但是c++11
还提供了更方便的工具unique_lock
,他和mutex
一样封装在<mutex>
头文件下,下面将介绍unique_lock
。
构造
由于std::mutex需要手动unlock(),当一个代码段有多个退出点时,需要在每个退出点都加上unlock(),容易漏。而且如果该代码段还会抛出异常,手动unlock()会更加麻烦。这时我们通常会使用std::unique_lock或std::scoped_lock,这两个类型的作用就是在构造这个类型的对象时,传入一个std::mutex,随后会自动加锁,在对象析构时,也会自动释放锁。
unique_lock
类较简单的构造方法为使用mutex
进行构造,构造函数为unique_lock (mutex_type& m);
。 e.g.:
1 | unique_lock lock(mtx); |
unique_lock
在构造时会自动获取mutex
,如果无法锁住mutex
,则此线程会停滞,直到成功获取mutex
。 在unique_lock
被析构时,他会自动unlock
自己获取的mutex
。这使得他在许多地方非常方便实用。
下面将用unique_lock
实现上一段代码中mutex
同样的效果。
1 |
|
这段程序也可以得到正确的输出结果。
同时,unique_lock
还有一个很方便的方法:try_lock()
,这个函数会尝试锁住当前线程并返回一个bool
值表示是否成功锁住,但不论是否成功锁住当前线程,当前线程都会继续运行。
原子变量和互斥锁的区别
互斥锁是通过线程休眠来等待其他线程完成当前任务,而原子变量是通过CPU空转来等待其他线程完成当前任务(准确的说是通过总线锁,但表现形式上和空转等待差不多)。因为没有发生线程切换,所以也就节省了线程切换时的开销。
第三部分:condition_variable
在有些场景下,一个线程可能希望满足一个条件后才继续运行,否则线程休眠等待该条件的发生。最典型的情况就是线程等待用户的输入,在没有用户输入时,线程直接休眠等待输入。这种情况就可以使用条件变量。
condition_variable
库提供了方便的与条件变量相关的类和函数,封装在头文件<condition_variable>
下。
构造
condition_variable
类有默认构造函数,可以直接构造,不需要传递参数。 e.g.:
1 | std::condition_variable cv; |
方法
wait(unique_lock<mutex>& lck, Predicate pred)
方法wait
方法传入的两个参数分别为unique_lock
和一个返回值为bool
的函数- 如果
pred
函数返回值为真,线程会正常运行,否则线程会被阻滞。 - 在调用
wait
函数的同时,lck.unlock()
会被自动调用,以保证其他线程正常运行。 - 当函数
notified_all()
被调用时,如果函数pred
为真,线程会恢复运行。
notified_all()
方法notified_all
将试图唤醒所有被此condition_variable
阻滞的线程,只有在线程wait
函数的返回值为真的情况下程序会恢复运行
下面是一个例子:
1 |
|
通过condition_variable
的延迟使得主线程中的内容先输出后子线程的内容才输出。
另外一个例子:
1 | using namespace std::chrono; |
条件变量的使用有几个关键点:
- 必须配合互斥锁使用
- 每当条件变量等待的条件发生变化,必须手动调用条件变量的唤醒函数notify_one()或notify_all()