标准线程库
C++11 新标准中引入了几个头文件来支持多线程编程:
< thread > :包含std::thread类以及std::this_thread命名空间。管理线程的函数和类在 中声明.
< atomic > :包含std::atomic和std::atomic_flag类,以及一套C风格的原子类型和与C兼容的原子操作的函数。
< mutex > :包含了与互斥量相关的类以及其他类型和函数
< future > :包含两个Provider类(std::promise和std::package_task)和两个Future类(std::future和std::shared_future)以及相关的类型和函数。
< condition_variable > :包含与条件变量相关的类,包括std::condition_variable和std::condition_variable_any。
std::thread
在C++11中可以简单地使用std:thread类来创建线程,构造thread对象时传入一个可调用对象作为参数(如果带参,则将参数也同时传入),可调用对象包括函数、函数指针、lambda表达式、bind创建的对象或重载了函数调用运算符的对象,例如:
/*无参*/
void thread01(){
/*....*/
}
/*含参*/
void thread02(int i,int j){
/*...*/
}
...
int main(){
/*...*/
thread first(thread01);/*构造thread对象完成后新的线程立马被创建。同时可调用对象被调用*/
thread second(thread02,10,10);/*传入参数,可调用对象被调用*/
}
若采用std:thread的默认构造函数来构造对象,则该thread不关联任何线程,可以利用joinable()来查看一个thread对象是否关联某个线程
int main(){
/*...*/
thread first(thread01);/*构造thread对象完成后新的线程立马被创建。同时该可调用对象立马被调用*/
thread second(thread02,10,10);/*传入参数*/
thread third;/*没有绑定*/
cout<<first.joinable()<< " "<<second.joinable()<<" "<<third.joinable()<<endl;
}
运行结果:1 1 0
join 汇聚
thread类调用join()函数后,原始线程会等待新线程执行完毕之后再去销毁线程对象。这样,如果新线程使用了原始线程中的某个共享对象不会发生异常,在新线程结束后才会析构共享对象;若新线程和主线程分离,主线程析构了某个对象或变量后,新线程再去调用的话则会产生异常。
thread::join()还会在进程结束后清理子线程相关的内存空间,此后该thread对象不再与该子线程相关,即thread::joinable() = false ,故join对一个线程而言只能调用一次
detach 分离
thread::detach将线程从thread对象分离出来,运行线程独立执行,此时即使主线程结束了子线程还有可能在后台运行。通常称分离线程为守护线程(daemon threads),UNIX中守护线程是指,没有任何显式的用户接口,并在后台运行的线程。
由于detach不像join一样会等待子线程与主线程汇聚,故要注意以下几点:
- 如果传递int这种简单类型,推荐使用值传递,不要用引用
- 如果传递类对象,避免使用隐式类型转换,全部都在创建线程这一行就创建出临时对象,然后在函数参数里,用引用来接,否则还会创建出一个对象。
std::mutex
考虑到多个线程对同一个对象进行操作,若全为只读操作,则数据是安全的,若有读有写,则很容易发生错误,因此要使用互斥量(mutex)来保护数据。
互斥量就是个类对象,可以理解为一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功,如果没有锁成功,那么线程将阻塞在lock()这里不断尝试去锁定,直到lock成功。
lock()&unlock()
实例分析:
未加锁状态
#include <iostream>
#include <thread>
#include <Windows.h>
#include<mutex>
using namespace std;
int num = 0;
mutex mtx;
void thread01()
{
for (int i = 0; i < 1000000; i++)
{
// mtx.lock();
++num;
// mtx.unlock();
}
}
void thread02()
{
for (int i = 0; i < 1000000; i++)
{
// mtx.lock();
++num;
// mtx.unlock();
}
}
int main()
{
thread task01(thread01);
thread task02(thread02);
task01.join();
task02.join();
cout << "The num is " << num << endl;
system("pause");
return 0;
}
多次运行程序结果:
可以看到每次运行的结果都与预期的结果不一样,数据未加锁则会引起数据冲突。下面是加锁状态:
void thread01()
{
for (int i = 0; i < 1000000; i++)
{
mtx.lock();//加锁
++num;
mtx.unlock();//解锁
}
}
void thread02()
{
for (int i = 0; i < 1000000; i++)
{
mtx.lock();
++num;
mtx.unlock();
}
}
//其他与上面的例子相同
程序运行结果:
这次程序运行的结果是正确的,可以看出数据加锁的重要性。不过由于加锁会导致阻塞,因此保护数据多了会影响程序运行的效率。
try_lock()
try_lock()会试图去lock mutex对象,但是不会造成阻塞,通常有以下几种情况:
- 如果mutex没有被任何线程lock,则调用线程会lock
- 如果mutex被其他线程lock,则该函数会失败,返回false,但是不会造成阻塞。
- 如果mutex被与调用该函数的线程相同的线程lock,则会产生一个死锁,因为mutex不支持递归,一般情况下会崩溃。
std::lock_guard
std::lock_guard用于管理mutex,很显然对于mutex的lock()和unlock()要成对调用,如果lock()之后忘记unlock()会出现死锁或其他很严重的错误,而有时程序会有很多出口,例如break
continue
return
,有时还会有异常抛出,在这些出口前都加上unlock()很麻烦。
std::lock_guard是mutex的封装器,创建lock_guard对象时,它会试图接收给定mutex的所有权。当线程离开创建lock_guard对象所在的作用域时,lock_guard对象被自动析构并释放mutex,我们可以将上述thread01()改为:
void thread01()
{
for (int i = 0; i < 1000000; i++)
{
std::lock_guard<std::mutex> guard(mtx);//利用lock_guard管理mutex
++num;
}
}
std::unique_lock
std::unique_lock 与std::lock_guard都能实现自动加锁与解锁功能,但是std::unique_lock要比std::lock_guard更灵活,但是更灵活的代价是占用空间相对更大一点且相对更慢一点。此处不展开说明。