C++并发编程实战笔记 [02] :线程管控

发起线程

线程通过构建 std::thread 对象而启动,该对象指明线程要运行的任务。可以传入任何可调类型std::thread 来构建一个 std::thread 对象。 需要包含头文件 <thread>

  • 传入的可调类型可以是函数:
void do_some_work();
std::thread my_thread(do_some_work);
  • 传入的可调类型可以是带有函数调用操作符的类:
class background_task {
public:
    void operator()() const {
        do_something();
        do_something_else();
    }
};
background_task f;
std::thread my_thread(f);

此时要防范二义性,对于有可能被解释成函数声明的C++语句,编译器就肯定会将其解释为函数声明。如 std::thread my_thread(background_task()); 语句本意是传入一个临时的匿名函数对象发起新线程,但却会被解释成一个函数声明。可以使用统一初始化语法来解决:

std::thread my_thread((background_task()));
std::thread my_thread{background_task()};
  • 传入的可调类型可以是lambda表达式
std::thread my_thread([]{
    do_something();
    do_something_else();
});

必须时刻注意在线程运行结束前,要保证它所访问的外部数据必须始终正确、有效。尤其要注意传给线程的可调对象含有指针或引用时,引用的外部对象在线程运行期间是否可能会被销毁,是否可能出现悬空指针。

等待线程完成

std::thread my_thread(foo);
my_thread.join(); //等待线程结束

对于某个给定的线程,join() 只能调用一次,只要 std::thread 对象曾经调用过 join(),线程就不再可汇合,成员函数 joinable() 将返回 false

利用 RAII 过程等待线程完成

如果打算等待线程结束,但在调用 join() 前就发生了异常,这会导致 join() 调用会被略过。为了在可能出现异常的情况下等待线程完成,最好是利用RAII过程。

RAII(Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。

class thread_guard {
    std::thread& t;
public:
    explicit thread_guard(std::thread& t_) :t(t_) {}
    ~thread_guard() {
        if (t.joinable()) t.join(); // join() 只能被调用一次
    }
    thread_guard(thread_guard const&) = delete; // 禁止拷贝构造函数
    thread_guard& operator=(thread_guard const&) = delete; // 禁止赋值构造函数
};

struct func {
    int& i;
    func(int& i_) :i(i_) {}
    void operator()() {
        for (unsigned j = 0; j < 1000000; ++j)
            do_something(i); // i是引用,需要注意可能导致悬空引用
    }
};

void f() {
    int some_local_state = 0;
    func my_func(some_local_state);
    std::thread t(my_func);
    thread_guard(t);
    do_something_in_current_thread();
}

当主线程执行到 f() 末尾时,按构建的逆序,所有局部对象都会被销毁,thread_guard 的对象 g 首先被销毁,在其析构函数中调用新线程的 join()。即使 do_something_in_current_thread() 发生异常,g 的析构函数仍会被调用,以上行为仍会发生。

分离线程

std::thread my_thread(foo);
my_thread.detach(); //分离线程

只有 joinable() 返回 true,线程才可被分离。分离后 joinable() 将返回 false

向线程函数传递参数

直接向 std::thread 的构造函数追加更多参数即可:

#include <iostream>
#include <thread>

void add(int a, int b) {
    std::cout << a + b << std::endl;
}

int main() {
    std::thread t(add, 2, 3); // 输出5
    t.join();

    return 0;
}

创建一个 std::thread 对象的时候,参数传递分为两步,先传给std::thread,再传给函数。线程具有内部存储空间,参数会按照默认方式先复制到该处,新创建的线程才能直接访问。然后,这些副本被当成临时变量,以右值形式传给新线程上的函数或可调用对象。

在传递引用时,需要使用 std::ref

void func(int& a) { a = 233; }

void f() {
    int a = 114514;
    std::thread t(func, std::ref(a));。// 传递引用
    // std::thread t(func, a); 这样将无法通过编译,因为无法将一个右值传递给期望左值引用参数的函数
    t.join();
    std::cout << a << std::endl; // 输出233
}

将某个类的成员函数设为线程函数,应传入一个函数指针,指向该成员函数,还需要给出对象指针,作为该函数的第一个参数:

class X {
public:
    void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work, &my_x);

向线程转移动态对象的归属权:

void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object, std::move(p));

使用 std::move,p所指向的对象的所属权会先转移到新线程的内部存储空间,再转移给 process_big_object() 函数。

移交线程归属权


void some_function();
void some_other_function();
std::thread t1(some_function);
std::thread t2 = std::move(t1);
t1 = std::thread(some_other_function);
std::thread t3;
t3 = std::move(t2); // 此时t3与运行some_function()的线程相关联
t1 = std::move(t3); // t1已有关联的线程,该赋值操作会导致终止整个程序

std::thread支持移动操作,可以方便地从函数内部返回 std::thread 对象:

std::thread f() {
    void some_function();
    return std::thread(some_function);
}
std::thread g() {
    void some_other_function(int);
    std::thread t(some_other_function, 42);
    return t;
}

将线程归属权转移到函数内部:

void f(std::thread t);
void g() {
    void some_function();
    f(std::thread(some_function));
    std::thread t(some_function);
    f(std::move(t));
}

于是,我们可以改进之前的 thread_guard 类,直接将新线程的归属权转移给类,不再需要先创建单独的具名变量,然后传引用。

在运行时选择线程数量

std::thread::hardware_concurrency() 返回程序在各次运行中可真正并发的线程数量。

识别线程

线程ID的类型是 std::thread::id,可以在与线程关联的 std::thread 对象上调用成员函数 get_id() 来获取,若当前对象没有关联到任何执行线程,则会返回一个值为 0 的默认构造的 std::thread::id 对象。当前线程的ID可以调用 std::this_thread::get_id() 来获得:

void f() {
    std::cout << std::this_thread::get_id() << std::endl;
}

void g() {
    std::thread t(f);
    std::cout << t.get_id() << std::endl;
    t.join();
    std::thread t2;
    std::cout << t2.get_id() << std::endl; // 输出0
}

std::thread::id 可以随意进行复制操作和比较操作。

posted @ 2022-10-04 14:56  AE酱  阅读(121)  评论(0编辑  收藏  举报