《C++并发编程实战》精读总结:第二章 线程管控

目录

1. 发起线程——thread<>

1.1 用函数对象表示线程任务

1.2 lambda表达式

2. 向线程任务传递参数

2.1 传递的参数是引用时

2.2 传递的参数是智能指针时

3. 等待线程完成——join()

4. 后台运行线程——detach()

5. 移交线程归属权——move()

6.选择线程数量及识别线程


 

1. 发起线程——thread<>

发起一个线程的动作两步即可完成,构建thread对象,指明该线程对象要运行的任务。以最简单的任务,一个空函数为例。发起一个线程执行空函数的方法如下,注意理解thread t1是在构建一个名为t1的thread对象,之后t1就与由他发起的线程关联,能够管控线程几乎所有的细节

void print(){cout << "do print function" << endl;}
thread t1(print);

理解了thread对象之后,下面在深入理解一下任务。刚刚的任务是最简单的一种,是一个普通函数,返回为空,也不接收参数。函数返回后,线程即终止。复杂一些的任务就不能用简单的函数表示了,可以用函数对象,函数指针,lambda表达式等。

1.1 用函数对象表示线程任务

// 定义一个函数对象类
class A {
public:
    // 重载 operator(),不接受输入参数
    void operator()() {
        std::cout << "default" << std::endl;
    }
};

int main() {
    
    A a2;
    std::thread t2(a2);  // 调用 a2.operator()()
    t2.join();  // 等待线程结束
    return 0;
}

上面的代码使用了一个函数对象类,这里也可以理解成任务类,复杂的任务都可以丢给这个类,但是必须要重载()符号,这样才能触发线程的启动机制thread t(a)。当然上面的事例比较简单,我们尝试复杂一点点,增加一个传入参数的功能,那样就再提供一个重载函数

#include <iostream>
#include <thread>
using namespace std;

// 定义一个函数对象类
class A {
public:
    // 重载 operator(),接受一个字符串参数
    void operator()(const string& name) {
        cout << name << endl;
    }

    // 重载 operator(),不接受输入参数
    void operator()() {
        cout << "default" << endl;
    }
};

int main() {
    A a1;  // 创建函数对象
    // 将函数对象和参数传递给线程
    thread t1(a1, "input");  // 调用 a1.operator()("input")
    t1.join();  // 等待线程结束
    
    A a2;
    thread t2(a2);  // 调用 a2.operator()()
    t2.join();  // 等待线程结束
    return 0;
}

1.2 lambda表达式

lambda表达式本质上解决了给thread构造函数传递临时变量而不是具名变量的问题。或者说的简单一点,有时候我们并不想显示的给一段函数名称或者在线程这里给一个任务命名,那么就可以使用lambda表达式。事例如下:

int main() {
    string name = "input";
    thread t3([name]{cout << "name" << endl;});
    t3.join(); 
    return 0;
}

2. 向线程任务传递参数

其实上面的代码中,我们已经在给线程中的任务(函数)传递参数了,但是目前为止我们并没有发现给线程上的函数传递参数有什么不同,而这恰恰是在线程上写代码的一个特殊之处,所以这里单独分一个标题来讲。核心思想就是记住:线程具有内部存储空间,参数会先被复制进来,当成临时变量以右值的形式传递给函数

2.1 传递的参数是引用时

所以,和原先传参考虑的不同,如果一旦是传入引用,而线程将他们作为右值传递给函数,与函数预期的传入不符,编译就不能通过。所以需要显式的在传递时表达出来,方法就是通过std::ref()。事例如下:

void f(Data& data);
int main(){

    Data data;
    // thread t4(f, data);
    thread t4(f, ref(data));
    t4.join(); 
    return 0;

}

2.2 传递的参数是智能指针时

单纯是指针时还好,右值传递有点类似于* const p, 不改变指针本身即可,当然如果想要改动指针本身再另外考虑。如果是智能指针,特别是unique_ptr,就会产生一个问题。传递进线程的unique_ptr会以副本的形式被复制到线程的空间中,这与智能存在唯一一个unique_ptr指向对象冲突,这时候就需要用到move()来转移对象的归属权。代码如下。p会先进入线程存储空间,然后将a对象的归属权转移(内部其实发生了拷贝)给任务函数f

