线程类thread的使用

1、构造函数

// ①
thread() noexcept;
// ②
thread( thread&& other ) noexcept;
// ③
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
// ④
thread( const thread& ) = delete;

构造函数①:默认构造函数,构造一个线程对象,在这个线程中不执行任何处理动作

构造函数②:移动构造函数,将 other 的线程所有权转移给新的thread 对象。之后 other 不再表示执行线程。

构造函数③:创建线程对象,并在该线程中执行函数f中的业务逻辑,args是要传递给函数f的参数

任务函数f的可选类型有很多,具体如下:

普通函数,类成员函数,匿名函数,仿函数(这些都是可调用对象类型) 可以是可调用对象包装器类型,也可以是使用绑定器绑定之后得到的类型(仿函数) 构造函数④:使用=delete显示删除拷贝构造, 不允许线程对象之间的拷贝

2、公共成员函数

应用程序启动后默认的原始线程称为 主线程 或 父线程,我们通过线程类开辟的线程称之为 子线程,每个被创建出的线程实例都有一个唯一的ID与之对应且可以用函数get_id() 获取std::thread::id get_id() const noexcept;

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

void func(int num, string str)
{
    for (int i = 0; i < 10; ++i)
    {
        cout << "id: i = " << i << "num: " 
             << num << ", str: " << str << endl;
    }
}

void func1()
{
    for (int i = 0; i < 10; ++i)
    {
        cout << "id: i = " << i << endl;
    }
}

int main()
{
    cout << "MainID: " << this_thread::get_id() << endl;
    thread t(func, 520, "i love you");
    thread t1(func1);
    cout << "t ID: " << t.get_id() << endl;
    cout << "t1ID: " << t1.get_id() << endl;
    //t.join();或者t.detach()
    //t1.join();或者t1.detach()
}

thread t(func, 520, “i love you”);:创建了子线程对象t,func()函数会在这个子线程中运行 func()是一个回调函数,线程启动之后就会执行这个任务函数,我们只需要实现即可 func()的参数是通过thread的参数进行传递的,520,i love you都是调用func()需要的实参 线程类的构造函数③是一个变参函数,因此无需担心线程任务函数的参数个数问题 任务函数func()一般返回值指定为void,因为子线程在调用这个函数的时候不会处理其返回值 thread t1(func1);:子线程对象t1中的任务函数func1(),没有参数,因此在线程构造函数中就无需指定了 通过线程对象调用get_id()就可以知道这个子线程的线程ID了,t.get_id(),t1.get_id()。

需注意: 我们必须要在线程对象销毁之前在线程以下二者状态之间作出选择,否则程序运行期间就会有bug产生。 加入式(join()) 分离式(detach())

join()

join()字面意思是连接一个线程,意味着等待此线程的终止(线程阻塞)。在某个线程中通过子线程对象调用join()函数,调用这个函数的线程被阻塞,但是子线程对象中的任务函数会继续执行,当任务执行完毕之后join()会清理当前子线程中的相关资源然后返回,同时,调用该函数的线程解除阻塞继续向下执行。

detach()

etach()函数的作用是进行线程分离,分离主线程和创建出的子线程。在线程分离之后,主线程退出也会一并销毁创建出的所有子线程,在主线程退出之前,它可以脱离主线程继续独立的运行,任务执行完毕之后,这个子线程会自动释放自己占用的系统资源。(其实就是孩子翅膀硬了,和家里断绝关系,自己外出闯荡了,如果家里被诛九族还是会受牵连)

上述代码如果俩个线程是join() 运行结果:
MainID: 1
t ID: 2
t1ID: 3
id: i = id: i = 0num: 520, str: i love you
id: i = 1num: 520, str: i love you
id: i = 2num: 520, str: i love you
id: i = 3num: 520, str: i love you
id: i = 4num: 520, str: i love you
id: i = 5num: 520, str: i love you
id: i = 6num: 520, str: i love you
id: i = 7num: 520, str: i love you
id: i = 8num: 520, str: i love you
id: i = 9num: 520, str: i love you
0
id: i = 1
id: i = 2
id: i = 3
id: i = 4
id: i = 5
id: i = 6
id: i = 7
id: i = 8
id: i = 9
上述代码如果俩个线程是detach() 运行结果:
可能出现多种情况,例如
===只输出主线程,子线程还没开始输出就被中止了===
MainID: 1
t ID: 2
t1ID: 3
===乱序输出===
MainID: 1
t ID: 2
t1ID: 3
id: i = id: i = 0num: 520, str: i love you
id: i = 1num: 520, str: i love you
id: i = 2num: 520, str: i love you
id: i = 3num: 520, str: i love you
id: i = 4num: 520, str: i love you
id: i = 5num: 520, str: i love you
id: i = 60
id: i = 1
id: i = 2
id: i = 3
id: i = 4
id: i = 5
id: i = 6
id: i = 7
id: i = num: 520, str: i love you
id: i = 7num: 520, str: i love you
id: i = 8num: 520, str: i love you
id: i = 9num: 520, str: i love you
8
id: i = 9

joinable()

oinable()函数用于判断主线程和子线程是否处理关联(连接)状态,一般情况下,二者之间的关系处于关联状态,该函数返回一个布尔类型:

返回值为true:主线程和子线程之间有关联(连接)关系 返回值为false:主线程和子线程之间没有关联(连接)关系 ,调用了detach()后返回的也是false

3、静态函数

thread线程类还提供了一个静态方法,用于获取当前计算机的CPU核心数,根据这个结果在程序中创建出数量相等的线程,每个线程独自占有一个CPU核心,这些线程就不用分时复用CPU时间片,此时程序的并发效率是最高的。

