thread库的使用

thread库简介

  thread类用于表示一个独立执行的线程。一个执行线程是一个指令序列,它可以再多线程环境中与其他类似的指令序列并发执行。这些并发的线程共享相同的地址空间。
  一个初始化过的线程对象就是自动执行的,这样的线程是可以join的(即可以调用join()函数来阻塞调用该函数的线程),并且具有唯一的线程id。但是,默认的构造函数是不能join的,这些不可join的线程都有一样的线程id。
  一个可以join的线程如果使用move转移了所有权,或者调用了join及detach,那么该线程就会变为不可join的线程。

成员函数

构造函数

  thread的构造函数有以下四个重载:

默认构造函数

thread() noexcept;

  构造一个不代表任何执行线程的线程对象。

初始化构造函数

template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

  该函数使用可变参数模板来构造一个线程对象,用来代表一个新的可join的执行线程。这个执行线程通过可变参数传入线程函数对象fn,以及函数的参数列表(参数列表使用左值或者右值引用的退化拷贝,可简单理解为通过传值的方式将参数传给该构造函数)。

拷贝构造函数

thread (const thread&) = delete;

  线程对象是不支持拷贝构造的,因此模式使用了delete的描述符来删除拷贝构造函数。

移动构造函数

thread (thread&& x) noexcept;

  转移参数x所代表的可执行指令的所有权,但是并不会影响线程的执行。转移后,参数x不再代表任何执行线程。

析构函数

  析构函数用于销毁线程,如果该线程是可join的,那么析构函数会调用terminate()函数来终止线程,示例如下所示:

#include<bits/stdc++.h>
using namespace std;
void printHelloWorld(){
    // 每一秒输出一次Hello World
    while(true){
        cout<<"Hello World!"<<endl;
        this_thread::sleep_for(1s);
    }
}
int main(){
    thread t1(printHelloWorld);
    //等待线程t1执行3s,然后终止线程执行
    this_thread::sleep_for(3s);
    t1.~thread();
    return 0;
}

  输出结果如下:

Hello World!
Hello World!
Hello World!
terminate called without an active exception

  可见,thread对象在被销毁前,应当尽可能将其join或者detach,以防止执行线程因为对象的销毁而终止。

operator=

  函数声明如下所示:

thread& operator= (thread&& rhs) noexcept;
thread& operator= (const thread&) = delete;

  thread对象不允许拷贝构造,同样的对于赋值操作符的重载实质是移动赋值。如果当前threa对象是非joinable的,那么它将获得参数thread对象所代表的执行线程,否则将会从调用terminate()函数终止当前线程。示例如下所示:

#include<bits/stdc++.h>
using namespace std;
void printHelloWorld(){
    cout<<"Hello World!"<<endl;
}
int main(){
    thread t1;
    t1 = thread(printHelloWorld);
    t1.join();
    return 0;
}

  输出结果如下所示:

Hello World!

get_id

  该函数返回线程id,如果此线程是joinable的,那么该函数返回一个代表此线程的唯一标识;否则,该函数返回一个默认构造函数生成的thead::id类。
  示例如下:

#include<bits/stdc++.h>
using namespace std;
static thread::id main_thread_id = std::this_thread::get_id();
void is_main_thread(){
    if(main_thread_id == std::this_thread::get_id())
        cout << "This is main thread" << endl;
    else
        cout << "This is not main thread" << endl;
}
int main(){
    is_main_thread();
    thread t1;
    t1 = thread(is_main_thread);
    if(t1.joinable()){
        cout<<"t1 is joinable, thread_id = "<<t1.get_id()<<endl;
        t1.join();
    } else {
        cout<<"t1 is not joinable, thread_id = "<<t1.get_id()<<endl;
    }

    thread t2;
    if(t2.joinable()){
        cout<<"t2 is joinable, thread_id = "<<t2.get_id()<<endl;
        t2.join();
    } else {
        cout<<"t2 is not joinable, thread_id = "<<t2.get_id()<<endl;
    }
    return 0;
}

  输出结果如下所示:

This is main thread
t1 is joinable, thread_id = This is not main thread
2
t2 is not joinable, thread_id = thread::id of a non-executing thread

joinable

  函数声明如下所示:

bool joinable() const noexcept;

  函数返回bool类型,标识当前的thread对象能否jion。
  如果thread对象代表了一个执行线程,那么该对象是joinable的。
  如果thread对象满足以下三种情况,那么该线程对象就是非joinable的:
  (1)使用默认构造函数构造的;
  (2)已经使用移动语义转移线程所有权的;
  (3)其成员函数join或者detach已经被调用过的。
  示例如下:

