[C++]std::thread用法

FL1768317420 2024-07-03 12:05:01 阅读 81

1. 基础

1.1 线程与进程

提到线程,不可避免的关联到进程,进程与线程有何区别和关联?

进程是资源分配的最小单位,线程是操作系统调度执行的最小单位。

每个进程对应一个虚拟地址空间,一个进程只能抢一个CPU时间片。进程有自己独立的地址空间,多个线程共用同一个地址空间。

在一个地址空间中多个线程独享:各种的栈区、寄存器在一个地址空间中多个线程共享:代码段、堆区、全局数据段(主要用于存储全局变量、静态变量)CPU调度和切换:线程调度比进程快

涉及上下文切换:进程/线程复用CPU时间片,切换之前会将上一个任务的状态保存,下次再切回这个任务时,基于这个状态继续进行,任务从保存到再次加载的过程就是一次上下文切换运行多个进程的固定 开销:需要时间启动进程、操作系统需要内部资源来管理进程等

1.2 std::thread

std::thread是C++11新增的特性,位于<thread>头文件中。

线程创建时可以指定入口函数,该函数执行完成后,线程也就结束了。、

启动线程时,需要明确是要等待线程结束(join),还是让其自主运行(detach)。

如果是让其自主运行,需要保证线程结束之前,可访问数据的有效性。如果线程还未进行,启动线程的函数已经退出,此时线程函数还持有函数局部指针或引用,此时继续访问该数据会发生未定义行为。

使用能访问局部变量的函数作为std::thread启动函数,这是一个糟糕的设计

std::thread不可以拷贝构造和拷贝赋值,但是支持移动构造和移动赋值。

不可复制性保证了同一时间,一个std::thread实例只能关联一个执行线程。可移动性使开发者可以控制哪个线程实例拥有线程执行权。

std::thread常用函数有:

函数名称 作用
joinable 成员函数,检查线程对象是否可被join
get_id 成员函数,获取线程id
native_handle 成员函数,获取线程句柄
hardware_concurrency 静态成员函数,获取硬件支持的并发线程数量
join 成员函数,等待线程执行结束,将阻塞启动该线程的主调函数的执行
detach 成员函数,分离线程实例与目标线程的关联,不阻塞主调函数的执行
swap 成员函数,交换两个线程实例

helloworld如下:

<code>// 引入标准库thread头文件

#include <thread>

// 线程回调函数

void helloWorld()

{

// 获取当前线程id

std::thread::id threadID = std::this_thread::get_id();

printf("Hello current thread.\n");

}

int main()

{

std::thread t(helloWorld);

t.join();

system("pause");

return 0;

}

2. 线程管理

2.1 线程启动

启动线程的两类方法如下:

