《C++并发编程实战》笔记(四)

四、并发同步

如果一个线程需要等待另一个线程完成某个操作后再执行,可以使用互斥量的方法实现。C++提供了一些更加简单的方式,以满足线程间的同步执行。

4.1 条件变量

条件变量存在于头文件 #include <condition_variable>中,通过和std::mutex配合使用,实现一个线程根据特定条件阻塞等待另一个线程。

条件变量使用下列两种成员函数实现线程的同步:

  1. 阻塞等待指定条件的成员函数wait

    void wait(std::unique_lock<std::mutex>& lock)  // 单形参的重载
    
    // 使用方法
    while (!some_condition()) {
      wait(lock);
    }
    
    • 调用单形参的wait时接受已加锁的lock,内部调用lock.unlock()后阻塞等待,当其他线程调用对象的notify_one()notify_all()时,内部调用lock.lock()后执行后续代码
    • 阻塞期间可能会因为伪唤醒而调用lock.lock()并执行后续代码,因此通常使用下面的重载版本,使得伪唤醒时根据判断条件决定是否跳出阻塞执行后续代码
    void wait(std::unique_lock<std::mutex>& lock,  // 已加锁的 unique_lock 对象
              Predicate pred);                     // 用于判断条件是否成立的函数
    
    • wait接受的unique_lock已加锁的,运行时先调用pred函数获取返回值
      • 如果返回true,直接去执行后续代码
      • 如果返回false,则调用lock.unlock()解锁互斥量,阻塞等待
    • 当有线程调用对象的notify_one()notify_all()时,阻塞等待的wait被唤醒,先调用lock.lock()加锁,再调用pred获取返回值
      • 如果返回true,去执行后续代码
      • 如果返回false,调用lock.unlock(),继续阻塞等待

    wait执行时有两个需要注意的:

    • 由于伪唤醒的次数和频率都是不确定的,如果pred函数的多次执行会对程序有副作用,应该通过其他方式避免
    • wait函数返回后,会继续执行后续的代码,此时lock仍然是已加锁状态
  2. 用于通知其他线程等待的条件已满足的成员函数

    void notify_one();   // 唤醒一个正在阻塞的 wait
    void notify_all();   // 唤醒所有正在阻塞的 wait
    

下面是使用条件变量的一个例子:

std::condition_variable cv; // 条件变量
std::mutex cv_m;            // 互斥量

int i = 0;

void waits() {
    std::unique_lock<std::mutex> lk(cv_m);
    std::cerr << "Waiting... \n";
    cv.wait(lk, []{ return i == 1; });
    std::cerr << "...finished waiting. i == 1\n";
}

void signals()
{
    std::cout << "Sleep one second" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
 
    {
        std::lock_guard<std::mutex> lk(cv_m);
        i = 1;
        std::cerr << "Notifying again...\n";
    }
    cv.notify_all();
}

int main() {
    std::thread t1(signals), t2(waits), t3(waits);
    t1.join();
    t2.join();
    t3.join();

    return 0;
}

/** 输出
Sleep one second
Waiting... 
Waiting... 
Notifying again...
...finished waiting. i == 1
...finished waiting. i == 1
*/

4.2 使用std::future执行单次异步任务

1. std::future

std::future是头文件<future>中的模板类:

  • 由一些异步操作创建并返回,此时该std::future对象与该异步操作关联
  • 用来保存这些异步操作的返回值异常std::future是模板对象,模板参数与所关联异步操作的返回值类型相同

std::future主要包含如下两个常用成员函数:

  • wait():阻塞等待对应的异步操作执行完成
  • get():如果异步操作已经完成,会直接返回异步操作的返回值;否则内部调用wait()阻塞线程直至异步操作执行完成,std::future变为就绪状态,返回异步操作的结果(优先以右值的方式返回结果)

2. std::async()函数

std::async()是头文件<future>中的函数,用于异步执行指定的可调用对象,包含两个重载:

