目录
- 定义
- std::thread
-
- 常用成员函数
- 用例
- 注意事项
- std::atomic和std::mutex
-
- 为什么需要atomic和mutex
- std::mutex
-
- mutex的常用成员函数
- std::lock_guard
- std::atomic
- std::async
-
- 为什么使用async而不是thread
- std::async参数
- std::future
-
- std::future常用成员函数
- 为什么要有void特化的std::future
- std::promise
-
- std::promise常用成员函数
- std::this_thread
- 参考
定义
多线程:是指从软件或者硬件上实现多个线程并法执行的技术。
进程与线程的区别:
- 进程是正在进行的程序的实例,而线程是进程中的实际运作单位。
- 一个程序有且只有一个进程,但可以拥有至少一个线程
- 不同进程拥有不同的地址空间,互不相关。而不同线程共同拥有相同进程的地址空间。
std::thread
常用成员函数
函数 | 作用 |
---|---|
void join() | 等待线程结束并清理资源(会阻塞) |
bool joinable() | 返回线程是否可以执行join函数 |
void detach() | 将线程与调用其的线程分离,彼此独立执行(此函数必须在线程创建时立即调用,且调用此函数会使其不能被jion) |
std::thread::id get_id() | 获取线程id |
用例
cmakelists:
find_package(thread)
add_executable(thread_test thread_test.cpp)
target_link_libraries(thread_test ${cmake_thread_libs_init})
- 例一:thread的基本使用
#include
#include
using namespace std;
void doit(){
cout << "world !" << endl;}
int main(){
// flush函数:刷新缓冲区。
// endl函数:终止一行并刷新缓冲区。
thread a([]{
cout << "hello," << flush;}), b(doit);
a.join();
b.join();
return 0;
}
输出结果:
hello, world!
或者是
world!
hello,
多线程运行时是以异步方式执行的,与我们平时写的同步方式不同。异步方式可以同时执行多条语句。
- 例2 :thread执行有参数的函数
#include
#include
using namespace std;
void output(int id){
cout << "thread" << id << "finish!" << endl;
}
int main(){
thread th[10];
for(int i = 0 ; i < 10; i ){
th[i] = thread(output,i);
}
for(int i = 0; i < 10 ; i ){
th[i].join();
}
return 0;
}
输出结果:
threadthread01finish!finish!
thread3finish!
thread2thread4finish!
finish!
thread5finish!
thread6finish!
thread7finish!
thread8finish!
thread9finish!
- 例3:thread执行带有引用参数的函数
#include
#include
using namespace std;
template void changevalue(t &x, t val){
x = val;
}
int main(){
thread th[10];
int num[10];
for(int i = 0 ; i < 10;i ){
th[i] = thread(changevalue , ref(num[i]), i 1);
}
for(int i =0 ;i <10;i ){
th[i].join();
cout << num[i] << " ";
}
return 0;
}
输出结果:
1 2 3 4 5 6 7 8 9 10
由于thread在传递参数时,是以右值传递的:
template
explicit thread(fn&& fn, args&&... args)
划重点:args&&… args
很明显的右值引用,那么我们该如何传递一个左值呢?std::ref和std::cref很好地解决了这个问题。
std::ref 可以包装按引用传递的值。 std::cref 可以包装按const引用传递的值。
注意事项
- 线程是在thread对象被定义的时候开始执行的,而不是在被调用join函数时才执行的。调用join函数只是阻塞等待线程结束并回收资源。
- 分离的线程(执行过detach的线程)会在调用它的线程结束或自己结束时释放资源。
- 线程会在函数运行完毕后自动释放,不推荐利用其他方法强制结束线程,可能会因资源未释放而导致内存泄露。
- 没有执行
join
或detach
的线程在程序结束时会引发异常
std::atomic和std::mutex
为什么需要atomic和mutex
#include
#include
using namespace std;
int n = 0;
void plus_n(){
for(int i =0 ;i < 10000;i ){
n ;
}
}
int main(){
thread th[100];
for(thread &x : th){
x = thread(plus_n);
}
for(thread &x: th){
x.join();
}
cout << " n : " << n << endl;
return 0;
}
输出结果:
n : 311747
我们的输出结果应该是1000000,可是为什么实际输出结果比1000000小呢?
在上文我们分析过多线程的执行顺序——同时进行、无次序,所以这样就会导致一个问题:多个线程进行时,如果它们同时操作同一个变量,那么肯定会出错。为了应对这种情况,c 11中出现了std::atomic和std::mutex。
std::mutex
std::mutex
是c 11中最基本的互斥量,一个线程将mutex锁住时,其他的线程就不能操作mutex,直到这个线程将mutex解锁。根据这个特征,我们可以修改一下上述的代码:
主要需要引入头文件#include
- 例四:std::mutex的使用
#include
#include
#include
using namespace std;
int n = 0;
std::mutex mtx;
void plus_n(){
for(int i =0 ;i < 10000;i ){
mtx.lock();
n ;
mtx.unlock();
}
}
int main(){
thread th[100];
for(thread &x : th){
x = thread(plus_n);
}
for(thread &x: th){
x.join();
}
cout << " n : " << n << endl;
return 0;
}
输出结果:
n : 1000000
mutex的常用成员函数
(这里用mutex
代指对象
)
函数 | 作用 |
---|---|
void lock() | 将mutex上锁。如果mutex已经被其他线程上锁,那么会阻塞,直到解锁; 如果mutex已经被同一个线程锁住,那么就会产生死锁 |
void unlock() | 解锁mutex,释放其所有权。如果有线程因为调用lock()不能上锁而被阻塞,则调用此函数会将mutex的主动权随机交给其中一个线程; 如果mutex不是被此线程上锁,那么会引发未定义的异常 |
bool try_lock() | 尝试将mutex上锁;如果mutex未被上锁,这将其上锁并返回true;如果mutex已被锁则返回false。 |
std::lock_guard
使用lock_guard相比于mutex更加安全,它是基于作用域的,能够自解锁,当该对象创建时,它会像mutex.lock()一样获得互斥锁,当生命周期结束时,它会自动析构(unlock),不会因为某个线程异常退出而影响其他线程。
int cnt = 20;
mutex m;
void t2()
{
while (cnt > 0)
{
lock_guard lockguard(m);
if (cnt > 0)
{
--cnt;
cout << cnt << endl;
}
}
}
std::atomic
mutex很好地解决了多线程资源争抢的问题,但是它太…慢…了…!
以例四为标准,我们定义了100个thread,每个thread要循环10000次,每次循环都要加锁、解锁,这样固然会浪费很多的时间,那么该怎么办呢?接下来就是atomic大展拳脚的时间了。
- 例五:std::atomic的使用
根据atomic的定义,我又修改了例四的代码:
#include
#include
#include
using namespace std;
atomic_int n = 0;
void plus_n(){
for(int i =0 ;i < 10000;i ){
n ;
}
}
int main(){
thread th[100];
for(thread &x : th){
x = thread(plus_n);
}
for(thread &x: th){
x.join();
}
cout << " n : " << n << endl;
return 0;
}
输出结果:
n : 1000000
代码解释
可以看到,我们只是改动了n的类型(int -> std::atomic _int
),其他地方一点没动,输出却正常了。其中std::atomic_int
是std::atomic
的别名。
atomic本意为原子,官方的解释是原子操作是最小且不可并行化的操作。这意味着即使是多线程,也要像同步进行一样同步操作atomic对象,从而省去了mutex上锁、解锁的时间消耗。
std::async
注意:std::async定义在future头文件中
为什么使用async而不是thread
thread可以快速、方便的创建线程,但是在async面前,就是小巫见大巫了。
async可以根据情况选择同步执行或创建线程来异步执行,当然也可以手动操作。对于async的返回值操作也比thread更加方便。
std::async参数
不同于thread,async是一个函数,所以没有成员函数。
重载版本 | 作用 |
---|---|
template future async (fn&& fn, args&&… args) |
异步或同步(根据操作系统而定)以args为参数执行fn 同样地,传递引用参数需要 std::ref 或std::cref |
template future async (launch policy, fn&& fn, args&&… args); |
异步或同步(根据policy参数而定(见下文))以args为参数执行fn 引用参数同上 |
std::launch强枚举类(enum class)
标识符 | 作用 |
---|---|
枚举值:launch::async | 异步启动 |
枚举值:launch::deferred | 在调用future::get、future::wait时同步启动(std::future见后文) |
特殊值:launch::async | launch::defereed | 同步或异步,根据操作系统而定 |
- 例六:std::async的使用
#include
#include
using namespace std;
int main(){
async(launch::async, [](const char *message){
cout << message << flush;
},"hello,");
cout << "world!" << endl;
return 0;
}
输出结果:
hello,world!
std::future
我们已经知道如何使用async来异步或同步执行任务,但如何获得函数的返回值呢?这时候,async的返回值std::future就派上用场了。
在之前的所有例子中,我们创建线程时调用的函数都没有返回值,但如果调用的函数有返回值呢?
- 例七:std::future的使用
#include
#include
using namespace std;
template // c 17折叠表达式
// decltype(auto)是c 14新增的类型指示符,可以用来声明变量以及指示函数返回类型。
decltype(auto) sum(args&&... args){
return (0 ... args); // "0 "避免空参数包错误
}
int main(){
future val = async(launch::async, sum,1,10,100);
cout << val.get() << endl ; // 阻塞等待线程结束并返回值
return 0;
}
输出结果:
111
代码解释:
我们定义了一个函数sum,它可以计算多个数字的和,之后我们又定义了一个对象val,它的类型是std::future
, 这里的int
代表这个函数的返回值是int类型。在创建线程后,我们使用了future::get()
来阻塞等待线程结束并获取其返回值。
std::future常用成员函数
函数 | 作用 |
---|---|
当类型为引用:r& future 当类型为void:void future::get() |
阻塞等待线程结束并获取返回值。 若类型为void,则与future::wait()相同。只能调用一次。 |
void wait() const | 阻塞等待线程结束 |
template future_status wait_for(const chrono::duration |
阻塞等待rel_time(rel_time是一段时间), 若在这段时间内线程结束则返回future_status::ready 若没结束则返回future_status::timeout 若async是以launch::deferred启动的,则不会阻塞并立即返回future_status::deferred |
为什么要有void特化的std::future
std::future的作用并不只有获取返回值,它还可以检测线程是否已结束、阻塞等待,所以对于返回值是void的线程来说,future也同样重要。
- 例八 void特化std::future
#include
#include
using namespace std;
void count_big_number(){
// c 14中,可以给数字中间加上单引号来分割数组,增强可读性
for(int i =0 ; i <= 100'0000'0000 ; i );
}
int main(){
future fut = async(launch::async, count_big_number);
cout << "please wait" << flush;
// 每次等待1秒
while(fut.wait_for(chrono::seconds(1)) != future_status::ready){
cout << "," << flush;
}
cout << endl << "finished!" << endl;
return 0;
}
等待线程运行结束,等待期间每过1秒输出一个“,”
输出结果:
please wait,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
finished
std::promise
在上文,我们已经讲到如何获取async创建线程的返回值。不过在某些特殊情况下,我们可能需要使用thread而不是async,那么如何获得thread的返回值呢?
还记得之前我们讲的thread成员函数吗?thread::join()的返回值是void类型,所以你不能通过join来获得线程返回值。只能通过传递引用的方式来获取返回值。
假如你写一个函数,需要返回3个值,那你会怎么办呢?vector?嵌套pair?不不不,都不需要,3个引用参数就可以了。
void get_circle(double r, double &d, double &c, double &s) {
d = r * 2;
c = pi * d;
s = pi * r * r;
}
promise实际上是std::future的一个包装,在讲解future时,我们并没有牵扯到改变future值的问题,但是如果使用thread以引用传递返回值的话,就必须要改变future的值,那么该怎么办呢?
实际上,future的值不能被改变,但你可以通过promise来创建一个拥有特定值的future
- 例九 std::future的值不能改变,那么如何利用引用传递返回值
std::promise常用成员函数
函数 | 作用 |
---|---|
当类型为引用:void promise 当类型为void:void promise::set_value (void) |
设置promise的值并将共享状态设为ready(将future_status设为ready) void特化:只将共享状态设为ready |
future get_future() | 构造一个future对象,其值与promise相同,status也与promise相同 |
- 例十:std::promise的使用
#include
#include
using namespace std;
template
decltype(auto) sum(args&& ... args){
return (0 ... args);
}
template
void sum_thread(promise &val, args&& ... args){
val.set_value(sum(args...));
}
int main(){
promise sum_value;
thread get_sum(sum_thread,ref(sum_value),1,10,100);
cout << sum_value.get_future().get() << endl;
get_sum.join();
return 0;
}
输出结果:
111
std::this_thread
在头文件中,不仅有std::thread这个类,而且还有一个std::this_thread命名空间,它可以很方便地让线程对自己进行控制。
函数 | 作用 |
---|---|
std::thread::id get_id() noexcept | 获取当前线程id |
template void sleep_for( const std::chrono::duration |
等待sleep_duration(sleep_duration是一段时间) |
void yield() noexcept | 暂时放弃线程的执行,将主动权交给其他线程 (放心,主动权还会回来) |
- 例十一:std::this_thread中常用函数的使用
#include
#include
using namespace std;
atomic_bool ready = false;
void sleep(uintmax_t ms){
this_thread::sleep_for(chrono::microseconds(ms));
}
void record(){
while(!ready) {
this_thread::yield();
}
cout << "thread [" << this_thread::get_id() << "] finished!" << endl;
}
int main(){
thread th[10];
for(int i =0 ; i < 10; i ){
th[i] = thread(record);
}
sleep(5000);
ready = true;
cout << "start! " << endl;
for (int i = 0; i < 10; i ){
th[i].join();
}
return 0;
}
输出结果:
thread [thread [140244805338880140244796946176thread [] finished!] finished!start! 140244813731584
thread [140244838909696] finished!
thread [140244788553472] finished!
thread [140244855695104] finished!
thread [140244780160768thread [] finished!
thread [140244830516992
140244822124288] finished!
] finished!
thread [140244847302400] finished!
] finished!