Skip to content

线程

概述

对于早期的C++语言而言,如果想使用线程,我们需要根据不同的平台使用不用的接口,比如:在Linux平台上,我们需要借助POSIX标准的线程库,在windows上需要借助windows线程库,因为C++自己没有独立的线程库。为了解决这个问题,在C++11标准中,做了完善,C++自己引入了与平台无关的线程库,这个库是语言层面的库,这就是C++11线程库,接下来我们就来学习一下C++11线程库的知识点。

头文件

cpp
#include<thread>

函数接口

在C++11中,将线程封装成了类的概念,下面就是线程类的构造函数形式。

cpp
thread() noexcept; //(1)

thread( thread&& other ) noexcept; //(2)

template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args ); //(3)

thread(const thread&) = delete;//(4)

从上面的几种形式可以看出,第一种形式,可以创建一个空的线程对象,但是线程创建出来之后,需要做任务,单独使用这种形式没有意义;第二种形式,可以从另外一个线程对象转移过来;第三种形式,传递任何可调用对象的形式,这种形式使用的最为通用;第四种形式,表明线程对象不能进行复制。

线程启动

线程的启动,也就是线程入口函数的传递方式。虽然函数接口中有多种形式,可以创建无参对象(线程没有入口函数,也就是线程不做任务),但是这种没有什么意义,一般不使用;可以将两外一个线程对象移动过来,但是移动之后,另外一个线程对象就没有了,这种一般用的也不多,接下来我们重点研究第三种接口形式。

传递普通函数

cpp
void func() {
    cout << "void func()" << endl;
}

void test() {
    thread th1(func);
}

传递函数指针(引用)

cpp
void func() {
    cout << "void func()" << endl;
}

void test() {
    void (*pFunc)() = func;
    thread th1(pFunc);
}

void test2() {
    void (&rFunc)() = func;
    thread th1(rFunc);
}

传递函数对象

cpp
class Example {
public:
    void operator()(int x) {
        cout << "void operator()()" << endl;
        cout << "x = " << x << endl;
    }
};

void test() {
    Example ex;
    thread th1(ex, 10);
}

传递lambda表达式

cpp
void test() {
    int a = 10;
    thread th1([&a](){
        a = 100;
        cout << "a = " << a << endl;
    })
};

传递function对象

cpp
void func() {
    cout << "void func()" << endl;
}

void test() {
    function<void()> f = bind(&func);
    thread th1(f);
}

线程终止

我们使用std::thread创建的线程对象是进程中的子线程,一般进程中还有主线程,在程序中就是main线程,那么当我们创建线程后至少是有两个线程的,那么两个线程谁先执行完毕谁后执行完毕,这是随机的,但是当进程执行结束之后,主线程与子线程都会执行完毕,进程会回收线程拥有的资源。并且,主线程main执行完毕,其实整个进程也就执行完毕了。一般我们有两种方式让子线程结束,一种是主线程等待子线程执行完毕,我们使用join函数,让主线程回收子线程的资源;另外一种是子线程与主线程分离,我们使用detach函数,此时子线程驻留在后台运行,这个子线程就相当于被C++运行时库接管,子线程执行完毕后,由运行时库负责清理该线程相关的资源。使用detach之后,表明就失去了对子线程的控制。

join函数

cpp
void func() {
    cout << "void func()" << endl;
    cout << "I'm child thread" << endl;
}

void test() {
    cout << "I'm main thread" << endl;
    thread th1(func);
    th1.join();//主线程等待子线程
}

这种等待子线程结束的方式是最常见的,也是最容易理解的,类似POSIX标准线程库中的pthread_join函数。

detach函数(了解)

cpp
void threadFunc() {
    cout << "I'm child thread 1" << endl;
    cout << "I'm child thread 2" << endl;
    cout << "I'm child thread 3" << endl;
    cout << "I'm child thread 4" << endl;
    cout << "I'm child thread 5" << endl;
}

int main(void) {
    thread th1(threadFunc);
    th1.detach();

    cout << "I'm main thread 1" << endl;
    cout << "I'm main thread 2" << endl;
    cout << "I'm main thread 3" << endl;
    cout << "I'm main thread 4" << endl;
    cout << "I'm main thread 5" << endl;
    
    return 0; 
}

执行上述代码,会发现并不是执行完子线程,再接着执行完主线程后面的代码,如果将detach()函数换成join()函数,就是先执行完子线程,然后执行子线程。

使用deatch可以让主线程与子线程分离,主线程不必回收子线程,但是有可能主线程在子线程之前结束,如果子线程使用了主线程中的局部变量,那么就会出现很多问题。

传递局部变量(对象)

cpp
void threadFunc(const int &value, char *pstr) {
    cout << "I'm child thread" << endl;
    printf("&value = %p\n", &value);
    //value的地址与num的地址不一致
    
    printf("pstr = %p\n", pstr);
    //与str指向变量的地址一样,如果主线程先执行完,pstr指向为空
}

int main(void) {
    //传递局部(变量)对象作为参数详解
    int num = 10;
    char str[] = "hello";
    printf("&num = %p\n", &num);
    printf("str = %p\n", str);

    thread th1(threadFunc, num, str);
    //num与str使用的值传递
    
    // th1.join();
    th1.detach();
    
    cout << "I'm main thread" << endl;
    
    return 0; 
}

隐式类型转换

cpp
void threadFunc(int value, const string &s) {
    cout << "I'm child thread" << endl;
    printf("&value = %p\n", &value);
    printf("s.c_str() = %p\n", s.c_str());
    printf("s.c_str() = %s\n", s.c_str());
}
int main(void) {
    int num = 10;
    char str[] = "hello";
    printf("&num = %p\n", &num);
    printf("str = %p\n", str);

    thread th1(threadFunc, num, str); 
    // str会隐式转换为string(str)
    
    th1.detach();
    cout << "I'm main thread" << endl;
    
    return 0;
}

疑问:在(1)处,因为str的类型与threadFunc中s的类型不匹配,那么就需要从str隐式转换为string(str),但是有没有这种情况呢,这个隐式转换的时机比较晚,在main执行完成后,str都已经回收了,才进行了隐式转换。那这样不就是有问题的吗?可以思考一下怎么处理这个问题呢?

解决方案:将(1)处的str隐式转换的过程显示写出来即可。

cpp
threadth1(threadFunc,num,string(str));

所以,可见使用detach函数会有很多坑,如果没有必要,可以直接使用join函数即可。

线程的状态

线程类中有一成员函数joinable,可以用来检查线程的状态。如果该函数为true,表示可以使用join()或者detach()函数来管理线程生命周期。

cpp
void test() {
    thread t([] {
        cout << "Hello, world!" << endl;
    });

    if (t.joinable()) {
        t.detach();
    }
}

void test2() {
    thread th1([] {
        cout << "Hello, world!" << endl;
    });
    
    if (t.joinable()) {
        t.join();
    }
}

线程id

为了唯一标识每个线程,可以给每个线程一个id,类型为 std::thread::id,可以使用成员函数get_id()进行获取。

cpp
void test() {
    thread th1([]() {
        cout << "子线程ID:" << std::this_thread::get_id() << endl;
    });
    th1.join();
}