查看原文
其他

C++之多线程(一)

思想觉悟 思想觉悟 2022-10-09

导读

终于,在万众期待之下,C++11有了自己的线程库,实现了真正意义上的跨平台,今天在了解C++11线程库的同时,也来温习下POSIX线程。

POSIX线程

在C++11之前因为没有C++语言没有语言级别的线程库,所以在Linux上用的都是POSIX线程,POSIX的相关API大概如下:

POSIX函数功能
pthread_create创建线程
pthread_exit退出线程
pthread_join等待线程退出
pthread_cancel取消线程
pthread_detach线程分离
pthread_self获取线程id
pthread_equal对比是否是同一线程,返回0是同一个线程

1、创建线程 创建线程的原型是:

int pthread_create(pthread_t _Nullable * _Nonnull __restrict,
  const pthread_attr_t * _Nullable __restrict,
  void * _Nullable (* _Nonnull)(void * _Nullable),
  void * _Nullable __restrict);

其中pthread_t是线程id,传入一个指针即可,如果创建成功会赋值为所创建线程的id。

pthread_attr_t是创建线程所需的参数,通过这些参数可以定制线程的属性,比如可以指定新建线程栈的大小、调度策略等,如果不需要传NULL即可。

第三个参数是一个函数指针,是线程创建成功所需要具体执行的任务函数。

第四个是线程携带参数,这个参数会在线程的函数指针执行时传递过去。

例如:


class Task{
public:
    void doSomeThing(){
        std::cout << "子线程的线程id:" << pthread_self() << std::endl;
        std::cout << "执行点什么任务吧" << std::endl;
    }
};

void* doWork(void * args){
    Task *task = static_cast<Task*>(args);
    if(nullptr != task){
        task->doSomeThing();
    }
}

int main() {
    std::cout << "主线程的线程id:" << pthread_self() << std::endl;
    // 创建线程
    pthread_t pthread;
    Task task;
    pthread_create(&pthread,nullptr,doWork,&task);
    pthread_join(pthread, nullptr); // 线程等待执行完毕
    return 0;
}

2、线程退出

正确的线程退出方式是在退出线程之后,再连接线程, 因为如果在线程退出后不执行连接操作,线程的资源就不能被释放,也不能被复用,这就造成了资源的泄漏。所谓的连接操作就是在线程退出后使用pthread_join即可。

更多其他的线程函数这里就不展开细说了,因为今天的主要内容还是C++11的线程知识。

更多关于POSIX线程的知识,笔者之前看过《Linux环境编程XXXX》(为避免广告就不写完整书名了)这本书讲得就不错。

C++11的线程

先来看一个C++11多线程的例子:

void doWork() {
    std::cout << "执行点什么任务吧" << std::endl;
}

int main() {
    thread thread(doWork);
    std::cout << "thread.joinable:" << thread.joinable()<< std::endl;
    thread.join();
    std::cout << "thread.joinable:" << thread.joinable()<< std::endl;
    return 0;
}

是不是比POSIX线程简洁得多?

1、线程分离 如果在上面的例子中,我们将join改成detach则表示我们不再接管这个线程了,相当于驻留在后台,完全被C++运行时库所接管了,当线程运行结束后,由C++运行库清理相关的线程资源。

需要说明的是上面的例子如果把join改成detach的话很大可能就看不到输出了,但是这是正常的,而且一旦线程被detach之后就不能再被join了。

对于一个线程我们可以通过joinable函数判断是否可以join或者detach,如果一个线程已经被join或者detach的,那么joinable则会返回false。

2、线程传参

如果在线程执行的时候,我们想给线程传递参数那该怎么办呢?

如果需要给线程传递参数,那么我们只需要在线程执行函数后增加参数,然后将实参传入线程的构造函数中即可,例如:

void doWork(int a) {
    std::cout << "我是传递过来的参数a:" << a << std::endl;
    std::cout << "执行点什么任务吧" << std::endl;
}