async(std::launch policy,       // 函数在哪个线程执行
      F&& f,                    // 执行的函数
      Args&&... args);          // 传递给函数的参数
      
async(F&& f,                    // 执行的函数
      Args&&... args );         // 传递给函数的参数

返回值:

  • 返回一个以函数f的返回值类型return_type作为模板参数的std::future<return_type>对象
  • std::future对象可以用来阻塞线程,等待指定的函数f执行完成

参数:

  • policy:指定函数在哪个线程执行,类型为std::launch,不同值的含义为:
    • std::launch::async:启动新线程执行函数
    • std::launch::deferred:调用返回值std::futurewaitget后在当前线程执行函数(延时调用指定的函数)
    • std::launch::async | std::launch::deferred:由async的实现自行选择运行的方式。上面只有两个参数的重载async就等价于指定了policy为这个值
template <typename VectorIt>
int ParallelSum(VectorIt beg, VectorIt end) {
    auto len = end - beg;
    if (len < 1000) {
        return std::accumulate(beg, end, 0);
    }
    auto mid = beg + len / 2;
    // 前一半开新线程计算
    std::future<int> f_res = std::async(std::launch::async, 
                                        ParallelSum<VectorIt>, beg, mid);

    // 后一半在当前线程执行
    auto sum = std::accumulate(mid, end, 0);

    // 前一半的结果 + 后一半的结果
    return f_res.get() + sum;
}

std::vector<int> vec(10000, 1);
std::cout << "The sum is " << ParallelSum(vec.begin(), vec.end()) << std::endl;
// The sum is 10000

3. std::packaged_task对象

std::packaged_task是头文件<future>中的模板类,它提供了对需要异步执行的可调用对象的封装,可以实现对异步任务的统一管理。

创建std::packaged_task的对象时:

  • 模板参数指定函数的调用形式(类似于std::function,由返回值和函数参数构成,用于描述函数的形式),指明其中所保存的可调用对象的调用形式
  • 同时接受一个待执行的可调用对象作为构造函数的参数
  • std::packaged_task对象只支持移动语义,其拷贝相关的函数被定义为了删除的

std::packaged_task中重载了函数调用运算符,所以可以直接通过对象调用所管理的可调用对象

同时std::packaged_task中包含如下成员函数,用于获取保存可调用对象返回值的std::future

std::future<R> get_future();
  • R的类型会根据所管理可调用对象的返回值类型确定
  • 调用返回std::future对象的get函数就可以阻塞等待对应的std::packaged_task执行完成,并获取执行结果

通过std::packaged_task及其get_future,可以获得一对相关联的对象:

  • packaged_task对象可以在特定线程中执行
  • 利用get_future获取的future对象可以在其他地方异步获取执行的结果

下面的代码利用std::packaged_taskfuture异步获取所有的结果:

int AccuVector(std::vector<int>::iterator beg, 
               std::vector<int>::iterator end) {
    
    return std::accumulate(beg, end, 0);
}

int SliceAccu(std::vector<int>::iterator beg, 
               std::vector<int>::iterator end) {

    // 保存所有 futrue 表示的异步任务的结果
    std::vector<std::future<int>> results;
    while (beg + 10000 <= end) {
        // 依次构造所有任务,并保存任务对应的 future
        auto sub_task = std::packaged_task<int(std::vector<int>::iterator, 
                                               std::vector<int>::iterator)>(AccuVector);
        
        results.push_back(sub_task.get_future());
        
        // 将任务放到特定线程中去执行
        std::thread task_td(std::move(sub_task), beg, beg + 10000);
        task_td.detach();

        // 新任务的起点
        beg += 10000;
    }

    int sum = 0;
    // 当前线程只利用所有 future 获取结果
    for (auto &res : results) {
        sum += res.get();
    }
    return sum;
}

std::vector<int> vec(50000, 1);
std::cout << "The sum is " << SliceAccu(vec.begin(), vec.end()) << std::endl;
// The sum is 50000

4. std::promise

