C++ 并发编程笔记(一)线程的启动、连接与分离

线程的启动、连接与分离

启动线程

这里常用的有三种方法,如下所示。

/* 使用函数指针表示线程入口点 */
void worker_func();
std::thread my_thread1(worker_func); // 函数指针

/*//////////////////////////////////////////////////////////////*/

/* 使用仿函数表示线程入口点 */
class mytask {
  public:
    void operator()() const {
        do_something();
    }
};
my_task task;
std::thread my_thread2(task); // 仿函数

/*//////////////////////////////////////////////////////////////*/

/* 使用 lambda 表达式表示线程入口点 */
std::thread my_thread3([](){  // lambda 表达式
    do_something();
});

等待或者分离线程

线程对象如果用以上方式被构造,那么就会启动线程,如果放任不管,当线程对象被析构之时(比如脱离它的作用域之后),它的析构函数就会默认调用 std::terminate() 终止这个线程。

分离线程 (detach)

线程安全:当需要分离线程的时候,需要保证线程需要使用到的资源在分离后依然有效

/* 错误的示例 */
struct func {
    int & _i;
    func(int & i) : _i(i) {}
    void operator()() {
        for (auto j = 0; j < 1000000; ++j)
            do_some_thing(i);
    }
};

void foo() {
    int local = 0;
    func my_func(local);
    std::thread my_thread(my_func);
    my_thread.detach();  // 线程分离,但是局部变量 local 将会被析构,线程引用了它的地址,这时线程如果还在执行,极有可能产生错误
}

连接线程 (join)

可以通过调用 thread::join() 函数连接到线程,来等待这个线程执行完毕。

thread::join() 只能等待这个线程执行完毕,如果需要更精细的控制,现代 C++ 还提供了条件变量期待 (futures)

调用 join() 是不可逆转的,它将清理线程相关的存储,std::thread 对象将不再与任何已结束的线程有关联,即对一个线程只能调用一次 join() ,一旦已调用过 join() 就无法再次连接,同时地,joinable() 函数将返回 false。

注意异常的情况

分离 (detach) 的情形:构造线程之后,可立刻调用 detach() 进行分离。

连接 (join) 的情形:需要细心挑选调用 join() 的位置,如果在调用前有异常抛出,那么 join() 函数很容易跳过。并且,由于 std::thread 对象的析构,线程将终止执行。这显然都不是我们想要的结果。以下是两种妥善处理这种情形的代码。

就近 catch 法

void f() {
    int local = 0;
    func my_func(local);
    std::thread t(my_func);
    try {
        may_cause_exception();
    } catch (...) {
        t.join();    // 捕获异常之后先 join,线程结束之后再向上抛出异常
        throw;
    }
    // 没有发生异常,照常调用
    t.join();
}

thread_guard 析构法

这种方法可以利用析构函数,在其中放置 join() 代码

class thread_guard : boost::noncopyable {
    std::thread & _t;
public:
    explicit thread_guard(std::thread & t) : _t(t) {}
    ~thread_guard() {
        if (_t.joinable()) {
            _t.join();
        }
    }
}
void f() {
    int local = 0;
    func my_func(local);
    std::thread t(my_func);
    thread_guard g(t);
    may_cause_exception(); // 出现异常之后,线程守护对象 g 将析构,将调用 join() 连接到线程 t
}

如果不需要等待线程结束,那么使用 detach() 函数分离线程通常是一个很好的选择,这将使得线程本体不在于 std::thread 对象关联,析构过程将不会使得线程终止。

线程的分离

线程分离之后,就无法用直接的方法连接到线程,而分离的线程确确实实在后台运行,所有权和控制权将传递给 C++ 运行时库,它将保证最终线程的相关资源会被正确地回收。

分离线程通常也叫做守护线程 (daemon thread) 。以下是一些逻辑上的示例,实现了一个多文档编辑器。

void edit_doc(const std::string & filename) {
    open_doc_and_display_gui(filename);
    while (!done_editing()) {
        user_cmd cmd = get_user_input();
        if (cmd.type == OPEN_NEW_INPUT) {
            const std::string new_name = get_filename_from_user();
            std::thread t(edit_doc, new_name);  // 启动新线程
            t.detach();                         // 分离线程
        } else {
            process_user_input(cmd);
        }
    }
}

传递参数给线程

传递自动变量产生的问题

一个引例:

void f(int i, const std::string & s);
std::thread t(f, 3, "hello");

延伸(传递自动变量一定要小心):

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);       // 将 buffer 传送进线程函数
    t.detach();                        // 分离线程,接着本函数返回。buffer 会被销毁,尤其是可能在它从 char * 隐式转换到 std::string 的过程还没完成之前,buffer 就已经被析构了,所以这些代码不是线程安全的
}

改良(先转换再分离):

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, std::string(buffer)); // 将 buffer 转换为 std::string 后传送进线程函数,确保分离之后是安全的
    t.detach();                               // 分离线程,接着本函数返回
}

传递引用产生的问题

示例:

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 的引用
    display_status();
    t.join();
    process_widget_data(data);
}

根据 std::thread 构造函数的机制,它在默认情况下只会拷贝对象(作为右值,为了照顾只能移动构造的对象),而不是真的传递引用。

上述代码将导致一个编译错误,因为没办法将一个右值传递到非 const 的左值引用。

所以需要使用 std::ref 将参数包装成引用的形式,故而改为如下代码。

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

传递只可移动的对象

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)); // 传递只可移动的 unique_ptr 对象
📅 更新时间:2021/10/29 Friday 23:14

🖊️ 本文由 Alone Café 创作,如果您觉得本文让您有所收获,请随意赞赏 🥺
⚖️ 本文以 CC BY-NC-SA 4.0,即《署名-非商业性使用-相同方式共享 4.0 国际许可协议》进行许可
👨‍⚖️ 本站所发表的文章除注明转载或出处外,均为本站作者原创或翻译,转载前请务必署名并遵守上述协议
🔗 原文链接:https://alone.cafe/2021/10/cpp并发编程笔记-1
📅 最后更新:2021年10月29日 Friday 23:14

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×