跳转至

6

对标准库的扩充:语言级线程支持*

编译本节代码需要使用 -pthread 选项。

std::thread*

std::thread 用于创建一个执行的线程示例,所以它是一切并发编程的基础,使用时需要包含 <thread> 头文件,它提供了很多基本的线程操作,例如 getid() 来获取所创建线程的线程 ID。

#include <iostream>
#include <thread>
void foo() {
    std::cout << "hello world" << std::endl;
}
int main() {
    std::thread t(foo);
    t.join();
    return 0;
}

std::mutex, std::unique_lock*

并发技术,mutex 就是其中的核心之一。C++11 引入了 mutex 相关的类,在 <mutex> 中。

std::mutex 是 C++11 中最基本的 mutex 类,通过实例化 std::mutex 可以创建互斥量,而通过其成员函数 lock() 上锁,unlock() 解锁。但是在实际编写代码过程中,最好不去直接调用成员函数,因为调用成员函数就需要在每个临界区的出口处调用 unlock(),还包括异常。C++11 还为互斥量提供了一个 RAII 语法的模板类 std::lock_guard。RAII 很好地保证了代码的异常安全性。

在 RAII 用法下,对于临界区的互斥量的创建只需要在作用域的开始部分:

void some_operation(const std::string &message) {
    static std::mutex mutex;
    std::lock_guard<std::mutex> lock(mutex);
    // ...操作
    // 当离开这个作用域的时候,互斥锁会被析构,同时unlock互斥锁
    // 因此这个函数内部的可以认为是临界区
}

由于 C++ 保证了所有栈对象在声明周期结束时会被销毁,所有这样的代码也是异常安全的。无论 some_operation() 正常返回、还是在中途抛出异常,都会引发堆栈回退,也就自动调用了 unlock()

std::unique_lock 则相对于 std::lock_guard 出现的,更加灵活,std::unique_lock 的对象会以独占所有权(没有其他的 unique_lock 对象同时拥有某个 mutex 对象的所有权)的方式管理 mutex 对象的上锁和解锁操作。因此并发编程中,推荐使用 std::unique_lock

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void block_area() {
    std::unique_lock<std::mutex> lock(mtx);
    //...临界区
}
int main() {
    std::thread thd1(block_area);

    thd1.join();

    return 0;
}

join() 函数是一个等待线程完成函数,主线程需要等待子线程运行结束了才继续。

detach() 函数是子线程的分离函数,调用后线程就被分离到后台运行,主线程不需要子线程结束才结束。

std::future, std::packaged_task*

std::future 则是提供了一个访问异步操作结果的途径。首先理解一下 C++11 之前的多线程行为。如果主线程 A 希望新开辟一个线程 B 去执行某个任务,并返回一个结果。这时候,线程 A 可能正在忙其他的事情,无暇顾及 B 的结果,所以我们会很自然的希望能够在某个特定的事件获得线程 B 的结果。

C++11 的 std::future 被引入之前,通常的做法是:创建一个线程 A,在线程 A 里启动任务 B,当准备完毕后发生一个事件,并将结果保存在全局变量中。而主函数线程 A 里正在做其他的事情,当需要结果时,调用一个线程等待函数来获得执行的结果。

C++11 提供的 std::future 简化了这个流程,可以用来获取异步任务的结果。很容易想到把它作为一种简单的线程同步手段。

此外,std::packaged_task 可以用来封装任何可以调用的目标,从而用于实现异步的调用。

#include <iostream>
#include <future>
#include <thread>

int main() {
    // 将一个返回值为7的 lambda 表达式封装到 task 中
    // std::packaged_task 的模板参数为要封装函数的类型
    std::packaged_task<int()> task([](){return 7;});
    // 获得 task 的 future
    std::future<int> result = task.get_future();    // 在一个线程中执行 task
    std::thread(std::move(task)).detach();    
    std::cout << "Waiting...";
    result.wait();
    // 输出执行结果
    std::cout << "Done!" << std:: endl << "Result is " << result.get() << '\n';
}

在封装好要调用的目标后,可以使用 get_future() 来获得一个 std::future 对象,以便之后实现线程同步。

std::condition_variable*

std::condition_variable 是为了解决死锁而生的。当互斥操作不够用时引入。condition_variable 实例被创建主要就是用于唤醒等待线程从而避免死锁。std::condition_variablenotify_one() 用于唤醒一个线程;notify_all() 则是通知所有线程。以下是一个生产者消费者的例子:

#include <condition_variable>
#include <mutex>
#include <thread>
#include <iostream>
#include <queue>
#include <chrono>

int main()
{
    // 生产者数量
    std::queue<int> produced_nums;
    // 互斥锁
    std::mutex m;
    // 条件变量
    std::condition_variable cond_var;
    // 结束标志
    bool done = false;
    // 通知标志
    bool notified = false;

    // 生产者线程
    std::thread producer([&]() {
        for (int i = 0; i < 5; ++i) {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            // 创建互斥锁
            std::unique_lock<std::mutex> lock(m);
            std::cout << "producing " << i << '\n';
            produced_nums.push(i);
            notified = true;
            // 通知一个线程
            cond_var.notify_one();
        }   
        done = true;
        cond_var.notify_one();
    }); 

    // 消费者线程
    std::thread consumer([&]() {
        std::unique_lock<std::mutex> lock(m);
        while (!done) {
            while (!notified) {  // 循环避免虚假唤醒
                cond_var.wait(lock);
            }   
            while (!produced_nums.empty()) {
                std::cout << "consuming " << produced_nums.front() << '\n';
                produced_nums.pop();
            }   
            notified = false;
        }   
    }); 

    producer.join();
    consumer.join();
}

总结*

C++11 语言层提供了并发编程的支持,本节提到内容可以让我们使用不超过 100 行代码编写一个简单的线程池库。


最后更新: November 26, 2020