std::promise用于在不同线程间传递值,且只能使用一次。

创建std::promise时需要通过模板参数指定传递的值类型

std::promise<int> pro_test;
  • std::promise的拷贝相关的函数被定义为了删除的,只能执行移动操作

std::promise包含一个get_future函数,可以获取用于保存所传递值的future

std::future<int> res_future = pro_test.get_future();

std::promise可以使用成员函数set_value,将对应类型的值传递给预先返回的future对象:

pro_test.set_value(val);
  • 创建std::promise时,模板参数可以为void,此时对应的std::future的模板参数也为void,则调用set_value时不应该传递任何参数(这通常用来作为某件事情的通知,并不传递实际的数据)

当使用预先获取的future对象试图获取传递的值时,如果std::promise对象还没有调用set_value,程序会阻塞,直至set_value被调用:

// 阻塞等待 set_value 调用
int res = res_future.get();

下面展示了使用std::promise在线程间传递值的方法:

void AccuVector(std::vector<int>::iterator beg, 
               std::vector<int>::iterator end,
               std::promise<int> accu_promise) {
    // 将数据累加
    int sum = std::accumulate(beg, end, 0);
    // 使用 std::promise 将数据传给 futrue
    accu_promise.set_value(sum);
} 

// 划分任务
int SliceAccu(std::vector<int>::iterator beg, 
               std::vector<int>::iterator end) {
    
    // 保存所有 futrue 表示的异步任务的结果
    std::vector<std::future<int>> results;
    while (beg + 10000 <= end) {
        // 依次构造所有任务,并保存任务对应的 future
        std::promise<int> cur_accu_promise;

        // 保存可以获取结果的 future
        results.push_back(cur_accu_promise.get_future());
        
        // 将特定范围的数据异步去执行
        std::thread task_td(AccuVector, beg, beg + 10000, std::move(cur_accu_promise));
        task_td.detach();

        // 新任务的起点
        beg += 10000;
    }

    int sum = 0;
    // 当前线程只利用所有 future 获取结果
    for (auto &res : results) {
        sum += res.get();
    }
    return sum;
}

std::vector<int> vec(50000, 1);
std::cout << "The sum is " << SliceAccu(vec.begin(), vec.end()) << std::endl;
// The sum is 50000

5. std::future保存异常

std::future都会关联到某个异步操作,如std::asyncstd::packaged_taskstd::promise等,但是在异步执行特定操作时,可能会抛出异常。C++定义,可以使用std::future保存对应异步操作所抛出异常。

  • std::asyncstd::packaged_task所调用的函数抛出异常时,异常对象会保存到所关联的std::future中;当调用std::futureget函数时,异常会重新抛出
  • std::promise由于并不直接调用异步操作的相关语句,如果想要向所关联的std::future中保存异常,不能调用set_value函数,应该直接调用成员函数set_exception(),这样在关联的std::future调用get时异常也会重新抛出
    void set_exception(std::exception_ptr p);
    
    • set_exception接受一个std::exception_ptr类型的对象,该类型的对象可以通过如下两种方法获得
      // 1. 使用 std::make_exception_ptr(E e)
      cur_promise.set_exception(std::make_exception_ptr(std::logic_error("error")));
      
      // 2. 使用 std::current_exception 在 catch 子句中根据捕获的异常创建,
      //    返回一个 std::exception_ptr 对象
      try {
          throw std::runtime_error("runtime error");
      } catch (...) {   // 这里也可以指定特定类型的异常,current_exception都可以获取到
          pro.set_exception(std::current_exception());
      }
      

另外,对于std::packaged_taskstd::promisestd::future,如果std::future在没有get获取结果前,std::packaged_taskstd::promise的对象被销毁了,则对象的析构函数会将保存了std::future_errc::broken_promise码的std::future_error异常对象保存到std::future中,通过异常对象的code成员函数可以获取到异常码。

使用std::future保存异常的示例:

void AsyncSaveExcepion() {
    throw std::runtime_error("runtime error async");
}

