《C++ thread类》
1. thread类
thread类
是C++中表示执行线程
的类,位于头文件<thread>
中。我们创建一个thread对象就会立即执行一个对应的线程。通过thread类的成员函数,我们可以标识线程或对线程进行控制。
1.1 启动线程
我们构造一个thread类的对象,就会立即执行一个与该thread对象对应的线程。通常我们会向thread类的构造函数传递一个可调用对象
,新创建的线程就会执行这个可调用对象。可调用对象可以是函数
、函数指针
、lambda表达式
或function对象
。
#include <iostream> #include <functional> #include <thread> using namespace std; void func(void) { cout << "hello world" << endl; } int main(void) { void (*func_ptr)(void) = func; function<void(void)> func_obj = func; thread t1(func); thread t2(func_ptr); thread t3(func_obj); thread t4([] { cout << "hello world" << endl; }); t1.join(); t2.join(); t3.join(); t4.join(); return 0; }
在上面的例子中,我们分别向thread类的构造函数传递了函数、函数指针、function对象和lambda表达式。
传递参数:
如果我们想要想可调用对象传递参数,只需要将参数和可调用对象一并放入thread类的构造函数的参数列表中即可:
#include <iostream> #include <functional> #include <thread> using namespace std; void add(int n1, int n2) { cout << n1 << " + " << n2 << " = " << n1 + n2 << endl; } int main(void) { thread t1(add, 2, 3); thread t2([](int n1, int n2) { cout << n1 << " + " << n2 << " = " << n1 + n2 << endl; }, 5, 6); t1.join(); t2.join(); return 0; }
在以上的例子中,我们向thread类传入可调用对象和两个参数,可调用对象在执行时输出两个参数的和。
thread类的构造函数可以接受任意多的额外的参数,只需要传入的参数与可调用对象的参数列表一一对应即可。
1.2 连接线程与分离线程
连接线程:
join成员函数的作用是等待线程完成。请读者试想一下,假如我们在main函数中创建了一个线程对象A,线程A在被创建之后执行了一个非常耗时的任务。而main函数(主线程)在创建完线程A后执行return 0,整个进程就会被终止,此时线程A即使没有执行完成也会被终止,程序的只想结果往往就不是我们所期望的了。所以我们需要在主线程中调用join成员函数来等线程执行完成,这个操作也叫连接线程。当我们调用join时,如果线程尚未执行完成,就对阻塞调用join的线程直至线程执行完成。若干在调用join时线程已经执行完成,则join会立即返回,不会阻塞调用线程。
分离线程:
如果我们不需要等待线程完成,可以调用thread对象的detach
成员函数分离线程,之后这个线程就可以独立运行,不需要我们调用join等待它执行完成。分离线程通常用于执行一些后台任务的线程。
其中连接线程执行完成后,需要用join来释放该完成线程的资源。分离线程执行完成后,资源就自动释放。
可连接线程与不可连接线程:
如果一个thread对象是符合以下任意一种情况,它将是不可连接的:
1.默认构造的thread对象。
2.该thread对象已经被移动到另一个thread对象。
3.thread对象已经被连接或分离。
不能对不可连接的线程对象调用join。
1.5 thread类的析构函数
当一个thread对象被析构时,如果该thread对象对应的线程还没有执行完成,线程仍然会继续执行
,不会因为thread对象被析构而停止执行。
如果一个线程是可连接的,则必须在thread对象被析构前调用join,否则析构函数会调用std::terminate
终止程序。
1.6 线程标识
在C++中用thread::id类
来标识线程。对于可连接线程
,可以通过get_id
成员函数获得标识该thread对象的thread::id对象,每个thread对象的id唯一。对于不可连接线程
,调用get_id会返回默认构造的thread_id对象
,所有不可连接线程的id相等。
thread::id类重载了一下运算符用于thread::id对象的相等性比较:
bool operator== (thread::id lhs, thread::id rhs) noexcept; bool operator!= (thread::id lhs, thread::id rhs) noexcept; bool operator< (thread::id lhs, thread::id rhs) noexcept; bool operator<= (thread::id lhs, thread::id rhs) noexcept; bool operator> (thread::id lhs, thread::id rhs) noexcept; bool operator>= (thread::id lhs, thread::id rhs) noexcept;
thread::id还重载了<<运算符
用于向输出流中输出thread::id:
template <class charT, class traits> basic_ostream<chasrT, traits>& operator<< (basic_ostream<charT,traits>& os, thread::id id);
1.7 this_thread命名空间
this_thread命名空间提供了访问当前线程的一些函数,除了上文提到的get_id
外,还有yield
、sleep_until
和sleep_for
。
yield:
调用yield函数可以让出当前线程,让操作系统调度同一进程的其他线程。
sleep_until:
sleep_until
可以阻塞调用线程直至某个时间点。函数原型为:
template <class Clock, class Duration> void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);
sleep_for:
sleep_for
可以在制定的时间跨度内阻塞线程的执行。函数原型为:
template <class Rep, class Period> void sleep_for (const chrono::duration<Rep,Period>& rel_time);
2.mutex和std::lock_guard的使用
头文件是#include <mutex>,mutex是用来保证线程同步的,防止不同的线程同时操作同一个共享数据。
但使用lock_guard则相对安全,它是基于作用域的,能够自解锁,当该对象创建时,它会像m.lock()一样获得互斥锁,当生命周期结束时,它会自动析构(unlock),不会因为某个线程异常退出而影响其他线程。示例:
#include <iostream> #include <thread> #include <mutex> #include <stdlib.h> int cnt = 20; std::mutex m; void t1() { while (cnt > 0) { std::lock_guard<std::mutex> lockGuard(m); // std::m.lock(); if (cnt > 0) { //sleep(1); --cnt; std::cout << cnt << std::endl; } // std::m.unlock(); } } void t2() { while (cnt > 0) { std::lock_guard<std::mutex> lockGuard(m); // std::m.lock(); if (cnt > 0) { --cnt; std::cout << cnt << std::endl; } // std::m.unlock(); } } int main(void) { std::thread th1(t1); std::thread th2(t2); th1.join(); //等待t1退出 th2.join(); //等待t2退出 std::cout << "here is the main()" << std::endl; return 0; }
因为lock_guard是基于作用域的,因此可以通过{}(花括号可以看作是作用域标识符,表示某些变量的作用域,决定他们的生命周期),来定义临界区的大小。
这边举个花括号决定作用域的例子:
void fun()
{ int a; { std::lock_guard<std::mutex> lockGuard(m); int b; a=0;//正确,a在作用域内 } b=0; //错误,b的生命周期在上个{}结束了 }