15.C++11线程—线程管控(笔记)


内容概要:

  • 启动线程,并通过几种方式为新线程指定运行代码
  • 等待线程完成和分离线程并运行
  • 唯一识别线程

1 线程的基本管控

每个c++程序都含有至少一个线程,即运行main()的线程,它由c++运行时(C++ runtime)系统启动。随后,程序就可以发起更多的线程,它们以别的函数作为入口(entry point)。这些新线程连同起始线程并发运行。

main()返回时,程序就会退出;同样,当入口函数返回时,对应的线程随之终结。我们会看到,如果借std::thread对象管控线程,即可选择等它自然结束。

1.1 发起线程

线程通过构建std::thread对象而启动,该对象指明线程要运行的任务。

#include <thread>
void do_some_work();
std::thread my_thread(do_some_work);

与c++标准库中的许多类型相同,任何可调用类型(callable type)都适用于std::thread。于是,可以设计一个带有函数调用操作符(function call operator)的类,并将该类的实例传递给std::thread的构造函数:

class background_task
{
public:
    void operator()() const
    {
        do_something();
        do_something_else();
    }
};
background_task f;
std::thread my_thread(f);

构造std::thread实例时,提供了函数对象f作为参数,它被复制到属于新线程的存储空间中,并在那里被调用,由新线程执行。故此,副本的行为必须与原本的函数对象等效,否则运行结果可能有违预期。

将函数对象传递给std::thread的构造函数时,要注意防范所谓的"C++最麻烦的解释"(C++‘s most vexing parse)。如果,传入的是临时变量,而不是具名变量,那么调用构造函数的语法有可能与函数声明相同。遇到这种情况,编译器就会将其解释成函数声明,而不是定义对象。

例如:std::thread my_thread(background_task());

为临时函数对象命名即可解决问题,做法是多用一对圆括号,或采用新式的统一初始化语法(uniform initialization syntax,又名列表初始化),例如:

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

为了避免这种问题,还能采用lambda表达式。

std::thread my_thread([]{
    do_something();
    do_something_else();
});

一旦启动了线程,就需要明确是等待它结束,还是由它独自运行。假如std::thread对象销毁之际还没有决定好,那std::thread的析构函数将调用std::terminate()终止整个程序。

如果选择了分离,且分离时新线程还未结束运行,那它将继续运行,甚至在std::thread对象销毁很久之后依然运行,它只有最终从线程函数返回时才会结束运行。那么在线程运行结束前,我们需保证它所访问的外部数据始终正确,有效。例如:

struct func
{
    int& i;
    func(int& i_):i(i_){}
    void operator()()
    {
        for(unsigned j=0;j<10000000;++j)
        {
            do_something(i);
        }
    }
};
void oops()
{
    int some_local_state = 0;
    func my_func(some_local_state);
    std::thread my_thread(my_func);
    my_thread.detach();
}

因此,以下做法极不可取:意图在函数中创建线程,并让线程访问函数的局部变量。除非线程肯定会在该函数退出前结束。

处理上述问题的另一种方法是,汇合新线程,此举可以确保在主线程的函数退出前,新线程执行完毕。

1.2 等待线程完成

若需等待线程完成,可调用std::thread实例的成员函数join()实现。

只要调用了join(),隶属与该线程的任何存储空间即会因此消除,std::thread对象遂不再关联到已结束的线程。事实上,它与任何线程均无关联。其中的意义是,对于某个给定的线程,join()仅能调用一次;只能std::thread对象增经调用过join(),线程就不再可汇合(joinable),成员函数joinable()将返回false

1.3 在出现异常的情况下等待

join()的调用需要小心地选择执行代码的位置,原因是,如果线程启动以后有异常抛出,而join()尚未执行,则该join()调用会被略过。

一般地,若调用join()仅仅是为了处理没有出现异常的情况,那么万一发生异常,我们也仍需调用join(),以避免意外的生存期问题。

struct func;
void f()
{
    int some_local_state = 0;
    func my_func(some_local_state);
    std::thread t(my_func);
    try
    {
        do_something_in_current_thread();
    }
    catch(...)
    {
        t.join();
        throw;
    }
    t.join();
}

try/catch块的使用则保证了新线程在函数f()退出前终结(无论f()是正常退出还是因为异常而退出)。try/catch块稍显冗余,还容易引发作用域轻微错乱。假如代码必须确保新线程先行结束,之后当前线程的函数才退出,那么关键在于,全部可能得退出路径都必须保证这种先后次序,无论是正常退出,还是抛出异常。

为实现此目标,可以运用标准的RAII手法,在其析构函数中调用join(),代码如下:

class thread_guard
{
    std::thread& t;
public:
    explicit thread_guard(std::thread& t):t(t_){}
    ~thread_guard()
    {
        if(t.joinable())
            t.join();
    }
    thread_guard(thread_guard const&)=delete;
    thread_guard& operator=(thread_guard const&)=delete;
};
struct func;
void f()
{
    int some_local_state = 0;
    func my_func(some_local_state);
    std::thread t(my_func);
    thread_guard g(t);
    do_something_in_current_thread();
}

当主线程执行到f()末尾时,按构建的逆序,全体局部对象都会被销毁。因此类型thread_guard的对象g首先被销毁,在其析构函数中,新线程汇合。即便do_something_in_current_thread()抛出异常,函数f()退出,以上行为仍然会发生。

thread_guard的析构函数先调用join

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值