前面章节中,我们借助操作系统提供的接口实现了 C 语言多线程程序的编写。C++ 11 标准中新引入了与多线程编程相关的多个头文件,包括 <thread>、<mutex>、<future>、<condition_variable> 和 <atomic>。
当我们在 Linux 环境中编写 C++ 的多线程程序时,既可以借助 POSIX 标准提供的 <pthread.h> 实现,也可以借助 C++11 标准提供的头文件实现。本节,我们就给大家详细地讲解如何利用 C++11 标准编写多线程程序。
C++11 标准中,<thread>头文件提供了 thread 类(位于 std 命令空间中),专门用来完成线程的创建和使用。
一个线程可以用 thread 类的对象来表示,thread类中重载了多种构造函数,最常用的有以下两个:
注意,thread 类只提供了移动构造函数,未提供拷贝构造函数。这意味着,我们不能直接将一个事先定义好的 thread 对象赋值给另一个 thread 对象,但可以将临时的(匿名的)thread 对象赋值给另一个 thread 对象。有关移动构造函数,读者可阅读《C++11移动构造函数详解》一文做详细了解。
POSIX 标准中,线程所执行函数的参数和返回值都必须为 void* 类型。而 thread 类创建的线程可以执行任意的函数,即不对函数的参数和返回值做具体限定。
举个例子:
#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,"https://www.cdsy.xyz");
//阻塞主线程,等待 thread1 线程执行完毕
thread1.join();
//阻塞主线程,等待 thread2 线程执行完毕
thread2.join();
return 0;
}
程序执行结果为(不唯一):
程序中,我们分别调用两种构造函数创建了两个线程,它们分别执行 threadFun1() 和 threadFun2() 函数。我们在主线程(main() 函数)中调用了 thread 类提供的 join() 成员函数,以 thread1.join() 为例,它的功能是阻塞主线程,直至 thread1 线程执行完毕后,主线程才能继续执行。
除了 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, "https://www.cdsy.xyz");
//输出 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 文件中,执行过程如下:
如果在 Windows 环境中运行,将程序中引入的 <unistd.h> 头文件改为 <Windows.h>,将第 6 行的 sleep(5); 语句改为 Sleep(5); 语句即可。
程序中创建了 2 个线程,通过调用 get_id() 成员函数分别获得了它们的线程 ID,其中 thread1 线程独立执行,thread2 线程先于主线程执行完成。通过执行结果可以看到,thread1 线程的执行结果并没有显示到屏幕上,这是因为 thread1 线程还未执行输出语句,主线程就已经执行结束(整个进程也执行结束),thread1 线程无法将执行结果输出到屏幕上。
<thread>头文件中不仅定义了 thread 类,还提供了一个名为 this_thread 的命名空间,此空间中包含一些功能实用的函数,如表 2 所示
函数 | 功 能 |
---|---|
get_id() | 获得当前线程的 ID。 |
yield() | 阻塞当前线程,直至条件成熟。 |
sleep_until() | 阻塞当前线程,直至某个时间点为止。 |
sleep_for() | 阻塞当前线程指定的时间(例如阻塞 5 秒)。 |
有关表 2 中这些函数的用法,我们不再一一举例,感兴趣的读者可查阅 C++ 函数手册。
C++ 11 标准为解决“线程间抢夺公共资源”提供了多种方案,其中就包括我们前面讲到的互斥锁和条件变量。
有关互斥锁实现线程同步的原理,这里不再赘述,您可以阅读《Linux互斥锁实现线程同步》一文做详细了解。
考虑到不同场景的需要,C++ 11 标准提供有多种互斥锁,比如递归互斥锁、定时互斥锁,自动“加锁”和“解锁”的互斥锁等。本节我们以普通的互斥锁为例,给大家讲解互斥锁的基本用法。
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;
}
程序执行结果为(不唯一):
程序中,访问公共变量 n 的线程有 2 个,为了避免它们之间竞争资源,我们对 threadFun() 函数中访问 n 变量的过程引入了互斥锁机制。
有关条件变量实现线程同步的原理,这里不再赘述,您可以阅读《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;
}
执行结果为: