你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

c++11关于并发的新特性

[复制链接]
gaosmile 发布时间:2020-5-21 16:17
c++11关于并发引入了好多好东西,这里按照如下顺序介绍:
  • std::thread相关
  • std::mutex相关
  • std::lock相关
  • std::atomic相关
  • std::call_once相关
  • volatile相关
  • std::condition_variable相关
  • std::future相关
  • async相关

std::thread相关
c++11之前你可能使用pthread_xxx来创建线程,繁琐且不易读,c++11引入了std::thread来创建线程,支持对线程join或者detach。直接看代码:

  1. #include <iostream>
  2. #include <thread>

  3. using namespace std;

  4. int main() {
  5.    auto func = []() {
  6.        for (int i = 0; i < 10; ++i) {
  7.            cout << i << " ";
  8.       }
  9.        cout << endl;
  10.   };
  11.    std::thread t(func);
  12.    if (t.joinable()) {
  13.        t.detach();
  14.   }
  15.    auto func1 = [](int k) {
  16.        for (int i = 0; i < k; ++i) {
  17.            cout << i << " ";
  18.       }
  19.        cout << endl;
  20.   };
  21.    std::thread tt(func1, 20);
  22.    if (tt.joinable()) { // 检查线程可否被join
  23.        tt.join();
  24.   }
  25.    return 0;
  26. }
复制代码

上述代码中,函数func和func1运行在线程对象t和tt中,从刚创建对象开始就会新建一个线程用于执行函数,调用join函数将会阻塞主线程,直到线程函数执行结束,线程函数的返回值将会被忽略。如果不希望线程被阻塞执行,可以调用线程对象的detach函数,表示将线程和线程对象分离。

如果没有调用join或者detach函数,假如线程函数执行时间较长,此时线程对象的生命周期结束调用析构函数清理资源,这时可能会发生错误,这里有两种解决办法,一个是调用join(),保证线程函数的生命周期和线程对象的生命周期相同,另一个是调用detach(),将线程和线程对象分离,这里需要注意,如果线程已经和对象分离,那我们就再也无法控制线程什么时候结束了,不能再通过join来等待线程执行完。

这里可以对thread进行封装,避免没有调用join或者detach可导致程序出错的情况出现:

  1. class ThreadGuard {
  2.   public:
  3.    enum class DesAction { join, detach };

  4.    ThreadGuard(std::thread&& t, DesAction a) : t_(std::move(t)), action_(a){};

  5.    ~ThreadGuard() {
  6.        if (t_.joinable()) {
  7.            if (action_ == DesAction::join) {
  8.                t_.join();
  9.           } else {
  10.                t_.detach();
  11.           }
  12.   }
  13.   }

  14.    ThreadGuard(ThreadGuard&&) = default;
  15.    ThreadGuard& operator=(ThreadGuard&&) = default;

  16.    std::thread& get() { return t_; }

  17.   private:
  18.    std::thread t_;
  19.    DesAction action_;
  20. };

  21. int main() {
  22.    ThreadGuard t(std::thread([]() {
  23.        for (int i = 0; i < 10; ++i) {
  24.            std::cout << "thread guard " << i << " ";
  25.       }
  26.        std::cout << std::endl;}), ThreadGuard::DesAction::join);
  27.    return 0;
  28. }
复制代码

c++11还提供了获取线程id,或者系统cpu个数,获取thread native_handle,使得线程休眠等功能

  1. std::thread t(func);
  2. cout << "当前线程ID " << t.get_id() << endl;
  3. cout << "当前cpu个数 " << std::thread::hardware_concurrency() << endl;
  4. auto handle = t.native_handle();// handle可用于pthread相关操作
  5. std::this_thread::sleep_for(std::chrono::seconds(1));
复制代码

std::mutex相关
std::mutex是一种线程同步的手段,用于保存多线程同时操作的共享数据。
mutex分为四种:
  • std::mutex:独占的互斥量,不能递归使用,不带超时功能
  • std::recursive_mutex:递归互斥量,可重入,不带超时功能
  • std::timed_mutex:带超时的互斥量,不能递归
  • std::recursive_timed_mutex:带超时的互斥量,可以递归使用