void PromiseSaveException(std::promise<void> &pro) {
    // 调用 std::make_exception_ptr 创建 exception_ptr 对象
    pro.set_exception(std::make_exception_ptr(std::runtime_error("runtime error promise")));
}

int main(int argc, char const *argv[])
{
    // async 获取异常
    std::future<void> async_exception = std::async(AsyncSaveExcepion);
    try {
        async_exception.get();
    } catch (std::runtime_error &err) {
        std::cout << "async exception: " << err.what() << std::endl;
    }

    // promise 获取异常
    std::promise<void> pro;
    std::future<void> promise_exception = pro.get_future();
    // 执行线程函数
    std::thread t(PromiseSaveException, std::ref(pro));

    try {
        promise_exception.get();
    } catch (std::runtime_error &err) {
        std::cout << "proimse exception: " << err.what() << std::endl;
    }

    t.join();
    
    return 0;
}
/**输出
async exception: runtime error async
proimse exception: runtime error promise
*/

6. std::shared_future共享异步结果

std::future只有移动语义,如果想在多个线程中同时获取std::future对应异步操作的结果,要使用std::shared_future

创建std::shared_future主要包含如下两种方式:

  1. shared_future(std::future<T>&& other):使用std::future的右值创建,此时对应的other对象将关联的异步操作转移到新的shared_future对象中,other对象变为空
  2. std::shared_future<T> future<T>::shared()std::future对象的shared()成员函数会返回一个shared_future对象,此操作也会转移std::future所关联的异步操作,原std::future对象会变为空

std::shared_future的使用方法和std::future相同:

  • 使用wait阻塞等待异步操作执行完成
  • 使用get获得异步操作的返回值

std::shared_future是可以复制的,当std::shared_future关联的异步操作执行结束并返回结果后,所有等待的shared_future对象都会处于就绪状态,它们都会获得对应异步操作的返回值

下面展示了利用shared_future在多个线程获取结果:

std::chrono::time_point<std::chrono::high_resolution_clock> start;

std::chrono::duration<double, std::milli> 
  WaitFuture1(std::shared_future<void> ready_future) {
    
    ready_future.wait();
    return std::chrono::high_resolution_clock::now() - start;
}


std::chrono::duration<double, std::milli> 
  WaitFuture2(std::shared_future<void> ready_future) {
    
    ready_future.wait();
    return std::chrono::high_resolution_clock::now() - start;
}

int main(int argc, char const *argv[])
{
    std::promise<void> pro;
    std::shared_future<void> ready_future(pro.get_future());

    std::future res1 = std::async(WaitFuture1, ready_future);
    std::future res2 = std::async(WaitFuture2, ready_future);

    // 记录开始时间
    start = std::chrono::high_resolution_clock::now();

    // 将所关联的 shared_future 转换为就绪态
    pro.set_value();

    std::cout << "WaitFuture1 received the signal after "
              << res1.get().count() << "ms" << std::endl;
              
    std::cout << "WaitFuture2 received the signal after "
              << res2.get().count() << "ms" << std::endl;
    return 0;
}
/**输出
WaitFuture1 received the signal after 0.055026ms
WaitFuture2 received the signal after 0.049686ms
*/

4.3 有限等待

1. sleep_forsleep_until

std::this_thread::sleep_until(point):接受一个std::chrono::time_point时间点类型的函数point,阻塞等待至该指定的时间后,继续执行后续代码

  • 通常使用Clock::now() + sleep_time获取目标的时间点
  • 由于进程调度或资源争用,实际唤醒的时间可能会大于该时间点

std::this_thread::sleep_for(dur):接受一个std::chrono::duration时长类型的参数dur,阻塞等待dur时间后,继续执行后续代码

  • 由于进程调度或资源争用,实际阻塞的时长可能会大于指定的时长

2. condition_variable的限时等待

对于一个条件变量对象,正常情况下,调用wait后会一直阻塞,直至在其他线程中调用了notify_onenotify_all

