首页 > 编程笔记

C++11多线程编程详解

前面章节中,我们借助操作系统提供的接口实现了 C 语言多线程程序的编写。C++ 11 标准中新引入了与多线程编程相关的多个头文件,包括 <thread>、<mutex>、<future>、<condition_variable> 和 <atomic>。

当我们在 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;

注意,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,"http://c.biancheng.net");
    //阻塞主线程,等待 thread1 线程执行完毕
    thread1.join();
    //阻塞主线程,等待 thread2 线程执行完毕
    thread2.join();
    return 0;
}
程序执行结果为(不唯一):

---thread1 running
n=10
---thread2 running
url=http://c.biancheng.net

程序中,我们分别调用两种构造函数创建了两个线程,它们分别执行 threadFun1() 和 threadFun2() 函数。我们在主线程(main() 函数)中调用了 thread 类提供的 join() 成员函数,以 thread1.join() 为例,它的功能是阻塞主线程,直至 thread1 线程执行完毕后,主线程才能继续执行。

2) 线程的使用

除了 join() 成员函数外,thread 类还提供有很多实用的成员函数,表 1 给大家列出了几个最常用的函数:

表 1 thread 类的常用成员函数
成员函数 功 能
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

如果在 Windows 环境中运行,将程序中引入的 <unistd.h> 头文件改为 <Windows.h>,将第 6 行的 sleep(5); 语句改为 Sleep(5); 语句即可。

程序中创建了 2 个线程,通过调用 get_id() 成员函数分别获得了它们的线程 ID,其中 thread1 线程独立执行,thread2 线程先于主线程执行完成。通过执行结果可以看到,thread1 线程的执行结果并没有显示到屏幕上,这是因为 thread1 线程还未执行输出语句,主线程就已经执行结束(整个进程也执行结束),thread1 线程无法将执行结果输出到屏幕上。

<thread>头文件中不仅定义了 thread 类,还提供了一个名为 this_thread 的命名空间,此空间中包含一些功能实用的函数,如表 2 所示

表 2 this_thread命名空间常用函数
函数 功 能
get_id() 获得当前线程的 ID。
yield() 阻塞当前线程,直至条件成熟。
sleep_until() 阻塞当前线程,直至某个时间点为止。
sleep_for() 阻塞当前线程指定的时间(例如阻塞 5 秒)。

有关表 2 中这些函数的用法,我们不再一一举例,感兴趣的读者可查阅 C++ 函数手册。

实现线程同步

C++ 11 标准为解决“线程间抢夺公共资源”提供了多种方案,其中就包括我们前面讲到的互斥锁和条件变量。

1) 互斥锁

有关互斥锁实现线程同步的原理,这里不再赘述,您可以阅读《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;
}
程序执行结果为(不唯一):

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

程序中,访问公共变量 n 的线程有 2 个,为了避免它们之间竞争资源,我们对 threadFun() 函数中访问 n 变量的过程引入了互斥锁机制。

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 所示。

表 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

推荐阅读