#include<bits/stdc++.h>
using namespace std;
void printHelloWorld(){
    cout<<"Hello World!"<<endl;
}
int main(){
    thread t1;
    if(!t1.joinable()){
        cout<<"Thread is not joinable! (default constructor)"<<endl;
    }
    t1 = thread(printHelloWorld);
    if(t1.joinable()){
        cout<<"Thread is joinable! (represents a thread of execution)"<<endl;
        t1.join();
        if(!t1.joinable()){
            cout<<"Thread is not joinable! (after call join())"<<endl;
        }
    }
    thread t3;
    thread t2(printHelloWorld);
    t3 = move(t2);
    if(!t2.joinable()){
        cout<<"Thread is not joinable! (after move)"<<endl;
    }
    t3.join();
    return 0;
}

  输出结果如下所示:

Thread is not joinable! (default constructor)
Thread is joinable! (represents a thread of execution)
Hello World!
Thread is not joinable! (after call join())
Thread is not joinable! (after move)
Hello World!

join

  函数声明如下所示:

void join();

  当线程执行完毕后函数返回,join函数可以用来阻塞调用此函数的线程。另外,调用此函数后,线程对象将变成非joinable并且可以被安全销毁。示例如下:

#include<bits/stdc++.h>
using namespace std;
void pauseThread(int n){
    this_thread::sleep_for(chrono::seconds(n));
    std::cout<<"pause of "<<n<<" seconds ended"<<std::endl;
}
int main(){
    cout<<"spawing 3 threads..."<<endl;
    std::thread t1(pauseThread,1);
    std::thread t2(pauseThread,2);
    std::thread t3(pauseThread,3);
    cout<<"Done spawning threads. Now wait for them to join: "<<endl;
    t1.join();
    t2.join();
    t3.join();
    cout<<"All threads joined!"<<endl;
    return 0;
}

  输出结果如下所示:

spawing 3 threads...
Done spawning threads. Now wait for them to join: 
pause of 1 seconds ended
pause of 2 seconds ended
pause of 3 seconds ended
All threads joined!

detach

  函数声明如下所示:

void detach();

  此成员函数会将执行线程与调用线程分离,允许他们彼此独立运行。两个线程都会继续执行,且不会以任何方式阻塞或者同步。另外,当任意一个线程结束后都会释放其拥有的资源。与join函数相同的,调用此函数后,线程对象将变为非joinable并且可以安全的销毁。
  示例如下:

#include<bits/stdc++.h>
using namespace std;
void pauseThread(int n){
    this_thread::sleep_for(chrono::seconds(n));
    std::cout<<"pause of "<<n<<" seconds ended"<<std::endl;
}
int main(){
    cout<<"spawing 3 threads..."<<endl;
    std::thread (pauseThread,1).detach();
    std::thread (pauseThread,2).detach();
    std::thread (pauseThread,3).detach();
    cout<<"Done spawning threads. (the main thread will now pause for 5 seconds)"<<endl;
    pauseThread(5);
    return 0;
}

  输出结果如下所示:

Done spawning threads. (the main thread will now pause for 5 seconds)
pause of 1 seconds ended
pause of 2 seconds ended
pause of 3 seconds ended
pause of 5 seconds ended

hardware_concurrency

  函数声明如下所示:

static unsigned hardware_concurrency() noexcept;

  返回硬件的线程数。此函数的返回值依赖于特定的系统和实现,可能不是准确值。返回的线程数不一定和系统中的可用处理器数量或者处理器的核心数匹配:计算机的单个处理器可能支持多个线程,也可能限制程序对某些处理器的访问权限。
  如果此值不可计算或者系统中没有被很好的定义,那么该函数会返回0。
  示例如下:

#include<bits/stdc++.h>
using namespace std;
int main(){
    cout<<"hardware_concurrency:"<<thread::hardware_concurrency()<<endl;
    return 0;
}

  输出结果如下所示:

hardware_concurrency:16    // CPU R7-5800H 8核16线程的运行结果(windows 11)
hardware_concurrency:1     // 单核云服务器的运行结果(ubuntu 20.04)

常用创建线程的方法

使用一般函数创建

  使用一般函数创建,可以直接使用函数名或者使用函数指针进行创建。示例如下:

#include<bits/stdc++.h>
using namespace std;
void printHelloWorld(){
    cout<<"Hello World!"<<endl;
}
int main(){
    // 直接使用函数名创建
    thread t1(printHelloWorld);
    t1.join();
    //使用函数指针创建
    void (*funptr)() = printHelloWorld; 
    thread t2(funptr);
    t2.join();
    return 0;
}

  输出结果如下所示:

Hello World!
Hello World!

使用类的成员函数创建

  可以使用类的成员函数创建thread对象。其中可以在类外创建,即使用一个具体类实例创建;也可以在类内创建,把thread对象作为类的成员函数。