但是在有些场景下,可能需要限制等待的时间,即阻塞时间如果超过指定的时间,则跳出阻塞,并返回相关的状态

condition_variable的限时等待包含两个成员函数:

  1. wait_until:阻塞等待,直至notify_xxx被调用到达指定的时间点。与wait相同,此函数也包含两个重载
    std::cv_status wait_until(unique_lock, time_point)
    
    • 调用lock.unlock()阻塞等待,当调用notify_one或调用notify_all或到达指定时间点time_point或发生伪唤醒时,执行lock.lock()后继续运行代码。
    bool wait_until(unique_lock, time_point, pred)
    
    • 函数被唤醒时,都会先执行lock.lock(),然后根据pred()的值,确定要调用lock.unlock()阻塞程序还是调用lock.lock()执行后续代码
  2. wait_for
    std::cv_status wait_for(unique_lock, rel_time)
    
    • 阻塞等待,当调用notify_one调用notify_all阻塞时长超过rel_time发生伪唤醒时,执行lock.lock()后继续运行后续代码
    bool wait_for(unique_lock, rel_time, pred)
    
    • 函数被唤醒时,都会先执行lock.lock(),然后根据pred()的值,确定要调用lock.unlock()阻塞程序还是调用lock.lock()执行后续代码

返回值:

  • std::cv_status是一个枚举类,包含两个成员:
    • std::cv_status::timeout表示函数因超过等待的时间而返回
    • std::cv_status::no_timeout表示函数并非因超时而返回
  • 返回boolwait_untilwait_for函数,其返回值和最后一次调用pred()时的值相同

下面的代码展示了使用wait_until等待特定的时间:

#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <random>
#include <thread>
#include <vector>

using namespace std::literals::chrono_literals;

std::mutex mut;
std::condition_variable cv;
int wait_value = 0;

void wait_random_time() {
  // 创建随机数发生器
  static std::default_random_engine e;
  static std::uniform_int_distribution<int> uni_real(2500, 4000);

  // 获取当前时间
  auto start_time = std::chrono::steady_clock::now();

  // 计算延迟随机时间后,得到的新时间点
  std::chrono::steady_clock::time_point dis_time =
      start_time + std::chrono::milliseconds(uni_real(e));

  // 阻塞等待
  std::unique_lock<std::mutex> uni_lock(mut);
  bool res = cv.wait_until(uni_lock, dis_time, [] { return wait_value == 1; });

  // 根据 res 确定是超时返回还是 notify 唤醒
  if (res) {
    std::cout << "[" << std::this_thread::get_id()
              << "]: return because of notify";
  } else {
    std::cout << "[" << std::this_thread::get_id()
              << "]: return because of timeout";
  }
}

/** @brief  3秒后调用 notify_all */
void notify_after_3_sec() {
  // 等待 3s
  std::this_thread::sleep_for(3s);

  // 锁住互斥量,修改变量值
  std::lock_guard lk(mut);
  wait_value = 1;

  // 唤醒所有等待条件的线程
  cv.notify_all();
}

int main(int argc, char const *argv[]) {
  // 保存所有的线程
  std::vector<std::thread> threads;

  // 创建五个随机等待的线程
  for (int i = 0; i < 5; ++i) {
    threads.emplace_back(wait_random_time);
  }

  // 创建一个用于唤醒的线程
  std::thread signal_thread(notify_after_3_sec);

  // join 所有线程
  for (int i = 0; i < 5; ++i) {
    threads[i].join();
  }
  signal_thread.join();

  return 0;
}
/**
[140737348179520]: return because of timeout
[140737339786816]: return because of timeout
[140737331394112]: return because of notify
[140737323001408]: return because of notify
[140737314608704]: return because of notify
*/

3. 可以指定超时的互斥量

对于之前介绍的互斥量std::mutexstd::recursive_mutexstd::shared_mutex,它们只能使用lock阻塞等待加锁或try_lock不阻塞地判断能否加锁,为了限制互斥量的阻塞时间,C++分别提供了timed的互斥量。