使用lambda表达式使用仿函数Functor(重载了小括号操作符的类的对象

使用新统一的初始化语法{}使用多组括号(不能使用常规临时变量声明方法)

class background_task

{

public:

// 重载operator() 操作符,无入参

// 调用时机:对象后边使用一对圆括号时,编译器自动调用

// 重载operator()时,返回值和入参类型可以是任何类型

void operator()() const

{

printf("background_task operator()()\n");

}

};

int main()

{

{

printf("method1:");

std::thread t([]() {

printf("lamda func\n");

});

t.join();

}

{

printf("method2:");

std::thread t(background_task{});

t.join();

}

{

printf("method3:");

std::thread t((background_task()));

t.join();

}

system("pause");

return 0;

}

注意:使用functor时,不能使用常规临时变量声明方法(类型()),这将被编译器认为声明函数指针,出现编译错误。

std::thread t(background_task());// Error

// std::thread t((background_task())); // OK

t.join();

编译错误

<code>std::thread t(background_task());代码将被编译器解析为声明了一个名为t的函数指针,其签名为 带有一个入参,返回值类型为std::thread的函数,而不是启动了一个线程。其入参也是一个函数指针,签名为无参返回值类型为background_task。

2.2 等待线程执行结束-join

join函数的作用是等待线程函数执行结束后再继续执行主调函数的逻辑,与joinable函数搭配使用。

joinable函数的作用是判断线程能否被join。

join函数只能选择等待或不等待线程函数执行结束,如果需要灵活控制线程逻辑(check线程是否结束、增加运行时间控制等),可能需要用到条件变量或futures。

一个线程只能执行一次join函数,为了保证join函数的正常执行,应该是用RAII方式,具体实现如下:

class ThreadGuard

{

public:

ThreadGuard(std::thread& t)

:mThread(t) { }

~ThreadGuard()

{

printf("~ThreadGuard() begin \n");

// 判断线程是否可加入

if (mThread.joinable())

{

// 等待子线程函数执行结束,完成后继续执行ThreadGuard的析构

mThread.join();

}

printf("~ThreadGuard() end \n");

}

// 禁止拷贝,否则会丢失子线程

ThreadGuard(const ThreadGuard& rhs) = delete;

ThreadGuard& operator=(const ThreadGuard& rhs) = delete;

private:

std::thread& mThread;

};

int main() {

// 局部对象逆序销毁,先销毁gurad,再销毁t

// 即便t的回调函数引用的外部的局部变量,也能保证在

int stateCount = 0;

std::thread t([&stateCount]() {

stateCount++;

printf("stateCount:%d\n", stateCount);

});

ThreadGuard guard(t);

printf("ThreadGuard t=========\n");

system("pause");

return 0;

}

2.3 后台运行线程-detach

detach函数的作用是分离线程函数。启动线程后,不等待线程函数执行完成,继续执行主调函数的逻辑。

使用detach方法将子线程与启动该线程的主调线程分离后,该线程将在后台运行,且无法被加入。

C++运行库保证,当子线程结束时,相关资源能被正常回收。

与join方法使用类似,必须先判断其joinable是否为true,若判断结果为true,才运行调用detach分离线程。

void doSomethingBack() {

}

std::thread t(doSomethingBack);

t.detach();

2.4 向线程传递参数

要点一:线程函数为非常量引用时,注意使用std::ref传递变量引用,否则将传递变量拷贝的引用(变量将执行拷贝构造)。

class ThreadData

{

public:

ThreadData()

{

printf("ThreadData::ThreadData()\n");

}

~ThreadData()

{

printf("ThreadData::~ThreadData\n");

}

ThreadData(const ThreadData& rhs)

{

printf("ThreadData::ThreadData(const ThreadData& data)\n");

}

ThreadData(const ThreadData&& rhs)noexcept

{

printf("ThreadData::ThreadData(const ThreadData&& data)\n");

}

ThreadData& operator=(const ThreadData& rhs)

{

printf("ThreadData::operator=(const ThreadData& rhs)\n");

return *this;

}

ThreadData& operator=(const ThreadData&& rhs)noexcept

{

printf("ThreadData::operator=(const ThreadData&& rhs)\n");

return *this;

}

public:

int num = 0;

};

// 非常量引用

void doSomething(int i, ThreadData& data)

{

data.num += 1;

printf("doSomething i:%d data.num:%d\n", i, data.num);

}

int main() {

if (true)

{

ThreadData data;

printf("start thread...\n");

// 传递给线程函数的是data的拷贝引用,而非data引用

// 此处data将被拷贝到线程函数(虽然函数形参类型是引用,但仍拷贝构造了一个临时变量)

std::thread t(doSomething, 3, data);//1

// data.num为0

printf("join thread..., data.num:%d\n", data.num);

t.join();

// data.num为0

printf("pause... data.num:%d\n", data.num);

}

printf("======================\n");

if (true)

{

ThreadData data;

printf("start thread...\n");

// 传递给线程函数的是data引用,而非data拷贝的引用

std::thread t(doSomething, 5, std::ref(data));//2

// data.num为0

printf("join thread..., data.num:%d\n", data.num);

t.join();

// data.num为1

printf("pause... data.num:%d\n", data.num);

}

system("pause");

return 0;

}

要点二:将参数所有权转移给线程函数。

当实参是临时变量时,自动进行移动拷贝。当实参是命名变量时,默认是拷贝构造,需要通过std::move进行移动拷贝。

{

// 临时对象默认调用移动构造产生临时变量

std::thread t1(doSomething, 23, ThreadData{});

t1.join();

}

{

ThreadData data;

// 命令变量默认通过拷贝构造产生临时变量,可以通过std::move函数改为移动构造

std::thread t(doSomething, 89, std::move(data));

t.join();

printf("pause... data.num:%d\n", data.num);

}



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。