拿一个std::mutex和std::timed_mutex举例吧,别的都是类似的使用方式:
std::mutex:

  1. #include <iostream>
  2. #include <mutex>
  3. #include <thread>

  4. using namespace std;
  5. std::mutex mutex_;

  6. int main() {
  7.    auto func1 = [](int k) {
  8.        mutex_.lock();
  9.        for (int i = 0; i < k; ++i) {
  10.            cout << i << " ";
  11.       }
  12.        cout << endl;
  13.        mutex_.unlock();
  14.   };
  15.    std::thread threads[5];
  16.    for (int i = 0; i < 5; ++i) {
  17.        threads[i] = std::thread(func1, 200);
  18.   }
  19.    for (auto& th : threads) {
  20.        th.join();
  21.   }
  22.    return 0;
  23. }
复制代码

std::timed_mutex:

  1. #include <iostream>
  2. #include <mutex>
  3. #include <thread>
  4. #include <chrono>

  5. using namespace std;
  6. std::timed_mutex timed_mutex_;

  7. int main() {
  8.    auto func1 = [](int k) {
  9.        timed_mutex_.try_lock_for(std::chrono::milliseconds(200));
  10.        for (int i = 0; i < k; ++i) {
  11.            cout << i << " ";
  12.       }
  13.        cout << endl;
  14.        timed_mutex_.unlock();
  15.   };
  16.    std::thread threads[5];
  17.    for (int i = 0; i < 5; ++i) {
  18.        threads[i] = std::thread(func1, 200);
  19.   }
  20.    for (auto& th : threads) {
  21.        th.join();
  22.   }
  23.    return 0;
  24. }
复制代码

std::lock相关
这里主要介绍两种RAII方式的锁封装,可以动态的释放锁资源,防止线程由于编码失误导致一直持有锁。
c++11主要有std::lock_guard和std::unique_lock两种方式,使用方式都类似,如下:

  1. #include <iostream>
  2. #include <mutex>
  3. #include <thread>
  4. #include <chrono>

  5. using namespace std;
  6. std::mutex mutex_;

  7. int main() {
  8.    auto func1 = [](int k) {
  9.        // std::lock_guard<std::mutex> lock(mutex_);
  10.        std::unique_lock<std::mutex> lock(mutex_);
  11.        for (int i = 0; i < k; ++i) {
  12.            cout << i << " ";
  13.       }
  14.        cout << endl;
  15.   };
  16.    std::thread threads[5];
  17.    for (int i = 0; i < 5; ++i) {
  18.        threads[i] = std::thread(func1, 200);
  19.   }
  20.    for (auto& th : threads) {
  21.        th.join();
  22.   }
  23.    return 0;
  24. }
复制代码

std::lock_gurad相比于std::unique_lock更加轻量级,少了一些成员函数,std::unique_lock类有unlock函数,可以手动释放锁,所以条件变量都配合std::unique_lock使用,而不是std::lock_guard,因为条件变量在wait时需要有手动释放锁的能力,具体关于条件变量后面会讲到。
std::atomic相关
c++11提供了原子类型std::atomic<T>,理论上这个T可以是任意类型,但是我平时只存放整形,别的还真的没用过,整形有这种原子变量已经足够方便,就不需要使用std::mutex来保护该变量啦。看一个计数器的代码:

  1. struct OriginCounter { // 普通的计数器
  2.    int count;
  3.    std::mutex mutex_;
  4.    void add() {
  5.        std::lock_guard<std::mutex> lock(mutex_);
  6.        ++count;
  7.   }

  8.    void sub() {
  9.        std::lock_guard<std::mutex> lock(mutex_);
  10.        --count;
  11.   }

  12.    int get() {
  13.        std::lock_guard<std::mutex> lock(mutex_);
  14.        return count;
  15.   }
  16. };

  17. struct NewCounter { // 使用原子变量的计数器
  18.    std::atomic<int> count;
  19.    void add() {
  20.        ++count;
  21.        // count.store(++count);这种方式也可以
  22.   }

  23.    void sub() {
  24.        --count;
  25.        // count.store(--count);
  26.   }

  27.    int get() {
  28.        return count.load();
  29.   }
  30. };
复制代码