<mutex>头文件中提供了:

std::timed_mutexstd::recursive_timed_mutex类型,分别对应于std::mutexstd::recursive_mutex,它们除了包含std::mutex包含的所有操作外,还包含两个限制阻塞时间的加锁函数:

  1. bool try_lock_until(time_point)

    阻塞等待对互斥量加锁成功,如果到达指定的time_point还没能成功加锁,返回false;否则返回true

  2. bool try_lock_for(duration)

    阻塞等待对互斥量加锁成功,如果阻塞时长达到duration还没有成功加锁,返回false;否则返回true

类似的,在头文件<shared_mutex>中还提供了std::shared_timed_mutex类型,对应于shared_mutex。除了提供shared_mutex包含的所有操作外,还提供了如下函数用于限制阻塞时间。

限制互斥锁的等待时长:

  1. bool try_lock_until(time_point)
  2. bool try_lock_for(duration)

限制共享锁的等待时长

  1. bool try_lock_shared_until(time_point)
  2. bool try_lock_shared_for(duration)

同样,管理互斥量的std::unique_lockstd::shared_lock也都包含了指定阻塞时长的函数,这些函数会调用所管理互斥量的对应函数,实现限制阻塞时的等待时长。其参数和返回值表达的含义与互斥量对应的函数一样:

  1. bool try_lock_until(time_point)
  2. bool try_lock_for(duration)
#include <chrono>
#include <iostream>
#include <mutex>
#include <sstream>
#include <thread>
#include <vector>

std::timed_mutex t_mut;

void try_lock_for(int id) {
  // 等待固定的时间
  std::unique_lock<std::timed_mutex> uni_timed_mut(t_mut, std::defer_lock);

  // 保存所有输出的字符串
  std::ostringstream str;

  for (int i = 0; i < 2; ++i) {
    // 阻塞等待最多 50ms
    bool res = uni_timed_mut.try_lock_for(std::chrono::milliseconds(50));

    if (res) {
      str << "success ";
      // 等待 50ms
      std::this_thread::sleep_for(std::chrono::milliseconds(50));
      // 解锁互斥量
      uni_timed_mut.unlock();
    } else {
      str << "failed ";
    }
  }

  std::cout << "[" << id << "]" << str.str() << std::endl;
}

int main(int argc, char const *argv[]) {
  std::vector<std::thread> threads;
  for (int i = 0; i < 5; ++i) {
    threads.emplace_back(try_lock_for, i);
  }

  for (auto &th : threads) {
    th.join();
  }

  return 0;
}
/**
[0]success success 
[2]failed failed 
[3]failed failed 
[4]failed failed 
[1]failed success 
 */

4. 限制std::future的等待时间

std::futurestd::shared_future类型中都定义了如下两个成员函数用于限制wait等待的时长或时间点:

  1. std::future_status wait_until(time_point)
  2. std::future_status wait_for(duration)

其返回值类型std::future_status是一个枚举类型,包含成员:

  • std::future_status::deferred:表示所关联的异步操作的函数等待执行
  • std::future_status::ready:表示所关联的异步操作已执行完成
  • std::future_status::timeout:表示阻塞等待超时
#include <chrono>
#include <future>
#include <iostream>
#include <random>
#include <thread>
#include <vector>
#include <ctime>

int main(int argc, char const *argv[]) {
  // 保存所有 future
  std::future<int> future;
  future = std::async(std::launch::async, []() {
    std::this_thread::sleep_for(std::chrono::milliseconds(300));
    return 0;
  });

  // 每次等待 100ms 检查执行状态,共检查三次
  std::future_status res;
  int check_times = 3;
  while (check_times--) {
    res = future.wait_for(std::chrono::milliseconds(100));
    std::cout << "The " << 3 - check_times << "th check: ";
    switch (res) {
      case std::future_status::deferred:
        std::cout << "deferred\n";
        break;
      case std::future_status::timeout:
        std::cout << "timeout\n";
        break;
      case std::future_status::ready:
        std::cout << "ready\n";
        break;
    }
  }
  std::cout << "res: " << future.get() << "\n";

  return 0;
}
/**
The 1th check: timeout
The 2th check: timeout
The 3th check: ready
res: 0
 */