定义:static unsigned hardware_concurrency() noexcept; 使用:cout << "CPU number: " << thread::hardware_concurrency()<< endl;

命名空间 this_thread⭐

在这个命名空间中提供了线程的四个公共的成员函数

1. get_id()

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

void func()
{
    cout << "子线程: " << this_thread::get_id() << endl;
}

int main()
{
    cout << "主线程: " << this_thread::get_id() << endl;
    thread t(func);
    t.join();
}

2. sleep_for()

线程被创建后与进程一样也有这五种状态:创建态,就绪态,运行态,阻塞态(挂起态),退出态(终止态) ,关于状态之间的转换是一样的。

为了能够实现并发处理,多个线程都是分时复用CPU时间片,快速的交替处理各个线程中的任务。因此多个线程之间需要争抢CPU时间片,抢到了就执行,抢不到则无法执行(因为默认所有的线程优先级都相同,内核也会从中调度,不会出现某个线程永远抢不到CPU时间片的情况)。

命名空间this_thread中提供了一个休眠函数sleep_for(),调用这个函数的线程会马上从运行态变成阻塞态并在这种状态下休眠一定的时长,因为阻塞态的线程已经让出了CPU资源,代码也不会被执行,所以线程休眠过程中对CPU来说没有任何负担。这个函数是函数原型如下,参数需要指定一个休眠时长,是一个时间段:

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

void func()
{
    for (int i = 0; i < 10; ++i)
    {
        this_thread::sleep_for(chrono::seconds(1));
        cout << "子线程: " << this_thread::get_id() << ", i = " << i << endl;
    }
}

int main()
{
    thread t(func);
    t.join();
}

在func()函数的for循环中使用了this_thread::sleep_for(chrono::seconds(1));之后,每循环一次程序都会阻塞1秒钟,也就是说每隔1秒才会进行一次输出。需要注意的是:程序休眠完成之后,会从阻塞态重新变成就绪态,就绪态的线程需要再次争抢CPU时间片,抢到之后才会变成运行态,这时候程序才会继续向下运行。

3. sleep_until()

名空间this_thread中提供了另一个休眠函数sleep_until(),和sleep_for()不同的是它的参数类型不一样

sleep_until():指定线程阻塞到某一个指定的时间点time_point类型,之后解除阻塞 sleep_for():指定线程阻塞一定的时间长度duration 类型,之后解除阻塞

templa <class Rep, class Period> void sleep_until (const chrono::time_point& abs_time);

void func()
{
    for (int i = 0; i < 10; ++i)
    {
        // 获取当前系统时间点
        auto now = chrono::system_clock::now();
        // 时间间隔为2s
        chrono::seconds sec(2);
        // 当前时间点之后休眠两秒
        this_thread::sleep_until(now + sec);
        cout << "子线程: " << this_thread::get_id() << ", i = " << i << endl;
    }
}

int main()
{
    thread t(func);
    t.join();
}

4. yield()

命名空间this_thread中提供了一个非常绅士的函数yield(),在线程中调用这个函数之后,处于运行态的线程会主动让出自己已经抢到的CPU时间片,最终变为就绪态,这样其它的线程就有更大的概率能够抢到CPU时间片了。使用这个函数的时候需要注意一点,线程调用了yield()之后会主动放弃CPU资源,但是这个变为就绪态的线程会马上参与到下一轮CPU的抢夺战中,不排除它能继续抢到CPU时间片的情况,这是概率问题。

void yield() noexcept;

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

void func()
{
    for (int i = 0; i < 100000000000; ++i)
    {
        cout << "子线程: " << this_thread::get_id() << ", i = " << i << endl;
        this_thread::yield();
    }
}

int main()
{
    thread t(func);
    thread t1(func);
    t.join();
    t1.join();
}

在上面的程序中,执行func()中的for循环会占用大量的时间,在极端情况下,如果当前线程占用CPU资源不释放就会导致其他线程中的任务无法被处理,或者该线程每次都能抢到CPU时间片,导致其他线程中的任务没有机会被执行。解决方案就是每执行一次循环,让该线程主动放弃CPU资源,重新和其他线程再次抢夺CPU时间片,如果其他线程抢到了CPU时间片就可以执行相应的任务了。

std::this_thread::yield() 的目的是避免一个线程长时间占用CPU资源,从而导致多线程处理性能下降

call_once

定义于头文件<mutex> 在某些特定情况下,某些函数只能在多线程环境下调用一次,比如:要初始化某个对象,而这个对象只能被初始化一次,就可以使用std::call_once()来保证函数在多线程环境下只能被调用一次。使用call_once()的时候,需要一个once_flag作为call_once()的传入参数

template < class Callable, class… Args > void call_once( std::once_flag& flag, Callable&& f, Args&&… args );

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

once_flag g_flag;
void do_once(int a, string b)
{
    cout << "name: " << b << ", age: " << a << endl;
}

void do_something(int age, string name)
{
    static int num = 1;
    call_once(g_flag, do_once, 19, "luffy");
    cout << "do_something() function num = " << num++ << endl;
}

int main()
{
    thread t1(do_something, 20, "ace");
    thread t2(do_something, 20, "sabo");
    thread t3(do_something, 19, "luffy");
    t1.join();
    t2.join();
    t3.join();

    return 0;
}
===output===
name: luffy, age: 19
do_something() function num = 1
do_something() function num = 2
do_something() function num = 3
//call_once()函数只会被调用一次