int main() {
    thread thread(doWork,100); // 传参数的例子
    thread.join();
    return 0;
}

既然涉及到传参,那么又回到了C++老生常谈的问题了,给线程传递参数,到底是按照值传参呢还是按照指针传参呢还是按照引用传参呢?

首先来看看以下的这个例子:

void doWork(const int &a) {
    this_thread::sleep_for(std::chrono::seconds(1)); // 休眠
    std::cout << "我是传递过来的参数a的地址:" << a << std::endl;
    std::cout << "我是传递过来的参数a:" << a << std::endl;
}

void threadTest(){
    int a = 130;
    std::cout << "a的地址:" << &a << std::endl;
    thread thread(doWork,a);
}

int main() {
    threadTest();
    this_thread::sleep_for(std::chrono::seconds(3)); // 休眠3s,让子线程执行
    return 0;
}

理论上来说上面的这个例子是有问题的,为什么呢?线程是在函数threadTest内开启的,但是函数返回时就销毁了栈变量thread,所以运行的时候我们是看到会报错的,以下是输出:

libc++abi: terminating // 注释,这是报错
a的地址:0x16f2fb7cc

所以如果我们想要在一个函数内部开启一个线程,需要在线程的任务执行结束之前线程变量thread不要被销毁,需要使用堆指针或者局部变量的方式。

首先我们来测试一下值传递的方式:


class Task {
public:

    Task(){
        std::cout << "构造函数,线程id:" << this_thread::get_id() << std::endl;
    }

    Task(const Task &task):a(task.a){
        std::cout << "拷贝构造,线程id:" << this_thread::get_id() << std::endl;
    }

    ~Task(){
        std::cout << "析构函数,线程id:" << this_thread::get_id() << std::endl;
    }

    void doSomeThing() const{
        std::cout << "-------------执行点什么任务吧,现在a是多少:" << a  << std::endl;
    }

public:
    int a;
};

void doWork(Task task) {
    std::cout << "----doWork   线程id:" << this_thread::get_id() << std::endl;
    this_thread::sleep_for(std::chrono::seconds(1)); // 休眠
    std::cout << "我是传递过来的参数地址:" << &task << std::endl;
    // 执行任务
    task.doSomeThing();
}

void threadTest(){
    Task task;
    task.a = 100;
    std::cout << "原始的地址:" << &task << std::endl;
    // 为了不让线程变量销毁,这里用堆指针测试,暂时不考虑指针释放
    thread *th = new thread(doWork,task);
    std::cout << "线程id:" << th->get_id() << std::endl;
    th->join();
}

int main() {
    std::cout << "主线程id:" << std::this_thread::get_id() << std::endl;
    threadTest();
    this_thread::sleep_for(std::chrono::seconds(3)); // 休眠3s,让子线程执行
    return 0;
}

下面是输出:

主线程id:0x1050a4580
构造函数,线程id:0x1050a4580
原始的地址:0x16af237bc
拷贝构造,线程id:0x1050a4580
拷贝构造,线程id:0x1050a4580
析构函数,线程id:0x1050a4580
线程id:0x16afab000
拷贝构造,线程id:0x16afab000
----doWork   线程id:0x16afab000
我是传递过来的参数地址:0x16afaaf3c
-------------执行点什么任务吧,现在a是多少:100
析构函数,线程id:0x16afab000
析构函数,线程id:0x16afab000
析构函数,线程id:0x1050a4580

在上面的测试中我们发现如果是通过值传递的方式的话,如果需要使用多线程,则会经历三次拷贝的过程,这着实是有点性能的消耗了。

不是说通过引用可以减少拷贝吗?那我们来测试下引用的传递的情况是怎么样子的,还是以上的程序,我们修改一下函数doWork

void doWork(const Task &task) {
    std::cout << "----doWork   线程id:" << this_thread::get_id() << std::endl;
    this_thread::sleep_for(std::chrono::seconds(1)); // 休眠
    std::cout << "我是传递过来的参数地址:" << &task << std::endl;
    // 执行任务
    task.doSomeThing();
}