4.4 latchbarrier

1. std::latch

std::latch在C++20被引入,在头文件<latch>中,是一个用来线程同步的对象,使用时设置一个计数器,线程可以阻塞等待计数器值减少为0后,再继续执行后续代码。

std::latch对象的构造函数

explicit latch(std::ptrdiff_t expected);
  • expected:指定计数器的初始值

在需要等待计数器值减少为0的线程中,可以使用wait函数阻塞等待

latch.wait();

对于完成特定任务的线程,可以使用count_down函数对计数器减少指定的数值

void count_down(std::ptrdiff_t n = 1);
  • n:对计数器的减少的数值,默认值为1
  • 对计数器值的减少是原子操作,不会因为多个线程同时操作而发生数据错误
  • 对计数器减少的值如果是负数大于内部计数器的值,会产生未定义的行为

有些线程可能需要先减少计数器的值,再阻塞等待计数器值变为0,这时可以使用简化的函数:

void arrive_and_wait(std::ptrdiff_t n = 1);
  • n:内部计数器要减少的值
  • 该函数对计数器的减少也是原子操作
  • std::latch的计数器值不可以重置,也不能增加,所以只能使用一次。但是可以在一个线程中多次减少计数器的值。
std::latch work_done{5};

void WorkThread() {
  static std::default_random_engine e;
  static std::uniform_int_distribution<int> uni_int(3, 6);

  int wait_time = uni_int(e);
  std::cout << "[" << std::this_thread::get_id() << "] wait for " 
            << wait_time << "s" << std::endl;

  // 休眠若干秒,模拟耗时操作
  std::this_thread::sleep_for(std::chrono::seconds(wait_time));

  // 减少计数器的值
  work_done.count_down();
}

int main(int argc, char const *argv[]) {
  std::vector<std::thread> threads;
  // 记录开始时间
  auto beg_time = std::chrono::steady_clock::now();
  for (int i = 0; i < 5; ++i) {
    threads.push_back(std::thread(WorkThread));
  }

  // 等待所有线程执行完成
  work_done.wait();

  // 计算等待时长
  auto dura = std::chrono::duration_cast<std::chrono::milliseconds>
              (std::chrono::steady_clock::now() - beg_time);

  std::cout << "main thread wait for " << dura.count() << "ms" << std::endl;

  for (int i = 0; i < 5; ++i) {
    threads[i].join();
  }
  return 0;
}
/** 输出
[140737348179520] wait for 3s
[140737339786816] wait for 3s
[140737331394112] wait for 6s
[140737323001408] wait for 4s
[140737314608704] wait for 5s
main thread wait for 6001ms
 */

2. std::barrier

头文件<barrier>中的std::barrier也提供了一种多个线程阻塞等待计数器值减少为0的功能,但是它相比于std::latch

  • 可以在计数器减少为0时自动执行指定函数
  • 当计数器减少为0解除阻塞后,可以重新开始新一轮的等待
  • 每一轮都可以调整下一轮的计数器的初始值

std::barrier构造函数

explicit barrier(std::ptrdiff_t expected,
                 CompletionFunction f = CompletionFunction());
  • expected:指定计数器的初始值
  • f:当计数器的值减少到0后,会首先调用该函数,默认是一个空的可调用对象

减少计数器值阻塞的函数:

arrival_token arrive(std::ptrdiff_t n = 1); // 减少计数器值,并返回标识当前轮次的 arrival

void wait(arrival_token&& arrival);         // 阻塞等待
  • arrive:函数可以指定减少的计数器值大小(原子操作),并返回一个与当前轮次相关联的标志
  • wait:接受一个标记了某个轮次的标志,并在该轮次阻塞等待计数器值变为0
  • 通常两者一起使用:
    wait(arrive());
    

