C++并发:从std::thread()开始

头文件的作用

是C++11新引入标准库基础设施,提供对多线程操作的支持。

我们可以用 std::thread 来控制线程的创建、运行、回收。

学习 std::thread 的用法是了解C++多线程编程的第一步。

构造std::thread对象

  • 方法一:传入函数对象
class background_task {
public:
    void operator()() const {
        do_something();
        do_something_else();
    }
};

background_task f;
std::thread my_thread(f);

在这种情况下,函数对象先被 copystd::thread 对象的内部,然后再传参、被调用

  • 方法二:传入lambda表达式(也是callable对象)
std::thread my_thread([]{
    do_something();
    do_something_else();
})
  • 方法三:传入函数指针和参数
void f(int i);
std::thread(f, 3);
  • 方法四:传入对象的成员函数
class X {
public:
    void do_lengthy_work();
};

X my_x;
std::thread t(&X::do_lengthy_work, &my_x);	// 后面可以加上一系列参数,如果需要的话

std::thread成员函数

.join()

作用:

  • 等待线程执行完毕
  • 清除对象内部与具体线程相关的内存,当前对象将不再和任何线程相关联
  • 只能调用一次 .join() ,调用后 .joinable() 将永远返回 false

例子:

if (t.joinable()) {
    t.join();
}

.detach()

作用:

  • 把线程放在后台运行,线程的所有权和控制权交给 C++ Runtime Library
  • 当前对象将不再和任何线程相关联
  • 调用后 .joinable() 将永远返回 false

thread function传参可能遇到的问题

问题一:传入临时的callable对象,编译器会误以为是函数声明

// Wrong
std::thread my_thread(background_task())

解决方案:用圆括号或者花括号加以说明

// Correct
std::thread my_thread((background_task()));
std::thread my_thread{background_task()};

问题二:因为传指针or局部变量的引用,导致thread function可能访问已经被销毁的内容

解决方案:

  • 把需要使用的临时变量copystd::thread内部,不要和局部上下文共享临时变量
  • 使用RAII(资源获取即初始化)
class thread_guard {
    std::thread &t;
public:
    // Constructor
    explicit thread_guard(std::thread &t_): t(t_) {}
    
    // Destructor
    ~thread_guard() {
        if (t.joinable()) {
            t.join();
        }
    }
    
    // Copy Constructor
    thread_guard(const thread_guard &) = delete;
    
    // Copy-assignment Operator
    thread_guard& operator=(const thread_guard &) = delete;
}

struct func; // a callable object

void f {
    int some_local_stats = 0;
    func my_func(some_local_stats);
    std::thread t(my_func);
    thread_guard g(t);
    
    do_something_in_current_thread();	// 函数返回时,自动调用thread_guard的析构函数,等待线程join
}

问题三:因为传指针or局部变量的引用,导致在thread function入参时因强制类型转换而访问已经被销毁的内容

理解这个问题之前,需要先梳理一下 std::thread 对象创建后发生了什么:

  1. 原线程:调用 std::thread 的构造函数or拷贝赋值运算符
  2. 原线程:一个callable对象和它的参数被拷贝到新创建的 std::thread 内部
  3. 新线程:之前被拷贝的一系列参数,现在被传入callable对象(发生强制类型转换)
  4. 新线程:调用callable对象
  5. ......

其中,第3步发生在新线程内,我们只知道它发生在第2步之后,却不知道具体的发生时间。

如果第3步发生时,原线程已经退出了相关上下文,那么新线程在传参时,可能对已经被销毁的内容进行类型转换操作。

void f(int i, const std::string &s);

void oops(int some_param) {
    char buffer[1024];
    sprintf(buffer, "%i", some_param);
    std::thread t(f, 3, buffer);	// const char*类型的buffer被转换成std::string的时机是未知的
    t.detach();
}

解决方案:由程序员显式完成传参操作,避免出现类型转换

void not_oops(int some_param) {
    char buffer[1024];
    sprintf(buffer, "%i", some_param);
    std::thread t(f, 3, std::string(buffer));	// 避免类型转换
    t.detach();
}

问题四:callable对象的参数要求传引用,但实际传入的是内部拷贝对象的引用(无法对原来的对象进行修改)

void update_data_for_widget(widget_id w, widget_data &data);

void oops_again(widget_id w) {
    widget_data data;
    std::thread t(update_data_for_widget, w, data);	// 传入的data被拷贝,拷贝后的临时data的引用被传入update函数
    display_status();
    t.join();
    process_widget_data(data);
}

解决方案:使用 std::ref() 声明传引用

std::thread t(update_data_for_widget, w, std::ref(data));

使用上的技巧

Trick 1:传入只可move不可copy的对象

在这种情况下,如果原对象是无名的临时对象,那么 move 操作是自动完成的。

如果原对象是命名对象(左值引用),那就需要用 std::move() 来将它转换成(右值引用),之后的的拷贝就自动是 move 完成的。

void process_big_object(std::unique_ptr);

std::unique_ptr p(new big_object);	// std::unique_ptr是典型的不可copy只能move对象,std::thread也是
p->prepare_data(42);
std::thread t(process_big_object, std::move(p));

Trick 2:std::thread的move操作

void some_function();
void some_other_function();
std::thread t1(some_function);	// constructor
std::thread t2 = std::move(t1);	// move-assignment operator
t1 = std::thread(some_other_function);	// constructor, then move-assignment operator
std::thread t3;	// default constructor
t3 = std::move(t2);	// move-assignment operator
t1 = std::move(t3);	// Error: std::terminate()

上述程序的最后一行中,对象t1已经与一个正在运行的线程互相绑定,不能接受move的对象,因此整个程序会调用 std::terminate() 退出。

不能 move 给已经绑定了线程的对象。

Trick 3:在函数传参和返回时使用转移std::thread的所有权

std::thread f() {
    void some_function();
    return std::thread(some_function);	// 自动调用move
}

std::thread g() {
    void some_other_function(int);
    std::thread t(some_other_function, 32);
    return t;	// 自动调用move
}

void f(std::thread t);

void g() {
    void some_function();
    f(std::thread(some_function));	// 自动调用move
    std::thread t(some_function);
    f(std::move(t));	// 显式调用move
}

Trick 4:运行时确定新开线程的个数

const unsigned long hardware_threads = std::thread::hardware_concurrency();

Trick 5:获取线程id

线程id有个单独定义的类型,std::thread::id ,该类对象有如下性质:

  • 调用 std::thread 对象的 get_id() 方法可以得到一个 std::thread::id 类型对象
  • std::thread::id 支持大小比较和相等判断,
    • 相等即为同一线程
    • 若a
  • 如果当前对象不与任何正在运行的线程绑定,那么 get_id() 返回一个默认构造的 std::thread::id 对象
  • get_id() 可以被 std::cout 打印出来,但它的值没有任何具体意义,标准库也不对它的具体实现类型作保证

参考来源

  • C++ Concurrency in Action, 2nd Edition

你可能感兴趣的:(C++并发:从std::thread()开始)