是不是使用原子变量更加方便了呢?
std::call_once相关
c++11提供了std::call_once来保证某一函数在多线程环境中只调用一次,它需要配合std:nce_flag使用,直接看使用代码吧:

  1. std::once_flag onceflag;

  2. void CallOnce() {
  3.    std::call_once(onceflag, []() {
  4.        cout << "call once" << endl;
  5.   });
  6. }

  7. int main() {
  8.    std::thread threads[5];
  9.    for (int i = 0; i < 5; ++i) {
  10.        threads[i] = std::thread(CallOnce);
  11.   }
  12.    for (auto& th : threads) {
  13.        th.join();
  14.   }
  15.    return 0;
  16. }
复制代码

volatile相关
貌似把volatile放在并发里介绍不太合适,但是貌似很多人都会把volatile和多线程联系在一起,那就一起介绍下吧。
volatile通常用来建立内存屏障,volatile修饰的变量,编译器对访问该变量的代码通常不再进行优化,看下面代码:

  1. int *p = xxx;
  2. int a = *p;
  3. int b = *p;
复制代码

a和b都等于p指向的值,一般编译器会对此做优化,把*p的值放入寄存器,就是传说中的工作内存(不是主内存),之后a和b都等于寄存器的值,但是如果中间p地址的值改变,内存上的值改变啦,但a,b还是从寄存器中取的值(不一定,看编译器优化结果),这就不符合需求,所以在此对p加volatile修饰可以避免进行此类优化。
注意:volatile不能解决多线程安全问题,针对特种内存才需要使用volatile,它和atomic的特点如下:
• std::atomic用于多线程访问的数据,且不用互斥量,用于并发编程中
• volatile用于读写操作不可以被优化掉的内存,用于特种内存中


std::condition_variable相关
条件变量是c++11引入的一种同步机制,它可以阻塞一个线程或者个线程,直到有线程通知或者超时才会唤醒正在阻塞的线程,条件变量需要和锁配合使用,这里的锁就是上面介绍的std::unique_lock。
这里使用条件变量实现一个CountDownLatch:

  1. class CountDownLatch {
  2.    public:
  3.     explicit CountDownLatch(uint32_t count) : count_(count);

  4.     void CountDown() {
  5.         std::unique_lock<std::mutex> lock(mutex_);
  6.         --count_;
  7.         if (count_ == 0) {
  8.             cv_.notify_all();
  9.         }
  10.     }

  11.     void Await(uint32_t time_ms = 0) {
  12.         std::unique_lock<std::mutex> lock(mutex_);
  13.         while (count_ > 0) {
  14.             if (time_ms > 0) {
  15.                 cv_.wait_for(lock, std::chrono::milliseconds(time_ms));
  16.             } else {
  17.                 cv_.wait(lock);
  18.             }
  19.         }
  20.     }

  21.     uint32_t GetCount() const {
  22.         std::unique_lock<std::mutex> lock(mutex_);
  23.       return count_;
  24.     }

  25.    private:
  26.     std::condition_variable cv_;
  27.     mutable std::mutex mutex_;
  28.     uint32_t count_ = 0;
  29. };
复制代码
关于条件变量其实还涉及到通知丢失和虚假唤醒问题,因为不是本文的主题,这里暂不介绍,大家有需要可以留言。
std::future相关
c++11关于异步操作提供了future相关的类,主要有std::future、std::promise和std::packaged_task,std::future比std::thread高级些,std::future作为异步结果的传输通道,通过get()可以很方便的获取线程函数的返回值,std::promise用来包装一个值,将数据和future绑定起来,而std::packaged_task则用来包装一个调用对象,将函数和future绑定起来,方便异步调用。而std::future是不可以复制的,如果需要复制放到容器中可以使用std::shared_future。
std::promise与std::future配合使用

  1. #include <functional>
  2. #include <future>
  3. #include <iostream>
  4. #include <thread>

  5. using namespace std;

  6. void func(std::future<int>& fut) {
  7.     int x = fut.get();
  8.     cout << "value: " << x << endl;
  9. }

  10. int main() {
  11.     std::promise<int> prom;
  12.     std::future<int> fut = prom.get_future();
  13.     std::thread t(func, std::ref(fut));
  14.     prom.set_value(144);
  15.     t.join();
  16.     return 0;
  17. }
复制代码