为了简化以上过程,C++提供了下面的简化成员函数:

void arrive_and_wait();
  • 原子性的将计数器的值减1,并在当前轮次阻塞等待计数器变为0,等价于wait(arrive())

std::barrier可以重复使用的,一般情况下,当计数器值变为0后,会重置计数器的值,并开始下一轮次的arrivewait。C++还提供了可以调整下一轮计数器值初始值的方法:

void arrive_and_drop()
  • 将当前轮次的计数器值减少1,并阻塞等待计数器值变为0
  • 指定后续所有轮次计数器的初始值减少1

综上所述,std::barrier每轮的运行过程为:

  1. 使用初始值初始化计数器
  2. 调用arrive_and_wait()arrive_and_drop()将当前计数器值减1并阻塞
  3. 当某个线程调用arrive_xx后如果计数器变为0,在该线程执行创建对象时指定的函数f之后解除所有的阻塞等待
  4. 根据调用arrive_and_drop的次数确定下一轮运行时计数器的初始值,并开始新一轮的运行过程

示例:

// 保存每轮生成的数字
std::vector<int> cur_num{4};

// 保存当前的轮次
int cur_phase = 1;

// 所有生成数字的累加和
int sum_num = 0;

/** @brief  在每个轮次计算本轮生成的所有数字和,用以确认是否结束线程 */
void ComplateFun() {
  std::cout << "[" << std::this_thread::get_id() << "]: ";
  for (int i = 0; i < 4; ++i) {
    sum_num += cur_num[i];
  }
  // 根据结果,输出不同的内容
  if (sum_num > 40) {
    std::cout << "end all phase, sum_num: " << sum_num << "\n";
  } else {
    std::cout << "sum_num: " << sum_num 
              << "\n...begin phase " << cur_phase++ << " ...\n";
  }
}

// 用于同步的 barrier
std::barrier sync_bar{4, ComplateFun};

/**
 * @brief  每个线程执行的函数,当 sum_num 达到 40 时退出线程
 * @param  thread_id 标识每个线程,并指定该线程生成的数字保存到 cur_num 的哪个位置
 */
void WorkFunc(int thread_id) {
  static std::default_random_engine e;
  static std::uniform_int_distribution<int> uin_int(3, 8);

  // 当 sum_num 达到 40 及以上时退出线程
  while (sum_num < 40) {
    // 随机生成一个整数
    cur_num[thread_id] = uin_int(e);
    std::cout << "[" << std::this_thread::get_id() << "]: gen num "
              << cur_num[thread_id] << "\n";

    // 根据生成的数字设置阻塞时长
    std::this_thread::sleep_for(std::chrono::seconds(cur_num[thread_id]));

    // 阻塞等待本轮所有数字都生成完成
    sync_bar.arrive_and_wait();
  }
}

int main(int argc, char const *argv[]) {
  std::cout << "...begin phase " << cur_phase++ << " ...\n";

  // 开启所有线程
  std::vector<std::thread> threads;
  for (int i = 0; i < 4; ++i) {
    threads.push_back(std::thread(WorkFunc, i));
  }

  // 释放所有线程
  for (int i = 0; i < 4; ++i) {
    threads[i].join();
  }
  return 0;
}
/**
...begin phase 1 ...
[140737348179520]: gen num 3
[140737339786816]: gen num 3
[140737331394112]: gen num 7
[140737323001408]: gen num 5
[140737331394112]: sum_num: 18
...begin phase 2 ...
[140737331394112]: gen num 6
[140737348179520]: gen num 4
[140737323001408]: gen num 7
[140737339786816]: gen num 3
[140737323001408]: sum_num: 38
...begin phase 3 ...
[140737323001408]: gen num 7
[140737339786816]: gen num 8
[140737348179520]: gen num 5
[140737331394112]: gen num 6
[140737339786816]: end all phase, sum_num: 64
 */
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值