看看输出:

主线程id:0x1041f0580
构造函数,线程id:0x1041f0580
原始的地址:0x16bcc77bc
拷贝构造,线程id:0x1041f0580
拷贝构造,线程id:0x1041f0580
析构函数,线程id:0x1041f0580
线程id:0x16bd4f000
----doWork   线程id:0x16bd4f000
我是传递过来的参数地址:0x6000016c1110
-------------执行点什么任务吧,现在a是多少:100
析构函数,线程id:0x16bd4f000
析构函数,线程id:0x1041f0580

对比前面的值传递我们发现是少了一次拷贝,但是不是说值传递调用函数不拷贝吗?这里怎么还是进行了拷贝,而且是两次拷贝呢?其实说引用传递不拷贝是对于直接调用来说的,这里的多线程调用,系统并不会马上给你调用任务函数,而是内部经历了n次的封装才调用到开发者制定的任务函数,所谓的没有中间商赚差价也是相对而言的...

** 注意:虽然是使用了引用,但是内部还是经历了拷贝,那么如果希望通过传递引用,然后在线程函数内部修改值然后在线程函数外起作用这个想法就行不通了哦。**

再来看看指针的传递,我们修改下函数threadTestdoWork

void doWork(Task *task) {
    std::cout << "----doWork   线程id:" << this_thread::get_id() << std::endl;
    this_thread::sleep_for(std::chrono::seconds(1)); // 休眠
    std::cout << "我是传递过来的参数地址:" << task << std::endl;
    // 执行任务
    task->doSomeThing();
}

void threadTest(){
    Task *task = new Task;
    task->a = 100;
    std::cout << "原始的地址:" << task << std::endl;
    // 为了不让线程变量销毁,这里用堆指针测试,暂时不考虑指针释放
    thread *th = new thread(doWork,task);
    std::cout << "线程id:" << th->get_id() << std::endl;
}

输出:

主线程id:0x1009fc580
构造函数,线程id:0x1009fc580
原始的地址:0x6000033ac030
线程id:0x16f63b000
----doWork   线程id:0x16f63b000
我是传递过来的参数地址:0x6000033ac030
-------------执行点什么任务吧,现在a是多少:100

厉害了,居然没有发生对象的拷贝,虽然内部还是发生了指针的拷贝,但是这个损耗是可以忽略不计的,看来如果要给多线程传参还是得指针呀。

既然是指针,又要陷入谁维护,谁释放的漩涡了。。。

试下智能指针?还是修改下函数threadTestdoWork

void doWork(const shared_ptr<Task> &ptr) {
    std::cout << "----doWork   线程id:" << this_thread::get_id() << std::endl;
    this_thread::sleep_for(std::chrono::seconds(1)); // 休眠
    std::cout << "我是传递过来的参数地址:" << ptr << std::endl;
    // 执行任务
    ptr->doSomeThing();
}

void threadTest(){
    shared_ptr<Task> ptr = make_shared<Task>();
    ptr->a = 100;
    std::cout << "原始的地址:" << ptr.get() << std::endl;
    // 为了不让线程变量销毁,这里用堆指针测试,暂时不考虑指针释放
    thread *th = new thread(doWork,ptr);
    std::cout << "线程id:" << th->get_id() << std::endl;
}

输出:

主线程id:0x10502c580
构造函数,线程id:0x10502c580
原始的地址:0x6000000d9118
线程id:0x16b033000
----doWork   线程id:0x16b033000
我是传递过来的参数地址:0x6000000d9118
-------------执行点什么任务吧,现在a是多少:100
析构函数,线程id:0x16b033000

小结

1、给线程任务传递参数的话如果是一般的数据类型,比如int等的建议直接使用值传递即可。2、如果是传递复杂的类的参数的话,建议使用智能指针。

推荐阅读

《C++之指针扫盲》
《C++之智能指针》
《C++之指针与引用》
《C++之右值引用》

关注我,一起进步,人生不止coding!!!


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存