std::packaged_task与std::future配合使用

  1. #include <functional>
  2. #include <future>
  3. #include <iostream>
  4. #include <thread>

  5. using namespace std;

  6. int func(int in) {
  7.     return in + 1;
  8. }

  9. int main() {
  10.     std::packaged_task<int(int)> task(func);
  11.     std::future<int> fut = task.get_future();
  12.     std::thread(std::move(task), 5).detach();
  13.     cout << "result " << fut.get() << endl;
  14.     return 0;
  15. }
复制代码


更多关于future的使用可以看我之前写的关于线程池和定时器的文章。
三者之间的关系
std::future用于访问异步操作的结果,而std::promise和std::packaged_task在future高一层,它们内部都有一个future,promise包装的是一个值,packaged_task包装的是一个函数,当需要获取线程中的某个值,可以使用std::promise,当需要获取线程函数返回值,可以使用std::packaged_task。
async相关
async是比future,packaged_task,promise更高级的东西,它是基于任务的异步操作,通过async可以直接创建异步的任务,返回的结果会保存在future中,不需要像packaged_task和promise那么麻烦,关于线程操作应该优先使用async,看一段使用代码:

  1. #include <functional>
  2. #include <future>
  3. #include <iostream>
  4. #include <thread>

  5. using namespace std;

  6. int func(int in) { return in + 1; }

  7. int main() {
  8.     auto res = std::async(func, 5);
  9.     // res.wait();
  10.     cout << res.get() << endl; // 阻塞直到函数返回
  11.     return 0;
  12. }
复制代码

使用async异步执行函数是不是方便多啦。
async具体语法如下:

  1. async(std::launch::async | std::launch::deferred, func, args...);
复制代码

第一个参数是创建策略:
  • std::launch::async表示任务执行在另一线程
  • std::launch::deferred表示延迟执行任务,调用get或者wait时才会执行,不会创建线程,惰性执行在当前线程。

如果不明确指定创建策略,以上两个都不是async的默认策略,而是未定义,它是一个基于任务的程序设计,内部有一个调度器(线程池),会根据实际情况决定采用哪种策略。
若从 std::async 获得的 std::future 未被移动或绑定到引用,则在完整表达式结尾, std::future的析构函数将阻塞直至异步计算完成,实际上相当于同步操作:

  1. std::async(std::launch::async, []{ f(); }); // 临时量的析构函数等待 f()
  2. std::async(std::launch::async, []{ g(); }); // f() 完成前不开始
复制代码


注意:关于async启动策略这里网上和各种书籍介绍的五花八门,这里会以cppreference为主。
• 有时候我们如果想真正执行异步操作可以对async进行封装,强制使用std::launch::async策略来调用async。

  1. template <typename F, typename... Args>
  2. inline auto ReallyAsync(F&& f, Args&&... params) {
  3.     return std::async(std::launch::async, std::forward<F>(f), std::forward<Args>(params)...);
  4. }
复制代码


总结


std::thread使线程的创建变得非常简单,还可以获取线程id等信息。
std::mutex通过多种方式保证了线程安全,互斥量可以独占,也可以重入,还可以设置互斥量的超时时间,避免一直阻塞等锁。
std::lock通过RAII技术方便了加锁和解锁调用,有std::lock_guard和std::unique_lock。
std::atomic提供了原子变量,更方便实现实现保护,不需要使用互斥量
std::call_once保证函数在多线程环境下只调用一次,可用于实现单例。
volatile常用于读写操作不可以被优化掉的内存中。
std::condition_variable提供等待的同步机制,可阻塞一个或多个线程,等待其它线程通知后唤醒。
std::future用于异步调用的包装和返回值。
async更方便的实现了异步调用,异步调用优先使用async取代创建线程。
关于c++11关于并发的新特性就介绍到这里

收藏 1 评论1 发布时间:2020-5-21 16:17

举报

1个回答
yklstudent-1794 回答时间:2020-5-21 20:31:43
纯粹为了C艹,顶下

所属标签

STM32团队

意法半导体微控制器和微处理器拥有广泛的产品线,包含低成本的8位单片机和基于ARM® Cortex®-M0、M0+、M3、M4、M33、M7及A7内核并具备丰富外设选择的32位微控制器及微处理器


最新内容

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
微信公众号二维码 微信公众号
手机版二维码 手机版