C++11多线程: thread创建线程的三种方式

C++11 多线程相关的头文件

  • <thread>:该头文件用于线程操作,主要声明了 std::thread 类,另外 std::this_thread 命名空间也在该头文件中,包含一些线程的操作函数。
  • <mutex>:该头文件用于互斥量操作,主要声明了与互斥量相关的类,包括 std::mutex 系列类,std::lock_guard,std::unique_lock,以及其他的类型和函数。
  • <condition_variable>:该头文件用于条件变量操作,主要声明了与条件变量相关的类,包括 std::condition_variable 和 std::condition_variable_any。
  • <future>:该头文件用于异步调用操作,主要声明了 std::promise,std::package_task 两个 Provider 类,以及 std::future 和 std::shared_future 两个 Future 类,另外还有一些与之相关的类型和函数,std::async() 函数就声明在此头文件中。
  • <atomic>:该头文件用于原子操作,主要声明了两个类,std::atomic 和 std::atomic_flag,另外还声明了一套 C 风格的原子类型和与 C 兼容的原子操作的函数。

thread 详解

default (1)thread() noexcept;
initialization (2)template explicit thread (Fn&& fn, Args&&… args);
copy [deleted] (3)thread (const thread&) = delete;
move (4)thread (thread&& x) noexcept;
  • 默认构造函数,创建一个空的 thread 执行对象。
  • 初始化构造函数,创建一个 thread 对象,该 thread 对象可被 joinable,新产生的线程会调用 fn 函数,该函数的参数由 args 给出。
  • 拷贝构造函数(被禁用),意味着 thread 不可被拷贝构造。
  • move 构造函数,move 构造函数,调用成功之后 x 不代表任何 thread 执行对象。
    线程对象可以被 move,但是不能被拷贝。而对于 move 赋值操作,如果当前对象不可 joinable,需要传递一个右值引用给 move 赋值操作;如果当前对象可被 joinable,则 terminate() 报错。关于线程是否 joinable,可以通过调用 joinable() 来获得,更多关于 joinable 的资料,可以参考 std::thread::joinable
    注意:可被 joinable 的 thread 对象必须在他们销毁之前被主线程 join 或者将其设置为 detached。
  • std::thread 创建线程,需要提供线程函数或者函数对象,并可以同时指定线程的参数。
  • join函数将会阻塞线程,直到线程函数执行结束,如果线程函数有返回值,返回值将被忽略。join()函数的另一个任务是回收该线程中使用的资源。
  • detach可以将线程与线程对象分离,让线程作为后台线程执行,当前线程也不会阻塞了。但是detach之后就无法在和线程发生联系了。如果线程执行函数使用了临时变量可能会出现问题,因为线程调用了detach之后是在后台运行,临时变量可能已经销毁,那么线程会访问已经被销毁的变量会报错。而join能保证。
    线程对象和对象内部管理的线程的生命周期并不一样,如果线程执行的快,可能内部的线程已经结束了,但是线程对象还活着,也有可能线程对象已经被析构了,内部的线程还在运行。
    总结(针对detach())
    (1)若传递int这种简单类型参数,建议都是值传递,不要用引用
    (2)如果传递类对象,避免隐式类型转换。全部都要在创建线程就构建临时对象,然后函数参数中用引用,否则系统还会构造一次
  • joinable函数判断一个线程对象能否调用join()。

除了 join,detach,joinable 之外,std::thread 头文件还在 std::this_thread 命名空间下提供了一些辅助函数:

  • get_id: 返回当前线程的 id
  • yield: 告知调度器运行其他线程,可用于当前处于繁忙的等待状态
  • sleep_for:给定时长,阻塞当前线程
  • sleep_until:阻塞当前线程至给定时间点
    其中 yield 是一个特殊的“线程睡眠”函数:
    std::this_thread::yield() 是将当前线程所抢到的 CPU ”时间片A”让渡给其他线程(其他线程会争抢”时间片A”,注意。此时”当前线程”不参与争抢。等到其他线程使用完”时间片A”后,再由操作系统调度,当前线程再和其他线程一起开始抢 CPU 时间片。
    如果将 std::this_thread::yield() 上述语句修改为: return;,则将未使用完的 CPU ”时间片A”还给操作系统,再由操作系统调度,当前线程和其他线程一起开始抢CPU时间片。
    因此 yield 使用的场景就是当当前线程运行条件不满足时调用,避免一个线程频繁与其他线程争抢 CPU 时间片, 从而导致多线程处理性能下降。sleep_for 也是让线程等待,需要等待若干时间。
    Linux 下编译: g++ –std=c++11 sample.cpp -lpthread

thread多线程创建

std::thread类的构造函数是使用可变参数模板实现的,也就是说,可以传递任意个参数,第一个参数是线程的入口函数,而后面的若干个参数是该函数的参数。

我们可以给 std::thread 对象添加函数,这个回调函数将在这个新线程启动时执行。这些回调可以是:
1.) 函数指针
2.) 函数对象
3.) Lambda 函数

//创建 thread 对象:
std::thread thObj(<CALLBACK>);

