C++11多线程编程详解
当我们在 Linux 环境中编写 C++ 的多线程程序时,既可以借助 POSIX 标准提供的 <pthread.h> 实现,也可以借助 C++11 标准提供的头文件实现。本节,我们就给大家详细地讲解如何利用 C++11 标准编写多线程程序。
线程的创建和使用
C++11 标准中,<thread>
头文件提供了 thread 类(位于 std 命令空间中),专门用来完成线程的创建和使用。
1) 创建线程
一个线程可以用 thread 类的对象来表示,thread类中重载了多种构造函数,最常用的有以下两个://1、Fn 表示线程要执行的函数,args 表示向 Fn 传递的多个参数,此构造函数支持泛型 template <class Fn, class... Args> explicit thread (Fn&& fn, Args&&... args); //2、移动构造函数 thread (thread&& x) noexcept;
POSIX 标准中,线程所执行函数的参数和返回值都必须为 void* 类型。而 thread 类创建的线程可以执行任意的函数,即不对函数的参数和返回值做具体限定。注意,thread 类只提供了移动构造函数,未提供拷贝构造函数。这意味着,我们不能直接将一个事先定义好的 thread 对象赋值给另一个 thread 对象,但可以将临时的(匿名的)thread 对象赋值给另一个 thread 对象。有关移动构造函数,读者可阅读《C++11移动构造函数详解》一文做详细了解。
举个例子:
#include <iostream> #include <thread> using namespace std; void threadFun1(int n) { cout << "---thread1 running\n"; cout << "n=" << n << endl; } void threadFun2(const char * url) { cout << "---thread2 running\n"; cout << "url=" << url << endl; } int main() { //调用第 1 种构造函数 thread thread1(threadFun1,10); //调用移动构造函数 thread thread2 = std::thread(threadFun2,"http://c.biancheng.net"); //阻塞主线程,等待 thread1 线程执行完毕 thread1.join(); //阻塞主线程,等待 thread2 线程执行完毕 thread2.join(); return 0; }程序执行结果为(不唯一):
---thread1 running
n=10
---thread2 running
url=http://c.biancheng.net
2) 线程的使用
除了 join() 成员函数外,thread 类还提供有很多实用的成员函数,表 1 给大家列出了几个最常用的函数:成员函数 | 功 能 |
---|---|
get_id() | 获取当前 thread 对象的线程 ID。 |
joinable() | 判断当前线程是否支持调用 join() 成员函数。 |
join() | 阻塞当前 thread 对象所在的线程,直至 thread 对象表示的线程执行完毕后,所在线程才能继续执行。 |
detach() | 将当前线程从调用该函数的线程中分离出去,它们彼此独立执行。 |
swap() | 交换两个线程的状态。 |
注意,每个thread 对象在调用析构函数销毁前,要么调用 join() 函数令主线程等待子线程执行完成,要么调用 detach() 函数将子线程和主线程分离,两者比选其一,否则程序可能存在以下两个问题:
- 线程占用的资源将无法全部释放,造成内存泄漏;
- 当主线程执行完成而子线程未执行完时,程序执行将引发异常。
举个例子:
#include <iostream> #include <thread> #include <unistd.h> using namespace std; void threadFun1(int n) { sleep(5); cout << "---thread1 running\n"; cout << "n=" << n << endl; } void threadFun2(const char * url) { cout << "---thread2 running\n"; cout << "url=" << url << endl; } int main() { //调用第 1 种构造函数 thread thread1(threadFun1, 10); //输出 thread1 线程的 ID cout << "thread1 ID:" << thread1.get_id() << endl; //调用移动构造函数 thread thread2 = std::thread(threadFun2, "http://c.biancheng.net"); //输出 thread2 线程的 ID cout << "thread2 ID:" << thread2.get_id() << endl; //将 thread1 与主线程分离开,thread1 线程独立执行。 thread1.detach(); //判断 thread2 线程是否可以调用 join() 函数 if (thread2.joinable()) { //阻塞主线程,直至 thread2 线程执行完毕。 thread2.join(); } cout << "main finished" << endl; return 0; }假设程序编写在 thread.cpp 文件中,执行过程如下:
[root@localhost ~]# g++ thread.cpp -o thread.exe -std=c++11 -lpthread
[root@localhost ~]# ./thread.exe
thread1 ID:140278776624896
thread2 ID:140278768232192
---thread2 running
url=http://c.biancheng.net
main finished
程序中创建了 2 个线程,通过调用 get_id() 成员函数分别获得了它们的线程 ID,其中 thread1 线程独立执行,thread2 线程先于主线程执行完成。通过执行结果可以看到,thread1 线程的执行结果并没有显示到屏幕上,这是因为 thread1 线程还未执行输出语句,主线程就已经执行结束(整个进程也执行结束),thread1 线程无法将执行结果输出到屏幕上。如果在 Windows 环境中运行,将程序中引入的 <unistd.h> 头文件改为 <Windows.h>,将第 6 行的 sleep(5); 语句改为 Sleep(5); 语句即可。
<thread>
头文件中不仅定义了 thread 类,还提供了一个名为 this_thread 的命名空间,此空间中包含一些功能实用的函数,如表 2 所示函数 | 功 能 |
---|---|
get_id() | 获得当前线程的 ID。 |
yield() | 阻塞当前线程,直至条件成熟。 |
sleep_until() | 阻塞当前线程,直至某个时间点为止。 |
sleep_for() | 阻塞当前线程指定的时间(例如阻塞 5 秒)。 |
有关表 2 中这些函数的用法,我们不再一一举例,感兴趣的读者可查阅 C++ 函数手册。
实现线程同步
C++ 11 标准为解决“线程间抢夺公共资源”提供了多种方案,其中就包括我们前面讲到的互斥锁和条件变量。1) 互斥锁
考虑到不同场景的需要,C++ 11 标准提供有多种互斥锁,比如递归互斥锁、定时互斥锁,自动“加锁”和“解锁”的互斥锁等。本节我们以普通的互斥锁为例,给大家讲解互斥锁的基本用法。有关互斥锁实现线程同步的原理,这里不再赘述,您可以阅读《Linux互斥锁实现线程同步》一文做详细了解。
C++11标准规定,互斥锁用 mutex 类(位于 std 命名空间中)的对象表示,该类定义在
<mutex>
头文件中。mutex 类提供有 lock() 和 unlock() 成员函数,分别完成“加锁”和“解锁”功能。举个例子:
#include <mutex> // std::mutex #include <chrono> // std::chrono::seconds() using namespace std; int n = 0; std::mutex mtx; // 定义一个 mutex 类对象,创建一个互斥锁 void threadFun() { while(n<10){ //对互斥锁进行“加锁” mtx.lock(); n++; cout << "ID" << std::this_thread::get_id() << " n = "<< n << endl; //对互斥锁进行“解锁” mtx.unlock(); //暂停 1 秒 std::this_thread::sleep_for(std::chrono::seconds(1)); } } int main() { thread th1(threadFun); thread th2(threadFun); th1.join(); th2.join(); return 0; }程序执行结果为(不唯一):
ID16064 n = 1
ID1956 n = 2
ID16064 n = 3
ID1956 n = 4
ID16064 n = 5
ID1956 n = 6
ID16064 n = 7
ID1956 n = 8
ID16064 n = 9
ID1956 n = 10
2) 条件变量
有关条件变量实现线程同步的原理,这里不再赘述,您可以阅读《Linux条件变量实现线程同步》一文做详细了解。C++ 11标准提供了两种表示条件变量的类,分别是 condition_variable 和 condition_variable_any,它们都定义在
<condition_variable>
头文件中。我们知道,为了避免线程间抢夺资源,条件变量通常和互斥锁搭配使用,condition_variable 类表示的条件变量只能和 unique_lock 类表示的互斥锁(可自行加锁和解锁)搭配使用,而 condition_variable_any 类表示的条件变量可以和任意类型的互斥锁搭配使用(例如递归互斥锁、定时互斥锁等)。这里我们以 condition_variable_any 为例,给大家讲解 C++11 标准中条件变量的基本用法。每个 condition_variable_any 类的对象都表示一个条件变量,该类提供的成员函数如表 3 所示。
成员函数 | 功 能 |
---|---|
wait() | 阻塞当前线程,等待条件成立。 |
wait_for() | 阻塞当前线程的过程中,该函数会自动调用 unlock() 函数解锁互斥锁,从而令其他线程使用公共资源。当条件成立或者超过了指定的等待时间(比如 3 秒),该函数会自动调用 lock() 函数对互斥锁加锁,同时令线程继续执行。 |
wait_until() | 和 wait_for() 功能类似,不同之处在于,wait_until() 函数可以设定一个具体时间点(例如 2021年4月8日 的某个具体时间),当条件成立或者等待时间超过了指定的时间点,函数会自动对互斥锁加锁,同时线程继续执行。 |
notify_one() | 向其中一个正在等待的线程发送“条件成立”的信号。 |
notify_all() | 向所有等待的线程发送“条件成立”的信号。 |
举个例子:
#include <iostream> #include <thread> // std::thread #include <mutex> // std::mutex, std::unique_lock #include <condition_variable> // std::condition_variable_any #include <chrono> // std::chrono::seconds() //创建一个互斥锁 std::mutex mtx; //创建一个条件变量 std::condition_variable_any cond; void print_id() { mtx.lock(); //阻塞线程,直至条件成立 cond.wait(mtx); std::cout << "----threadID " << std::this_thread::get_id() <<" run" << std::endl; //等待 2 秒 std::this_thread::sleep_for(std::chrono::seconds(2)); mtx.unlock(); } void go() { std::cout << "go running\n"; //阻塞线程 2 秒钟 std::this_thread::sleep_for(std::chrono::seconds(2)); //通知所有等待的线程条件成立 cond.notify_all(); } int main() { //创建 4 个线程执行 print_id() 函数 std::thread threads[4]; for (int i = 0; i < 4; ++i) threads[i] = std::thread(print_id); //创建 1 个线程执行 go() 函数 std::thread goThread(go); //等待所有线程执行结果后,主线程才能继续执行 goThread.join(); for (auto& th : threads) { th.join(); } return 0; }执行结果为:
go running
----threadID 11416 run
----threadID 18696 run
----threadID 11268 run
----threadID 16824 run