当前位置:   article > 正文

c++ 多线程std::thread总结

std::thread

定义

多线程:是指从软件或者硬件上实现多个线程并法执行的技术。

进程与线程的区别

  1. 进程是正在进行的程序的实例,而线程是进程中的实际运作单位。
  2. 一个程序有且只有一个进程,但可以拥有至少一个线程
  3. 不同进程拥有不同的地址空间,互不相关。而不同线程共同拥有相同进程的地址空间。
    在这里插入图片描述

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})
  • 1
  • 2
  • 3
  • 例一:thread的基本使用
#include <iostream>
#include <thread>

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

输出结果:

Hello, World!
  • 1

或者是

World!
Hello,
  • 1
  • 2

多线程运行时是以异步方式执行的,与我们平时写的同步方式不同。异步方式可以同时执行多条语句。

  • 例2 :thread执行有参数的函数
#include <iostream>
#include <thread>
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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

输出结果:

threadthread01finish!finish!

thread3finish!
thread2thread4finish!
finish!
thread5finish!
thread6finish!
thread7finish!
thread8finish!
thread9finish!
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 例3:thread执行带有引用参数的函数
#include <iostream>
#include <thread>
using namespace  std;

template<class T> 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<int> , 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
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

输出结果:

1 2 3 4 5 6 7 8 9 10 
  • 1

由于thread在传递参数时,是以右值传递的:

template <class Fn, class... Args>
explicit thread(Fn&& fn, Args&&... args)
  • 1
  • 2

划重点:Args&&… args
很明显的右值引用,那么我们该如何传递一个左值呢?std::refstd::cref很好地解决了这个问题。

std::ref 可以包装按引用传递的值。 std::cref 可以包装按const引用传递的值。

注意事项

  • 线程是在thread对象被定义的时候开始执行的,而不是在被调用join函数时才执行的。调用join函数只是阻塞等待线程结束并回收资源。
  • 分离的线程(执行过detach的线程)会在调用它的线程结束或自己结束时释放资源。
  • 线程会在函数运行完毕后自动释放,不推荐利用其他方法强制结束线程,可能会因资源未释放而导致内存泄露。
  • 没有执行joindetach的线程在程序结束时会引发异常

std::atomic和std::mutex

为什么需要atomic和mutex

#include <iostream>
#include <thread>
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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

输出结果:

 n : 311747
  • 1

我们的输出结果应该是1000000,可是为什么实际输出结果比1000000小呢?

在上文我们分析过多线程的执行顺序——同时进行、无次序,所以这样就会导致一个问题:多个线程进行时,如果它们同时操作同一个变量,那么肯定会出错。为了应对这种情况,c++11中出现了std::atomic和std::mutex。

std::mutex

std::mutex是c++11中最基本的互斥量,一个线程将mutex锁住时,其他的线程就不能操作mutex,直到这个线程将mutex解锁。根据这个特征,我们可以修改一下上述的代码:

主要需要引入头文件#include <mutex>

  • 例四:std::mutex的使用
#include <iostream>
#include <thread>
#include <mutex>
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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

输出结果:

 n : 1000000
  • 1

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<mutex> lockGuard(m);
        if (cnt > 0)
        {
            --cnt;
            cout << cnt << endl;
        }
    
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

std::atomic

mutex很好地解决了多线程资源争抢的问题,但是它太…慢…了…!
以例四为标准,我们定义了100个thread,每个thread要循环10000次,每次循环都要加锁、解锁,这样固然会浪费很多的时间,那么该怎么办呢?接下来就是atomic大展拳脚的时间了。

  • 例五:std::atomic的使用
    根据atomic的定义,我又修改了例四的代码:
#include <iostream>
#include <thread>
#include <atomic>
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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

输出结果:

 n : 1000000
  • 1

代码解释

可以看到,我们只是改动了n的类型(int -> std::atomic _int),其他地方一点没动,输出却正常了。其中std::atomic_intstd::atomic<int>的别名。

atomic本意为原子,官方的解释是原子操作是最小且不可并行化的操作。这意味着即使是多线程,也要像同步进行一样同步操作atomic对象,从而省去了mutex上锁、解锁的时间消耗。

std::async

注意:std::async定义在future头文件中

为什么使用async而不是thread

thread可以快速、方便的创建线程,但是在async面前,就是小巫见大巫了。
async可以根据情况选择同步执行或创建线程来异步执行,当然也可以手动操作。对于async的返回值操作也比thread更加方便。

std::async参数

不同于thread,async是一个函数,所以没有成员函数。

