在C++11之前,涉及到多线程问题,都是和平台相关的,比如Windows和Linux下有各自的接口,这使得代码的可移植性较差。C++11中最重要的特性就是对线程进行了支持,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念
一、线程库thread
1.1 线程对象的构造
调用无参构造函数
thread提供了无参构造函数,调用无参构造函数创建出来的线程对象没有关联任何线程函数,即没有启动任何线程
thread t;
由于thread提供了移动赋值函数,因此当后续需要让该线程对象与线程函数关联时,可以以带参的方式创建一个匿名对象,然后调用移动赋值将该匿名对象关联线程的状态转移给该线程对象
#include <iostream> #include <thread> using namespace std; void func(int num) { for (int i = 0; i < num; ++i) { cout << i << endl; } } int main() { thread t; //... t = thread(func, 100); t.join(); return 0; }
调用带参构造函数
template <class Fn, class... Args> explicit thread (Fn&& fn, Args&&... args);
- fn:可调用对象,比如函数指针、仿函数、lambda表达式、被包装器包装后的可调用对象等
- args...:调用可调用对象fn时所需要的若干参数。
#include <iostream> #include <thread> using namespace std; void func(int num) { for (int i = 0; i < num; i++) { cout << i << endl; } } int main() { thread t(func, 10); t.join(); return 0; }
调用移动构造函数
thread提供了移动构造函数,能够用一个右值线程对象来构造一个线程对象
#include <iostream> #include <thread> using namespace std; void func(int num) { for (int i = 0; i < num; ++i) { cout << i << endl; } } int main() { thread t = thread(func, 10); t.join(); return 0; }
注意:
线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态
若创建线程对象时没有提供线程函数,那么该线程对象实际没有对应任何线程。
若创建线程对象时提供了线程函数,那么就会启动一个线程来执行这个线程函数,该线程与主线程一起运行
thread类是防拷贝的,不允许拷贝构造和拷贝赋值,但是可以移动构造和移动赋值,可以将一个线程对象关联线程的状态转移给其他线程对象,并且转移期间不影响线程的执行
1.2 thread类的成员函数
thread类中常用的成员函数如下:
joinable()函数还可以用于判定线程是否是有效的,若是以下任意情况,则线程无效:
采用无参构造函数构造的线程对象(该线程对象没有关联任何线程)
线程对象的状态已经转移给其他线程对象(已经将线程交给其他线程对象管理)
线程已经调用join或detach结束(线程已经结束)
启动一个线程后,当这个线程退出时,需要对该线程所使用的资源进行回收,否则可能会导致内存泄露等问题。thread库提供了两种回收线程资源的方式:
join方式
主线程创建新线程后,可以调用join()函数等待新线程终止,当新线程终止时join()函数就会自动清理线程相关的资源。join()函数清理线程的相关资源后,thread对象与已销毁的线程就没有关系了,因此一个线程对象一般只会使用一次join(),否则程序会崩溃
#include <iostream> #include <thread> using namespace std; void func(int n) { for (int i = 0; i < n; i++) { cout << i << endl; } } int main() { thread t(func, 10); t.join(); t.join(); //程序崩溃 return 0; }
但如果一个线程对象join后,又调用移动赋值函数,将一个右值线程对象的关联线程的状态转移过来了,那么这个线程对象又可调用一次join
#include <iostream> #include <thread> using namespace std; void func(int n) { for (int i = 0; i < n; i++) { cout << i << endl; } } int main() { thread t(func, 10); t.join(); t = thread(func, 10); t.join(); return 0; }
但采用join的方式结束线程,在某些场景下也可能会出现问题。如在该线程被join前,若中途因为某些原因导致程序不再执行后续代码,这时这个线程将不会被join
#include <iostream> #include <thread> #include <windows.h> using namespace std; void func(int num) { for (int i = 0; i < num; i++) { cout << i << endl; } } bool DoSomething(){ return false; } int main() { thread t(func, 10); Sleep(3); if (!DoSomething()) return -1; t.join(); //不会被执行 return 0; }
因此采用join方式结束线程时,join()函数的调用位置非常关键,为了避免上述问题,可以采用RAII的方式对线程对象进行封装,即利用对象的生命周期来控制线程资源的释放
#include <iostream> #include <thread> #include <windows.h> using namespace std; class Thread { public: Thread(thread& t):_thread(t) {} ~Thread() { if (_thread.joinable()) _thread.join(); } private: //防拷贝 Thread(const Thread&) = delete; Thread& operator=(const Thread&) = delete; private: thread& _thread; }; void func(int num) { for (int i = 0; i < num; i++) { cout << i << endl; } } bool DoSomething(){ return false; } int main() { thread t(func, 10); Thread T(t); Sleep(3); if (!DoSomething()) return 1; return 0; }
每当创建一个线程对象后,就用Thread类对其进行封装产生一个Thread对象。
当Thread对象生命周期结束时就会调用析构函数,在析构中会通过joinable()判断这个线程是否需要被join,若需要那么就会调用join对其该线程进行等待
detach方式
主线程创建新线程后,也可以调用detach函数将新线程与主线程进行分离,分离后新线程会在后台运行,其所有权和控制权将会交给C++运行库,此时C++运行库会保证当线程退出时,其相关资源能够被正确回收
使用detach的方式回收线程的资源,一般在线程对象创建好之后就立即调用detach函数。
否则线程对象可能会因为某些原因,在后续调用detach函数分离线程之前被销毁掉,这时就会导致程序崩溃。
因为当线程对象被销毁时会调用thread的析构函数,而在thread的析构函数中会通过joinable判断这个线程是否需要被join,如果需要那么就会调用terminate终止当前程序(程序崩溃)
1.3 this_thread类
函数名 | 功能 |
get_id | 获取当前线程ID |
yeild | 当前线程出让时间片,CPU调度其他时间片 |
sleep_until | 使调用线程休眠到一个固定时间(绝对时间) |
sleep_for | 使调用线程休眠一个时间段(相对时间) |
当想获取线程ID时,可以通过线程对象的get_id()接口,但想在与线程关联的线程函数中获取线程ID,这个办法就行不通了,可以调用this_thread类中的接口get_id()
#include <iostream> #include <thread> using namespace std; void func() { cout << this_thread::get_id() << endl; } int main() { thread t(func); t.join(); return 0; }
1.4 线程函数的参数问题
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,就算线程函数的参数为引用类型,在线程函数中修改后也不会影响到外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参
若要通过线程函数的形参改变外部的实参,可以参考以下三种方法:
方法一:借助std::ref()
当线程函数的参数类型为引用类型时,若要想线程函数形参引用的是外部传入的实参,而不是线程栈空间中的拷贝,那么在传入实参时需要借助ref()函数保持对实参的引用
#include <iostream> #include <thread> using namespace std; void add(int& num) { num++; } int main() { int num = 0; thread t(add, ref(num)); t.join(); cout << num << endl; return 0; }
方法二:指针地址
将线程函数的参数类型改为指针类型,将实参的地址传入线程函数,此时在线程函数中可以通过修改该地址处的变量,进而影响到外部实参
#include <iostream> #include <thread> using namespace std; void add(int* num) { ++(*num); } int main() { int num = 0; thread t(add, &num); t.join(); cout << num << endl; return 0; }
方法三:lambda表达式
将lambda表达式作为线程函数,利用lambda函数的捕捉列表,以引用的方式对外部实参进行捕捉,此时在lambda表达式中对形参的修改也能影响到外部实参
#include <iostream> #include <thread> using namespace std; int main() { int num = 0; thread t([&num](){ ++num; }); t.join(); cout << num << endl; return 0; }