菜鸟笔记
提升您的技术认知

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

目录

  • 定义
  • 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
  • 参考

定义

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

进程与线程的区别

  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})
  • 例一: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;
}

输出结果:

Hello, World!

或者是

World!
Hello,

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

  • 例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;
}

输出结果:

threadthread01finish!finish!

thread3finish!
thread2thread4finish!
finish!
thread5finish!
thread6finish!
thread7finish!
thread8finish!
thread9finish!
  • 例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 

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

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

划重点: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;
}

输出结果:

 n : 311747

我们的输出结果应该是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;
}

输出结果:

 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<mutex> lockGuard(m);
        if (cnt > 0)
        {
  
            --cnt;
            cout << cnt << endl;
        }
    
    }
}

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;
}

输出结果:

 n : 1000000

代码解释

可以看到,我们只是改动了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;
}

输出结果:

Hello,World!

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;
}

输出结果:

111

代码解释
我们定义了一个函数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秒输出一个“,”

输出结果:

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<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;
}

输出结果:

111

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;
}

输出结果:

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!