重载版本作用
template <class Fn, class… Args>
future<typename result_of<Fn(Args…)>::type>
async (Fn&& fn, Args&&… args)
异步或同步(根据操作系统而定)以args为参数执行fn
同样地,传递引用参数需要std::refstd::cref
template <class Fn, class… Args>
future<typename result_of<Fn(Args…)>::type>
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 <iostream>
#include <future>

using namespace  std;

int main(){
    async(launch::async, [](const char *message){
      cout << message << flush;
    },"Hello,");
    cout << "World!" << endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

输出结果:

Hello,World!
  • 1

std::future

我们已经知道如何使用async来异步或同步执行任务,但如何获得函数的返回值呢?这时候,async的返回值std::future就派上用场了。

在之前的所有例子中,我们创建线程时调用的函数都没有返回值,但如果调用的函数有返回值呢?

  • 例七:std::future的使用
#include <iostream>
#include <future>

using namespace  std;

template<class ... Args> // c++17折叠表达式
// decltype(auto)是C++14新增的类型指示符,可以用来声明变量以及指示函数返回类型。
decltype(auto) sum(Args&&... args){
    return (0 + ... + args); // "0+"避免空参数包错误
}

int main(){
    future<int> val = async(launch::async, sum<int,int,int>,1,10,100);
    cout << val.get() << endl ; // 阻塞等待线程结束并返回值
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

输出结果:

111
  • 1

代码解释
我们定义了一个函数sum,它可以计算多个数字的和,之后我们又定义了一个对象val,它的类型是std::future<int>, 这里的int代表这个函数的返回值是int类型。在创建线程后,我们使用了future::get()阻塞等待线程结束并获取其返回值。

std::future常用成员函数

函数作用
当类型为引用:R& future<R&>::get()
当类型为void:void future::get()
阻塞等待线程结束并获取返回值。
若类型为void,则与future::wait()相同。只能调用一次。
void wait() const阻塞等待线程结束
template <class Rep, class Period>
future_status wait_for(const chrono::duration<Rep,Period>& rel_time) const;
阻塞等待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 <iostream>
#include <future>

using namespace  std;

void count_big_number(){
    // c++14中,可以给数字中间加上单引号来分割数组,增强可读性
    for(int i =0 ; i <= 100'0000'0000 ; i++);
}

int main(){
    future<void> 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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

等待线程运行结束,等待期间每过1秒输出一个“,”

输出结果:

Please wait,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
finished
  • 1
  • 2

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5

promise实际上是std::future的一个包装,在讲解future时,我们并没有牵扯到改变future值的问题,但是如果使用thread以引用传递返回值的话,就必须要改变future的值,那么该怎么办呢?

实际上,future的值不能被改变,但你可以通过promise来创建一个拥有特定值的future

  • 例九 std::future的值不能改变,那么如何利用引用传递返回值
    在这里插入图片描述

std::promise常用成员函数

函数作用
当类型为引用:void promise<R&>::set_value (R& val)
当类型为void:void promise::set_value (void)
设置promise的值并将共享状态设为ready(将future_status设为ready)
void特化:只将共享状态设为ready
future get_future()构造一个future对象,其值与promise相同,status也与promise相同
  • 例十:std::promise的使用
#include <iostream>
#include <future>

using namespace  std;

template<class ... Args>
decltype(auto) sum(Args&& ... args){
    return (0 + ... + args);
}

template<class ... Args>
void sum_thread(promise<long long> &val, Args&& ... args){
    val.set_value(sum(args...));
}

int main(){
    promise<long long> sum_value;
    thread get_sum(sum_thread<int,int,int>,ref(sum_value),1,10,100);
    cout << sum_value.get_future().get() << endl;
    get_sum.join();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

输出结果:

111
  • 1

std::this_thread

在头文件中,不仅有std::thread这个类,而且还有一个std::this_thread命名空间,它可以很方便地让线程对自己进行控制。

函数作用
std::thread::id get_id() noexcept获取当前线程id
template<class Rep, class Period>
void sleep_for( const std::chrono::duration<Rep, Period>& sleep_duration )
等待sleep_duration(sleep_duration是一段时间)
void yield() noexcept暂时放弃线程的执行,将主动权交给其他线程
(放心,主动权还会回来)
  • 例十一:std::this_thread中常用函数的使用
#include <iostream>
#include <future>

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

输出结果:

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!
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

参考

https://blog.csdn.net/sjc_0910/article/details/118861539
https://www.cnblogs.com/whlook/p/6573659.html

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/153580
推荐阅读
相关标签
  

闽ICP备14008679号