线程同步基础

并发场景下,有时我们并不仅仅想保护数据,我们还希望多个线程之间同步某些操作,例如等待某个条件为真或者某个事件发生时执行一些操作。C++标准库提供了条件变量(condition variables)和futures;并发技术规范(Concurrency Technical Specification (TS))提供了latches和barriers。

条件变量

标准库中提供两种条件变量:std::condition_variable和std::condition_variable_any,它们都必须与互斥锁配合使用,以提供同步功能,而前者必须与std::mutex使用,后者可以与满足互斥锁最小要求的互斥锁类配合使用,由于通用性,其性能和开销会较大。

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

std::mutex mut;
std::queue<int> data_queue;
std::condition_variable data_cond;

int prepare_data()
{
    return rand() % 100;
}

void process(int data)
{
    std::cout << "process data: " << data << " remain: " << data_queue.size() << std::endl;
}

bool is_last_chunk(int data)
{
    return data == 0;
}

void data_preparation_thread()
{
    //srand仅对当前线程有效
    srand((unsigned int)time(nullptr));
    while (true)
    {
        const int data = prepare_data();
        {
            std::lock_guard<std::mutex> lk(mut);//1
            data_queue.push(data);
        }//3
        data_cond.notify_one();//4
        if (is_last_chunk(data))
            break;
    }
}
void data_processing_thread()
{
    while (true)
    {
        std::unique_lock<std::mutex> lk(mut);//2
        data_cond.wait(
            lk, []
            { return !data_queue.empty(); });
        int data = data_queue.front();
        data_queue.pop();
        lk.unlock();
        process(data);//5
        if (is_last_chunk(data))
            break;
    }
}

int main()
{
    std::thread thread1(data_preparation_thread);
    std::thread thread2(data_processing_thread);
    thread1.join();
    thread2.join();
}

这里模拟了一个线程生产数据,另一个线程取数据的场景,生产的数据是一系列数组,0代表数据结束标记。
显然取数据和放数据都要先获取互斥锁(1,2)。对生产数据线程直接使用lock_guard最方便,我们通过一个单独的作用域使得在3位置互斥锁被释放。push数据后通过条件变量的notify_one方法通知等待当前条件的线程,
对处理数据线程,我们使用了unique_lock而不是lock_guard,这是因为unique_lock可以手动unlock和lock,这不仅有利于提高效率(处理数据时没必要持有锁5),也是条件变量wait方法的要求:wait方法传入两个参数,第一个参数是一个unique_lock,第二个参数是可调用对象,表示等待的条件。进行条件判断前先获取互斥锁,然后执行可调用对象,如果返回false,则释放锁,继续等待;如果返回true,继续持有锁,直到unlock或者离开作用域自动释放锁。所谓“等待”是指线程进入阻塞或者等待状态。当其他线程在相同条件变量调用了notify_one,等待的线程再次进入获取锁并判断是否满足条件的流程,不满足则继续等待,满足则往下执行。
注意,因为条件检测可能执行很多次,因此传入的函数或可调用对象不能有副作用

通过运行程序,发现并非每次notify_one都会使得等待线程苏醒,获取这是因为notify_one的频率过快?或者跟时间片有关?或者出于效率考虑?参考输出如下:

process data: 92 remain: 12
process data: 41 remain: 25
process data: 93 remain: 24
process data: 95 remain: 23
process data: 39 remain: 22
process data: 68 remain: 21
process data: 82 remain: 20
process data: 74 remain: 19
process data: 95 remain: 18
process data: 38 remain: 17
process data: 26 remain: 16
process data: 81 remain: 15
process data: 98 remain: 14
process data: 39 remain: 13
process data: 95 remain: 12
process data: 7 remain: 11
process data: 32 remain: 10
process data: 48 remain: 9
process data: 48 remain: 8
process data: 20 remain: 7
process data: 89 remain: 6
process data: 12 remain: 5
process data: 20 remain: 4
process data: 43 remain: 3
process data: 27 remain: 2
process data: 9 remain: 1
process data: 0 remain: 0