新线程将在创建新对象后立即启动,并将并行地执行(当参数)传递给线程的回调函数。

使用函数指针创建线程

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

//传入的不是myNum的引用,指向地址不同,实际是值传递,即便主线程detach(),子线程使用num不会出问题
//传入的指针依旧是指向muBuf的地址,所以detach后,子线程会出问题。可以将char*改为const string&
//使用const string&也有问题,即:不知道mybuf不知道什么时候转换为string,如果主线程执行完毕后,还没转换完毕,就有问题
void test(const int& num, char* buf) {
    for(int i = 0; i < 5; i++){
        cout << "线程1开始执行" << endl;
        cout << "num1 = " << num << endl;
        cout << "buf1 = " << buf << endl;
        cout << "线程1执行完毕" << endl;
    }

}

void test2(const int& num, const string& buf) {
    for(int i = 0; i < 5; i++) {
        cout << "线程2开始执行" << endl;
        cout << "num2 = " << num << endl;
        cout << "buf2 = " << buf << endl;
        cout << "线程2执行完毕" << endl;
    }
}
int main() {
    int myNum = 1;
    int& num = myNum;
    char myBuf[] = "only for test!";
    const string& myBuf2 = "buf2 only for test!";
    thread th(test, num, myBuf);
    thread th2(test2, num, myBuf2);
    //thread th2(test2,num,string(myBuf));// 针对detach ,生成一个临时string对象,绑定const string&,使用这个方法,就会让string对象在主线程执行完毕前构造 在创建线程的同时构造临时对象是可行的
    th.join();
    //th2.detach();
    th2.join();
    cout << "主线程执行完毕" << endl;
    return 0;
}

使用类对象创建线程

//使用类对象创建线程
#include<iostream>
#include<thread>
using namespace std;

class Test {
public:
    Test(){
        cout << "Test()构造函数被执行" << endl;
    }
    Test(const Test& test) {
        cout << "拷贝构造函数执行" << endl;
    }
    ~Test() {
        cout << "析构函数执行" << endl;
    }
    void operator()() {//不能带参数
        cout << "线程开始执行" << endl;
        for (int i = 0; i < 10; ++i);
        cout << "线程执行完毕" << endl;
    }
};

int main() {
    Test test;
    thread th(test);
    th.detach();
    //th.join()
    for (int i = 0; i < 10; ++i) {
        cout << "主线程执行完毕" << i << endl;
    }
    return 0;
}

输出

Test()构造函数被执行
拷贝构造函数执行//证明确实复制了一份对象
拷贝构造函数执行
析构函数执行
主线程执行完毕0
主线程执行完毕1
主线程执行完毕2
主线程执行完毕3
主线程执行完毕4
主线程执行完毕5
主线程执行完毕6
主线程执行完毕7
主线程执行完毕8
主线程执行完毕9
析构函数执行//线程还没有执行完毕,所以这个析构是主线程对象
线程开始执行
线程执行完毕
析构函数执行

detach换成join的输出

Test()构造函数被执行
拷贝构造函数执行
拷贝构造函数执行
析构函数执行
线程开始执行
线程执行完毕
析构函数执行//此时主线程还未退出,但该线程执行完毕,所以这个析构对象是析构复制对象的
主线程执行完毕0
主线程执行完毕1
主线程执行完毕2
主线程执行完毕3
主线程执行完毕4
主线程执行完毕5
主线程执行完毕6
主线程执行完毕7
主线程执行完毕8
主线程执行完毕9
析构函数执行//此时主线程退出,所以这个析构对象是主函数中的对象

线程参数为引用时修改值,不会影响主线程的值

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

class A {
public:
    mutable int num;//标注为const也能修改
    //类型转换构造函数,可以把一个int转换成一个类A对象
    A(int num) :num(num) {
        cout << "构造函数执行" << this << "threadid=" << std::this_thread::get_id() << endl;
    }
    A(const A& a) :num(a.num) {
        cout << "拷贝构造函数执行" << this << "threadid=" << std::this_thread::get_id() << endl;
    }
    ~A() {
        cout << "析构函数执行" << this << "threadid=" << std::this_thread::get_id() << endl;
    }
};

void test(const A& a) {
    a.num = 199;//修改不会影响main的值
    cout << "线程test的a.num=" << a.num << endl;
    cout << "线程test的参数地址是" << &a << "threadid=" << std::this_thread::get_id() << endl;
}

int main() {
    cout << "主线程id是" << std::this_thread::get_id() << endl;
    A a(10);
    thread th(test, a);//将类对象作为线程参数
    th.join();
    cout << "主线程的a.num=" << a.num << endl;
    cout << "主线程执行完毕" << endl;
    return 0;
}

输出

主线程id是140575262807872
构造函数执行0x7ffd04eaf38cthreadid=140575262807872
拷贝构造函数执行0x7ffd04eaf340threadid=140575262807872
拷贝构造函数执行0x56237eb422c8threadid=140575262807872
析构函数执行0x7ffd04eaf340threadid=140575262807872
线程test的a.num=199
线程test的参数地址是0x56237eb422c8threadid=140575262803712
析构函数执行0x56237eb422c8threadid=140575262803712
主线程的a.num=10
主线程执行完毕
析构函数执行0x7ffd04eaf38cthreadid=140575262807872

