C++之多线程(一)

简介: C++进阶之多线程上

导读

终于,在万众期待之下,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++之右值引用》

目录
相关文章
|
1月前
|
缓存 安全 C++
C++无锁队列:解锁多线程编程新境界
【10月更文挑战第27天】
55 7
|
1月前
|
消息中间件 存储 安全
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
60 1
|
2月前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
|
2月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
82 6
|
2月前
|
缓存 负载均衡 Java
c++写高性能的任务流线程池(万字详解!)
本文介绍了一种高性能的任务流线程池设计,涵盖多种优化机制。首先介绍了Work Steal机制,通过任务偷窃提高资源利用率。接着讨论了优先级任务,使不同优先级的任务得到合理调度。然后提出了缓存机制,通过环形缓存队列提升程序负载能力。Local Thread机制则通过预先创建线程减少创建和销毁线程的开销。Lock Free机制进一步减少了锁的竞争。容量动态调整机制根据任务负载动态调整线程数量。批量处理机制提高了任务处理效率。此外,还介绍了负载均衡、避免等待、预测优化、减少复制等策略。最后,任务组的设计便于管理和复用多任务。整体设计旨在提升线程池的性能和稳定性。
86 5
|
2月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
42 0
|
2月前
|
资源调度 Linux 调度
Linux C/C++之线程基础
这篇文章详细介绍了Linux下C/C++线程的基本概念、创建和管理线程的方法,以及线程同步的各种机制,并通过实例代码展示了线程同步技术的应用。
34 0
Linux C/C++之线程基础
|
4月前
|
Java 调度
基于C++11的线程池
基于C++11的线程池
|
4月前
|
Dart 编译器 API
Dart ffi 使用问题之在C++线程中无法直接调用Dart函数的问题如何解决
Dart ffi 使用问题之在C++线程中无法直接调用Dart函数的问题如何解决