Post

现代c++并发编程1-线程创建

c++语言层面没有提供进程级别的原语支持,所以你只用关注c++层面的线程并发

区别于并发和并行,前者是指多个任务交替执行,后者是指多个任务同时执行

因此,当某个场景说的并发的时候,你可能要理解一下他到底指什么

  1. 多核机器,真的并行
  2. 单个核心,任务切换

1. 线程的使用

在c++11中,线程完全依赖于std::thread类,这个类的构造函数接受一个可调用对象,这个对象会在新线程中执行

1.1 从hello world开始

1
2
3
4
5
6
7
#include <iostream>
#include <thread>

int main() {
  std::thread t1{[]() { std::cout << "Hello from t1\n"; }};
  t1.join();
}

首先看到的是std::thread的构造函数,这个构造函数接受一个可调用对象,这个对象会在新线程中执行。

t1.join()是等待线程结束, 否则会一直阻塞在这里,直到线程结束。这里的调用时必须的,否则会导致thread析构的时候检查到joinable为true,调用std::terminate()终止程序。

因此不难知道,在join结束之后,thread绑定的线程就不再活跃,这个时候可以调用joinable()来检查线程是否还活跃。

1.2 线程相关的属性

std::thread::hardware_concurrency();, 这个函数会返回一个unsigned int,表示支持的并发数目,如果返回0,表示不支持多线程。

另外,std::this_thread包含4个函数,

  1. std::this_thread::get_id(), 返回当前线程的id
  2. std::this_thread::sleep_for(), 休眠一段时间
  3. std::this_thread::sleep_until(), 休眠到某个时间点
  4. std::this_thread::yield(), 让出cpu,建议实现重新调度各执行线程

1.3 线程的构造函数和结束

默认构造下,没有活跃线程会与这个t相关联。

其余情况下,只要传递给thread的是一个callable对象,那么这个对象会在新线程中执行。

callable大概有两种,一种是function类的对象,另一种就是实现了operator()的类对象。

但要注意的是,operator()的类对象,就没法直接想函数指针那样传递了。

1
2
3
4
5
6
7
8
struct func {
  void operator()() { std::cout << "Hello from func\n"; }
};

int main() {
  std::thread t1{func{}};
  t1.join();
}

另外在这种情况下,还有一点值得注意,如果你使用()初始化了函数对象,那么这个其实会看作一个声名,比如std::thread t(func());

启动线程后(也就是构造std::thread对象)我们必须在线程对象的生存期结束之前,即std::thread::~thread调用之前,决定它的执行策略,是join()(合并)还是detach()(分离

因为先前简单介绍了join,这里主要讲一下detach,线程对象调用了 detach(),那么就是线程对象放弃了对线程资源的所有权,不再管理此线程,允许此线程独立的运行,在线程退出时释放所有分配的资源。

可以理解为此时std::thread t也没有活跃的线程与之相关联,放弃了对线程资源的所有权。

但这种写法其实值得注意,因为不会阻塞在detach,所以如果分离的线程还在访问父线程的资源,那么可能会导致未定义行为。

1.4 参数传递

正常情况下,thread的参数传递很简单,直接写在构造函数里就可以了。

但需要注意的是,这些参数会复制到新线程的内存空间中,即使函数中的参数是引用,依然实际是复制

1
2
3
4
void f(int, const int& a);

int n = 1;
std::thread t{ f, 3, n };

如果想要传递引用,可以使用std::ref,这个函数会返回一个std::reference_wrapper对象,这个对象可以被拷贝,但是拷贝的是引用。

1
2
3
4
5
6
7
8
9
10
void f(int, int& a) {
    std::cout << &a << '\n'; 
}

int main() {
    int n = 1;
    std::cout << &n << '\n';
    std::thread t { f, 3, std::ref(n) };
    t.join();
}

1.5 jthread

c++20的jthread相较于thread,可以看作只是多了两个功能

  1. RAII,自动在析构时调用join
  2. 线程停止

std::jthread 的通常实现就是单纯的保有 std::thread + std::stop_source 这两个数据成员:

关于线程停止,C++ 的 std::jthread 提供的线程停止功能并不同于常见的 POSIX 函数 pthread_cancel。pthread_cancel 是一种发送取消请求的函数,但并不是强制性的线程终止方式。目标线程的可取消性状态和类型决定了取消何时生效。当取消被执行时,进行清理和终止线程

std::jthread 所谓的线程停止只是一种基于用户代码的控制机制,而不是一种与操作系统系统有关系的线程终止。使用 std::stop_source 和 std::stop_token 提供了一种优雅地请求线程停止的方式,但实际上停止的决定和实现都由用户代码来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using namespace std::literals::chrono_literals;

void f(std::stop_token stop_token, int value){
    while (!stop_token.stop_requested()){ // 检查是否已经收到停止请求
        std::cout << value++ << ' ' << std::flush;
        std::this_thread::sleep_for(200ms);
    }
    std::cout << std::endl;
}

int main(){
    std::jthread thread{ f, 1 }; // 打印 1..15 大约 3 秒
    std::this_thread::sleep_for(3s);
    // jthread 的析构函数调用 request_stop() 和 join()。
}
This post is licensed under CC BY 4.0 by the author.

Trending Tags