void f(unique_ptr<a>); // 线程的任务函数
unique_ptr<a> p; // 待传入线程的参数
thread t5(f, move(p));

3. 等待线程完成——join()

join的操作前面一直在用,我们已经知道如果需要保证新线程的执行,那么在主线程的结束位置,需要加入新线程的join操作。为什么是在主线程结束之前,是因为join()的含义是将隶属于该线程的任何存储空间都清除,所以放在这个位置能保证在主线程执行完之前,新线程也要执行完然后被清除。之前的代码主线程什么操作都没有,我们假设主线程也进行操作。代码如下:

void main_func(){
    cout << "main operation" << endl;
}

int main() {
    string name = "input";
    thread t3([name]{cout << "name" << endl;});

    main_func();
   
    t3.join()

    return 0;
}

按照之前的描述,这么写顺理成章。但是我们忽略了一件事,就是异常。如果在main_func()执行的时候,抛出异常,那么很有可能t3.join()就执行不到。那么线程的资源就无法释放。为了解决这个问题,我们当然可以把t3.join()移动到main_func()之前,但是这么做不能根本解决问题(实际主线程的操作会很复杂,不是总能放在主线程操作之前),同时这也违背了多线程的意愿,那样的话新线程永远先执行。怎么操作可以让这条语句放在主函数操作前,又能在主线程无论什么原因退出时做一遍新线程的join操作呢。RAII手法是个很好的选择,设置一个监控线程类,将join操作放入这个类的析构中,而新线程t3作为监控线程类的成员变量。这样主线程中对的监控线程类因为主线程的无论什么原因他退出都会调用其析构,join()得以执行。代码如下:

class Thread_guard{
   thread& t;
   explicit Thread_guard(thread& t_):t(t_){}
    ~Thread_guard(){
        if(t.joinable())
            t.join();
    }
    // 不自动生成拷贝构造函数和复制函数
    Thread_guard(Thread_guard const&)=delete;
    Thread_guard& operator=(Thread_guard const&)=delete;
};

void main_func(){
    cout << "main operation" << endl;
}
int main() {
    
    string name = "input";
    thread t3([name]{cout << "name" << endl;});
    
    // 用RAII的手法,设置一个监控进程,保证主函数的操作异常不会影响t3线程的释放
    Thread_guard thread_guard1(t3);

    main_func();
    // 原本应该在这里调用t3的join,但是那样主函数抛出异常会导致join无法执行
    // t3.join()
    return 0;
}

4. 后台运行线程——detach()

如果不需要等待新线程的完成,可以使用detach(),这样新线程就会在后台运行,其实就是分离线程和thread对象,thread对象无法再和被分离的线程进行交互,其归属权转移给C++运行库。

5. 移交线程归属权——move()

thread类和unique_ptr类类似,能够掌握资源但是不能复制,这种类提供了归属权的移交方法。最有用的地方时可以将归属权转移到函数内部,函数就能够接收thread实例作为右值传递的参数。我们将上面Thread_guard类做一点修改,就能够用其构建线程,并将线程交由该类管控。代码如下。在构造函数时,显示的将传入的线程归属转移到监控线程类Scoped_thread内部。

class Scoped_thread{
    thread t;
    explicit Scoped_thread(thread t_):t(move(t_)){
        if(!t.joinable())
            throw logic_error("No thread");
    }
    ~Scoped_thread(){t.join();}
    Scoped_thread(Scoped_thread const&) = delete;
    Scoped_thread& operator=(Scoped_thread const&) = delete;
};

int main(){
    Scoped_thread t(thread([]{cout << "name" << endl;}));
    return 0;
}

6.选择线程数量及识别线程

这两部分比较简单,主要知道thread::hardware_concurrency()函数是返回运行中可真正并发的线程数量,以及get_id()成员函数可以用来返回对应线程的id即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值