std::ref函数,可以让线程传入的参数不被复制(注:没使用detach())

//将线程传入参数加上std::ref
thread th(test, std::ref(a));
//主线程的a.num = 199,被成功修改

传递智能指针

#include<iostream>
#include<thread>

using namespace std;

void test(unique_ptr<int> uptr) {
    cout << "子线程id是" << std::this_thread::get_id() << endl;
    cout <<  "当前智能指针的值是" << *uptr << endl;
}

int main() {
    cout << "主线程id是" << std::this_thread::get_id() << endl;
    unique_ptr<int> uptr(new int(10));
    cout << "当前智能指针的值为"  << *uptr << endl;
    thread th(test, std::move(uptr));//将类对象作为线程参数
    th.join();
    cout << "主线程执行完毕" << endl;
    return 0;
}

若改为detach(),当主线程执行完毕后,uptr被释放,但子线程还在执行,此时就会出现问题

成员函数指针做线程函数

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

class A {
public:
    mutable int num;
    //类型转换构造函数,可以把一个int转换成一个类A对象
    A(int num) :num(num) {
        cout << "构造函数执行, " << this << "threadid=" << std::this_thread::get_id() << endl;
    }
    A(const A& a) :num(a.num) {
        cout << "拷贝构造函数执行, " << this << "threadid=" << std::this_thread::get_id() << endl;
    }
    ~A() {
        cout << "析构函数执行, " << this << "threadid=" << std::this_thread::get_id() << endl;
    }
    void thread_work(int num) {
        num = 199;//修改不会影响main的值
        cout << "子线程num=" << num << endl;
        cout << "子线程thread_work执行了, " << "threadid=" << std::this_thread::get_id() << endl;
    }
};

void test(const A& a) {
    a.num = 199;//修改不会影响main的值
    cout << "a.num=" << a.num << endl;
    cout << "线程test的参数地址是" << &a << "threadid=" << std::this_thread::get_id() << endl;
}

int main() {
    cout << "主线程id是" << std::this_thread::get_id() << endl;
    A a(10);
    int num = 100;
    //第一个参数是对象函数指针,第二个参数对象,其后参数为函数所需参数
    thread th(&A::thread_work, a,num);
    th.join();
    cout << "主线程的num=" << num << endl;
    cout << "主线程的a.num=" << a.num << endl;
    cout << "主线程执行完毕" << endl;
    return 0;
}

使用 Lambda 函数创建线程

#include<iostream>
#include<thread>
using namespace std;
int main() {
    auto test = [] {
        cout << "线程开始执行" << endl;
        for (int i = 0; i < 10; ++i);
        cout << "线程执行完毕" << endl;
    };
    thread th(test);
    th.join();
    cout << "主线程执行完毕"  << endl;
    return 0;
}

如何区分线程: 线程ID

每个 std::thread 对象都有一个 ID,使用下面的函数可以获取:

std::thread::get_id()

如果 std::thread 对象没有和任何对象关联,则 get_id() 函数会返回默认构造的 std::thread::id 对象,即“非线程”。
获取当前线程的 ID:

std::this_thread::get_id()

用法示例

#include <iostream>
#include <thread>
void thread_function()
{
    std::cout<<"Inside Thread :: ID  = "<<std::this_thread::get_id()<<std::endl;
}
int main()
{
    std::thread threadObj1(thread_function);
    std::thread threadObj2(thread_function);

    if(threadObj1.get_id() != threadObj2.get_id())
        std::cout<<"Both Threads have different IDs"<<std::endl;

    std::cout<<"From Main Thread :: ID of Thread 1 = "<<threadObj1.get_id()<<std::endl;
    std::cout<<"From Main Thread :: ID of Thread 2 = "<<threadObj2.get_id()<<std::endl;

    threadObj1.join();
    threadObj2.join();
    return 0;
}
#include<iostream>
#include<thread>
using namespace std;

class A {
public:
    int num;
    //类型转换构造函数,可以把一个int转换成一个类A对象
    A(int num) :num(num) {
        cout << "构造函数执行" << this << "threadid=" << std::this_thread::get_id() << endl;
    }
    A(const A& a) :num(a.num) {
        cout << "拷贝构造函数执行" << this << "threadid=" << std::this_thread::get_id() << endl;
    }
    ~A() {
        cout << "析构函数执行" << this << "threadid=" << std::this_thread::get_id() << endl;
    }
};

void test(const A& a) {
    cout << "线程test的参数地址是" << &a << "threadid=" << std::this_thread::get_id() << endl;
}

int main() {
    cout << "主线程id是" << std::this_thread::get_id() << endl;
    int num = 2;
    thread th(test, num);
    th.join();
    cout << "主线程执行完毕" << endl;
    return 0;
}

参考

C++11 并发编程系列(一):多线程初探(thread)
[c++11]多线程编程(二)——理解线程类的构造函数
C++11多线程编程
c++11线程基本用法
[c++11]多线程编程(一)——初识

  • 3
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值