在类外创建thread对象

  在类外创建thread对象,初始化函数需要依次传入成员函数地址,成员函数所属的对象地址,然后是函数的参数。示例如下:

#include<bits/stdc++.h>
using namespace std;
class SaySomething{
public:
    void sayHello(){
        cout<<"Hello World!\n";
    }
    void saySomething(string str){
        cout<<str;
    }
    static void sayHelloStatic(){
        cout<<"Hello static!\n";
    }
};
int main(){
    SaySomething sh;
    thread t(&SaySomething::sayHello,&sh);                      //需要传入对象的地址
    thread t1(&SaySomething::saySomething,&sh,"Hello cpp!\n");  //需要传入对象的地址和参数
    thread t2(&SaySomething::sayHelloStatic);                   //静态成员函数不需要传入对象的地址,因为静态成员函数不属于对象
    t.join();
    t1.join();
    t2.join();
    return 0;
}

  输出结果如下所示:

Hello World!
Hello cpp!
Hello static!

在类内创建thread对象

  在类内创建thread对象,参数的传入类似于在类外创建,只不过因为在类内创建,所以传入的对象指针为自身this指针。示例如下:

#include<bits/stdc++.h>
using namespace std;
class SaySomething{
private:
    thread workThread;
    void sayHello(){
        cout<<"Hello world!\n";
    }
public:
    SaySomething(){
        workThread=thread(&SaySomething::sayHello,this);  //需要传入对象的地址
    }
    ~SaySomething(){
        workThread.join();                                //等待线程结束,防止主线程退出时子线程被强制结束
    }
};
int main(){
    SaySomething saySomething;
    return 0;
}

  输出结果如下所示:

Hello world!

使用函数对象创建

  C++中除了一般的函数以外,还有函数对象包括函数类、lambda函数、function类和使用bind创建的函数闭包。thread类可以使用这些函数对象来创建,方法如下所示。

使用函数类创建

  函数类通过重载operator()来实现类似于函数的操作,所以可以用来初始化一个线程。示例如下:

#include<bits/stdc++.h>
using namespace std;
class FunctionObject{
public:
    void operator()(string str){
        cout << str << endl;
    }
    void operator()(){
        cout << "Hello World!\n";
    }
};
int main(){
    FunctionObject fo;
    thread t(fo);                       //无参数版本
    thread t1(fo, "Hello parameter!\n");    //有参数版本
    t.join();
    t1.join();
    return 0;
}

  输出结果如下所示:

Hello World!
Hello parameter!

使用lambda函数创建

  lambda函数又称为匿名函数,作为可以像函数一样使用的对象,也是可以用来创建线程的。示例如下:

#include<bits/stdc++.h>
using namespace std;
int main(){
    thread t([]{
        cout << "Hello from thread!" << endl;
    });
    t.join();
    return 0;
}

  输出结果如下所示:

Hello from thread!

使用function类创建

  function类实例化的对象可以包装以下任何类型的可调用对象:函数、函数指针、指向成员函数的指针或任何类型的函数对象(即,其类定义 operator()的对象,包括闭包)。显而易见,我们可以通过function类来创建thread类。示例如下:

#include<bits/stdc++.h>
using namespace std;
void add(int a,int b){
    cout<<"Sum is: "<<a+b<<"\n";
}
int main(){
    function<void(int,int)> f=add;                          //function类包装add函数
    function<void(void)> f1 = bind(add,15,25);              //function包装一个闭包
    function<void(void)> f2 = [](){cout<<"from lambda"<<endl;};//function类包装一个lambda函数
    thread t(f,10,20);                                      //需要传递参数,输出Sum is: 30
    thread t1(f1);                                          //无需传参,输出Sum is: 40
    thread t2(f2);                                          //无需传参
    t.join();
    t1.join();
    t2.join();
    return 0;
}

  输出结果如下所示:

Sum is: Sum is: 40
from lambda30

使用bind的返回值创建

  bind可以将函数和其参数绑定形成一个函数对象,直接对该对象调用operator()来实现对函数的执行。因此,也可以直接使用闭包创建一个线程。示例如下:

#include<bits/stdc++.h>
using namespace std;
void add(int a,int b){
    cout<<"Sum is: "<<a+b<<"\n";
}
int main(){
    auto f1 = bind(add,5,10);
    f1();                           // Sum is: 15
    auto f2 = bind(add,placeholders::_1,placeholders::_2);
    f2(10,20);                      // Sum is: 30
    thread t1(f1);
    thread t2(f2,20,30);
    t1.join();                      // Sum is: 15 来自线程的执行结果
    t2.join();                      // Sum is: 50 来自线程的执行结果
    return 0;
}

  输出结果如下所示:

Sum is: 15
Sum is: 30
Sum is: 15
Sum is: 50

