查看原文
其他

C++ std::thread 必须要熟悉的几个知识点

字节流动 2022-05-08

std::thread


C++在 11 标准中增加了对线程(threads)的支持,看以下例子:


#include <thread>
#include <iostream>
using namespace std;

void f() {
  cout << "hello, world!" << endl;
}

int main() {
  thread t(f);  t.join();  return 0;
}


使用 C++11的线程功能必须包含 <thread> 头文件,之后便可以使用 std::thread 类来创建一个线程。


创建线程的时候必须传入一个可执行体作为参数,在上面的例子中这个可执行体是函数f()。


std::promise


为了在不同的线程之间传递数据,C++ 引入了 std::promise 和 std::future 这两种数据结构,在头文件 <future> 中包含。


promise 是一个范型的数据结构,你可以定义一个整形的 promise:promise<int>,这意味着线程之间传递的值是整形。


promise 的 get_future() 方法返回一个 future 数据结构,从这个 future 数据结构可以获取设置给 promise 的值,下面是一个例子:


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

using namespace std;

int main() {
  promise<int> a_promise;
  auto a_future = a_promise.get_future();  a_promise.set_value(10);  cout << a_future.get() << endl;
  cout << "after get()" << endl;
  return 0;
}


上面例子的输出结构是:


10
after get()


实际上,上面的例子并没有使用线程,但是很好得展示了 promise 和 future 之间的关系。


更复杂一点的使用场景可能是下面这样子的:


  • 主线程定义一个promise,命名为p

  • 主线程调用p.get_future(),并把返回值保存为引用f

  • 主线程启动一个子线程,并把p作为启动参数传给子线程

  • 主线程调用f.get(),但是此时子线程还未将数据放入promise内,所以主线程挂起

  • 子线程执行完,获取到结果,并把结果写入p

  • 主线程从f.get()的调用中被唤醒,获取到子线程写入p的值,继续执行


std::packaged_task


C++11很贴心地提供了packaged_task类型,让我们不用直接使用std::thread和std::promise,直接就能够生成线程,派遣任务:


#include <iostream>
#include <future>

using namespace std;

int f() {
  string hi = "hello, world!";
  cout << hi << endl;
  return hi.size();
}

int main() {
  packaged_task<int ()> task(f);
  auto result = task.get_future();

  task();

  cout << result.get() << endl;

  return 0;
}


上面代码的运行结果为:


hello, world!
13


std::async


std::packaged_task 要求你自己启动任务,比如上一章节例子中你要显示调用 task()。如果连这一步都想省了的话,可以使用 std:async:


#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <future> 

template <typename RandomIt>

int parallel_sum(RandomIt beg, RandomIt end)
{
    auto len = end - beg;
    if (len < 1000) 
        return std::accumulate(beg, end, 0);

    RandomIt mid = beg + len/2;
    auto handle = std::async(std::launch::async,
                             parallel_sum<RandomIt>, mid, end);
    int sum = parallel_sum(beg, mid);
    return sum + handle.get();
}

int main()
{
    std::vector<int> v(10000, 1);
    std::cout << "The sum is " << parallel_sum(v.begin(), v.end()) << '\n';
}


上面的代码来自 cpp reference ,运行结果:The sum is 10000。


std::this_thread


C++11专门提供了一个命名空间std::this_thread来表示当前线程。std::this_thread提供了几个方法可以对线程做一定的控制:


  • get_id(),获取线程id

  • yield(), 释放执行权

  • sleep_for(),使线程沉睡一定时间


下面是一个具体例子:


#include <iostream>#include <future>#include <thread>using namespace std;

int f(promise<int> my_promise) {
  string hi = "hello, world!";

  my_promise.set_value(hi.size());

  this_thread::sleep_for(0.1s);
  cout << hi << endl;
}

int main() {
  promise<int> f_promise;

  auto result = f_promise.get_future();

  thread f_thread(f, move(f_promise));
  cout << result.get() << endl;

  f_thread.join();

  return 0;
}


程序退出相关的函数


由于增加了线程,如果优雅得退出一个程序变得更加复杂,因为当主线程决定要退出的时候,它并不知道其他线程正在干什么。


看看下面这些程序退出相关的函数:


  • abort(),异常中止程序,不承诺清理资源,不调用 atexit() 注册的清理函数。实际上程序会接收到信号 SIGABRT,并退出。

  • terminate(), 程序中抛出异常,但是没有得到处理,于是调用这个函数来终止程序,实际的实现会调用abort()

  • exit(),正常退出整个程序(包括所有线程),清理所有资源,并调用atexit()注册的清理函数。对于C++程序而言,会调用静态对象的析构函数,并处理好IO缓冲

  • quick_exit(), 正常退出整个程序,会处理好IO缓冲,但是不会调用静态对象的析构函数,并且会调用at_quick_exit()注册的清理函数

  • _exit()/_Exit(),正常退出程序,但是不调用atexit()注册的清理函数,不清理任何资源


总结下问题:如果想使用exit()来从多线程函数中退出,必须等所有的线程都执行完。


如果不进行这个同步的话,exit()执行的时候会对析构静态对象,而哪些在调用exit()之后才运行结束的线程也会析构静态对象,会导致对静态对象的二次析构,从而导致未定义的结果。


另外一个选项是直接调用_exit()/_Exit()(这两等价)来直接终止程序,但是这样做完全不会清理任何资源,可能会导致重要的资源没有被释放。


所以C++11中引入了quick_exit()函数,不对静态对象进行析构,但是可以通过at_quick_exit()注册清理函数,用来清理和释放重要的资源。


来源:https://marvinsblog.net/post/2018-11-12-c-11-threads/


推荐:

Android FFmpeg 实现带滤镜的微信小视频录制功能

全网最全的 Android 音视频和 OpenGL ES 干货,都在这了

一文掌握 YUV 图像的基本处理

Android FFmpeg 流媒体边播放边录制功能

FFmpeg + Android AudioRecorder 音频录制编码

利用 FFmpeg 和 ANativeWindow 实现视频解码播放

FFmpeg、x264以及fdk-aac 编译整合

现代 C++ 并发编程基础

现代 C++ 智能指针使用入门


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存