this_thread命名空间

  this_thread命名空间定义了一系列可以访问当前线程的函数:get_id、yield、sleep_until、sleep_for。

get_id

  函数声明如下所示:

thread::id get_id() noexcept;

  返回调用此函数的线程自身的线程id,该线程id唯一标识此线程。示例如下:

#include<bits/stdc++.h>
using namespace std;
void printThreadId(){
    cout << "Thread id: " << this_thread::get_id() << endl;
}
int main(){
    printThreadId();            //输出主线程的id
    thread t(printThreadId);    //输出子线程的id
    t.join();
    return 0;
}

  输出结果如下所示:

// windows下的输出结果
Thread id: 1
Thread id: 2

yield

  函数声明如下所示:

void yield() noexcept;

  yield会给其他线程让出cpu资源,调用yield的函数会获得重新调度的机会。当一个线程没有被阻塞,但是需要等待其他线程时,可以调用此函数。示例如下:

#include<bits/stdc++.h>
using namespace std;
atomic_bool ready(false);   //原子布尔类型
void count_one_million(int id){
    while(!ready){          //等待主线程中设置ready为true
        this_thread::yield();
    }
    for(int i=0;i<1000000;i++);
    cout<<id;               //完成计数的输出自己的id
}
int main(){
    thread threads[10];
    cout<<"thread 10 id will be outputed after main thread set ready to true..."<<endl;
    for(int i=0;i<10;i++){
        threads[i]=thread(count_one_million,i);
    }
    ready=true;             //设置ready为true,允许线程执行,所有线程开始执行计数
    for(auto& th:threads){
        th.join();
    }
    cout<<endl;
    return 0;
}

  输出结果如下所示:

thread 10 id will be outputed after main thread set ready to true...
6930427581

sleep_until

  函数声明如下所示:

template <class Clock, class Duration>
  void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);

  阻塞调用该函数的线程直到abs_time时刻。当前线程被阻塞到abs_time,其他函数可以继续执行。示例如下:

#include<bits/stdc++.h>
using namespace std;
int main(){
    using chrono::system_clock;
    // 记录当前时间
    auto current = system_clock::now();
    time_t current_time = system_clock::to_time_t(current);
    cout << "current time: " << ctime(&current_time) << endl;
    // 计算60秒后的时间
    current_time += 60;
    cout<<"wait 60 seconds..."<<endl;
    auto new_time = system_clock::from_time_t(current_time);
    // 等待60秒
    this_thread::sleep_until(new_time);
    // 记录60秒后的时间
    current = system_clock::now();
    current_time = system_clock::to_time_t(current);
    cout << "after 60 seconds current time: " << ctime(&current_time) << endl;
    return 0;
}

  输出结果如下所示:

current time: Mon Dec 05 16:13:17 2022

wait 60 seconds...
after 60 seconds current time: Mon Dec 05 16:14:17 2022

sleep_for

  函数声明如下所示:

template <class Rep, class Period>
  void sleep_for (const chrono::duration<Rep,Period>& rel_time);

  阻塞当前线程一段时间,这段时间是一个时间长度,而非绝对的时间点。如果当前线程调用了该函数,那么函数会停止参数给定的时间长度,其他线程正常执行。示例如下:

#include<bits/stdc++.h>
using namespace std;
int main(){
    cout<<"main thread begin..."<<endl;
    cout<<"main thread will create a new thread then sleep 5 seconds..."<<endl;
    thread t([](){
        int cnt =0;
        while(cnt<3){
            cnt++;
            cout<<"hello world -- from new thread"<<endl;
            this_thread::sleep_for(chrono::seconds(1));
        }
    });
    t.detach();
    this_thread::sleep_for(chrono::seconds(5));
    cout<<"main thread end..."<<endl;
    return 0;
}

  输出结果如下所示:

main thread begin...
main thread will create a new thread then sleep 5 seconds...
hello world -- from new thread
hello world -- from new thread
hello world -- from new thread
// 停顿等待,因为主线程中sleep_for的参数为5s
main thread end...

  另外,c++11的std::chrono_literals命名空间中支持直接使用时间的字面量来表示时间长度,因此使用sleep_for函数是可以直接使用时间的字面量来表示。实例如下:

#include<bits/stdc++.h>
int main(){
    using namespace std::chrono_literals;
    std::cout<<"main thread begin..."<<std::endl;
    // h表示小时 min表示分 s表示秒 ms表示毫秒 us表示微秒 ns表示纳秒, 支持浮点数
    std::this_thread::sleep_for(5s);
    std::cout<<"main thread end..."<<std::endl;
    return 0;
}

  输出结果如下所示:

main thread begin...
// 停顿5s
main thread end...

当珍惜每一片时光~