C++多线程编程——thread线程创建与使用(2W字保姆级介绍)

目录

前言

线程创建

标准库thread(同步线程的创建过程)

启动线程:实例thread

线程执行单元(可调用对象)

线程等待

线程传参

线程id

成员方法获取线程id

命名空间获取线程id

让出线程资源

sleep_for()

sleep_until

thread的构造函数

thread的成员函数

异步async(异步线程)、future

从实例中理解异步同步线程

std::future可以从异步线程中获取返回值结果

shared_future可以获取多次结果

async参数

std::launch::deferred:表示线程入口函数调用被延迟到std::future的wait()或者get()函数调用时才执行;(如果没有调用,即使主线程结束,线程入口函数也永远不会被调用)

wait()函数

std::launch::async:在调用async函数的时候就开始创建线程

wait_for函数

同步和异步总结补充

std::promise

单向通信

 双向通信

std::package_task

创建使用(对参数的具体讨论)

线程传参

lambda表达式作为线程执行单元

函数对象作为线程执行单元

成员函数作为线程执行单元

引用传参

类成员作为执行单元的共享成员变量


前言

C++标准库封装了POSIX线程库,将其封装成了一个线程类。学习C++标准库多线程的好处是:第一可以跨平台,第二上层封装可以解决很多不必要的麻烦,同时解决了运行效率问题,尤其对于线程同步来说,比用原生态的C接口简单得多,提供了高效写多线程的一种方式

多进程编程主要考虑的是进程间通信,而多线程主要考虑的是线程同步

线程创建

C++提供了两种线程:同步线程和异步线程

  • 同步执行:同步,是所有的操作都做完,才返回给用户结果
  • 异步执行:异步,不用等所有操作都做完,就响应用户请求

基本上所有实际应用的线程都是异步的,比如点击浏览器进行下载,浏览器正在下载的同时,我们还可以做其它事情。

对于线程的创建,必须要掌握:

第一步:要学会用thread创建同步线程,

第二步:当某些情况下需要使用async创建异步任务,

第三步:在thread和async里最重要的就是future,用future来获取线程返回值结果,当thread创建时需要用packaged_task做绑定包装

第四步:当要做线程间通信时,需要用promise和future做绑定

标准库thread(同步线程的创建过程)

启动线程:实例thread

线程执行单元(可调用对象)

  • 普通函数
  • 函数对象
  • 类成员函数:第二个参数为对象的引用或指针
  • lambda表达式

线程等待

  • void std::thread::join();
  • void std::thread::detach();

线程传参

  • 用detach()时,如果主线程先结束,变量就会被回收;所以用detach()的话,不推荐用引用
  • 如果参数有引用就会发生错误,需要使用std::ref来告诉其传递的是引用。(std::ref()函数的作用:可以实现真正的引用传递)
  • 智能指针做参数传递,可以直接传

线程id

  • std::this_thread命名空间:命名空间保存了当前线程的所有信息
  • std::this_thread::get_id()来获取

成员方法获取线程id

#include 
#include 
#include 
using namespace std;

class MyThread
{
public:
    int myfun1()
    {
        for(int i=0;i<3;i++)
        {
            m_count++;
            sleep(1);
        }
        return 0; 
    }

    int myfun2()
    {
        for(int i=0;i<3;i++)
        {
            cout << "count = "<C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第1张图片

命名空间获取线程id

#include 
#include 
#include 
using namespace std;

class MyThread
{
public:
    int myfun1()
    {
        for(int i=0;i<3;i++)
        {
            m_count++;
            sleep(1);
        }
        cout <<"t1:id = "<C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第2张图片

让出线程资源

 在线程中使用this_thread::yield()函数可以交出执行当前线程的CPU去调度其它线程。

#include 
#include 
#include 
using namespace std;

class MyThread
{
public:
    int myfun1()
    {
        for(int i=0;i<3;i++)
        {
            m_count++;
            sleep(1);
            this_thread::yield();
        }
        cout <<"t1:id = "<C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第3张图片

sleep_for()

作用和sleep一样,但可以实现精确传参。作用是使线程休眠某个指定的时间片(time span),该线程才被重新唤醒。

#include 
#include 
#include 
using namespace std;

class MyThread
{
public:
    int myfun1()
    {
        for(int i=0;i<3;i++)
        {
            m_count++;
            //sleep(1);
            this_thread::sleep_for(3000ms);
        }
        cout <<"t1:id = "<C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第4张图片

sleep_until

线程休眠至某个指定的时刻(time point),该线程才被重新唤醒。

作用:阻塞当前正在执行的线程直到sleep_time溢出。

sleep_time是和时钟相关联的,也就是要注意时钟调整会影响到sleep_time。

因此, 时钟的时长有可能或没有可能会短于或长于sleep_time。Clock::now()返回调用这个函数时的时间,取决于调整方向。该函数也有可能因为调度或者资源竞争而导致阻塞时间延长到sleep_time溢出之后。

#include   
#include   
#include   
#include   
#include   
#pragma warning(disable:4996)//加上可去掉unsafe 请使用localtime_s的编译报错  
int main()  
{  
    using std::chrono::system_clock;  
    std::time_t tt = system_clock::to_time_t(system_clock::now());  
    struct std::tm *ptm = std::localtime(&tt);  
    std::cout << "Current time: " << std::put_time(ptm, "%X") << '\n'; //必须大写X,若小写x,输出的为日期  
    std::cout << "Waiting for the next minute to begin...\n";  
    ++ptm->tm_min;   
    ptm->tm_sec = 0;  
    std::this_thread::sleep_until(system_clock::from_time_t(mktime(ptm)));  
    std::cout << std::put_time(ptm, "%X") << "reached!\n";  
    getchar();  
    return 0;  
}  

 

thread的构造函数

在C++中,std::thread的构造函数可以接受一个可调用的对象(函数、函数指针、lambda表达式、bind表达式等),以此来指定在新线程中要执行的任务。

以下是std::thread的几种常见的构造函数形式:

1.默认构造函数:创建一个表示未开始执行的线程的对象。

std::thread();

2.接受一个可调用的对象作为参数,并开始一个新的线程来执行该任务。

template 
explicit thread(Function&& f, Args&&... args);

例如:

void my_function(int x, int y) { ... }
int main() {
    std::thread t(&my_function, 10, 20);  // 创建一个新线程来执行 my_function,参数为 10 和 20
    t.join();  // 等待新线程执行完毕
    return 0;
}

3.接受两个可调用的对象作为参数,并开始一个新的线程来执行这两个任务。这是两个任务的并行执行,不是第一个任务等待第二个任务。

template 
void thread(Function1&& f1, Function2&& f2);

例如:

void my_function1() { ... }
void my_function2() { ... }
int main() {
    std::thread t1(&my_function1);  // 创建一个新线程来执行 my_function1
    std::thread t2(&my_function2);  // 创建一个新线程来执行 my_function2
    t1.join();  // 等待第一个线程执行完毕
    t2.join();  // 等待第二个线程执行完毕
    return 0;
}

thread的成员函数

常用函数在前面和后面都有具体介绍,重要的有join、detach、get_id和native_handle。

此外C++11的线程可以使用平台的特性拓展

std::native_handle_type std::native_handle();

该函数的作用是获取当前平台真正的线程id,如windows就返回windows的线程id,Linux就返回Linux的线程id。转换之后就可以通过线程id使用C语言的线程接口了。

#include 
#include 
#include 
using namespace std;

class MyThread
{
public:
    int myfun1()
    {
        for(int i=0;i<3;i++)
        {
            m_count++;
            //sleep(1);
            this_thread::sleep_for(3000ms);
        }
        cout <<"t1:id = "<异步async(异步线程)、future 
  

std::async用于创建异步任务,实际上就是创建一个线程执行相应任务

std::futureresult = std::async(mythread);//创建一个线程并开始执行,绑定关系,流程并不卡在这里

std::futureresult = std::async(std::launch::deferred,&A::mythread2,&a,tmppar);//第二个参数是对象引用,才能确保线程里面是同一个对象

从实例中理解异步同步线程

创建同步线程,当主线程退出时,不用detach和join会导致子线程未退出而造成资源泄露。

#include 
#include 
#include 

using namespace std;

int mythread(int num)
{
    for(int i=0;i<3;i++)
    {
        cout << "hello world"<

当使用异步线程,则可以正常打印(创建异步线程,会使主线程阻塞,在次线程执行完之后再继续执行)。异步任务不属于当前开销,即使主线程结束,次线程任务仍可执行

#include 
#include 
#include 

using namespace std;

int mythread(int num)
{
    for(int i=0;i<3;i++)
    {
        cout << "hello world"<

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第5张图片

std::future可以从异步线程中获取返回值结果

future可以和async结合使用,来接收异步线程的返回值,当结合时,主线程将不会阻塞,可以实现真正的异步任务操作

#include 
#include 
#include 

using namespace std;

int mythread(int num)
{
    for(int i=0;i<3;i++)
    {
        cout << "hello world"<result = async(mythread,5);//创建异步线程
    for(int i=0;i<3;i++)
    {
        cout <<"main exit!"<

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第6张图片

在主线程使用future::get方法会使主线程阻塞等待次线程结束

#include 
#include 
#include 

using namespace std;

int mythread(int num)
{
    for(int i=0;i<3;i++)
    {
        cout << "hello world"<result = async(mythread,5);//创建异步线程
    for(int i=0;i<3;i++)
    {
        cout <<"main exit!"<

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第7张图片

shared_future可以获取多次结果

使用std::future创建的对象只可以调用一次get方法来获取线程结果,但使用shared_future可以共享某个共享状态的最终结果,可以拷贝多个

#include 
#include 
#include 
#include 
using namespace std;

int thread_func(int num)
{
    for(int i=0;i<3;i++)
    {
        cout <<"num = "<mt(thread_func);
    thread t(ref(mt),10);
    t.join();
    cout <<"thread exit!"<result = mt.get_future();

    cout <<"result1 = "<C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第8张图片

async参数

std::launch::deferred:表示线程入口函数调用被延迟到std::future的wait()或者get()函数调用时才执行;(如果没有调用,即使主线程结束,线程入口函数也永远不会被调用)

 

#include 
#include 
#include 

using namespace std;

int mythread(int num)
{
    for(int i=0;i<3;i++)
    {
        cout << "hello world"<result = async(mythread,5);//创建异步线程
    futureresult1 = async(std::launch::deferred,mythread,6);

    //cout<

wait()函数

使用wait()方法只用于通知此线程任务开始执行

#include 
#include 
#include 

using namespace std;

int mythread(int num)
{
    for(int i=0;i<3;i++)
    {
        cout << "hello world"<result = async(mythread,5);//创建异步线程
    futureresult1 = async(std::launch::deferred,mythread,6);
    cout << "main exit!"<

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第9张图片

std::launch::async:在调用async函数的时候就开始创建线程

#include 
#include 
#include 

using namespace std;

int mythread(int num)
{
    for(int i=0;i<3;i++)
    {
        cout << "hello world"<result = async(mythread,5);//创建异步线程
    futureresult1 = async(std::launch::async,mythread,6);

    //cout<

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第10张图片

wait_for函数

wait_for函数第一个作用是用于创建同步线程时,主线程固定阻塞一段时间后退出

#include 
#include 
#include 

using namespace std;

int mythread(int num)
{
    for(int i=0;i<3;i++)
    {
        cout << "hello world"<mt(mythread);
    futureresult=mt.get_future();
    thread t(ref(mt),5);
    //futureresult = async(mythread,5);//创建异步线程
    //futureresult1 = async(std::launch::async,mythread,6);
    cout << "main exit!"<

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第11张图片

wait_for第二个作用是用来判断当前某个线程状态

#include 
#include 
#include 

using namespace std;

int mythread(int num)
{
    for(int i=0;i<3;i++)
    {
        cout << "hello world"<mt(mythread);
    //futureresult=mt.get_future();
    //thread t(ref(mt),5);
    //futureresult = async(mythread,5);//创建异步线程
    futureresult1 = async(std::launch::deferred,mythread,6);
    cout << "main exit!"<

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第12张图片

同步和异步总结补充

  • 同步执行:同步,是所有的操作都做完,才返回给用户结果
  • 异步执行:异步,不用等所有操作都做完,就响应用户请求
  • 处理耗时操作(数据库大量写入或者查询、文件下载、复杂计算)
    • 同步操作:所有的操作都做完,才返回给用户,这样用户在线等待的时间太长,给用户一种卡死了的感觉
    • 异步操作:即先响应用户请求然后慢慢去执行耗时操作,用户体验较好

std::promise

对于线程间的通信,我们一般通过全局变量来实现,但在C++中通过std::promise也可以实现线程间的通信,相对于全局变量,不需要用锁来确保安全性

  • 类模板,std::promise保存的值可被与之关联的std::promise读取,读取操作可以发生再其它线程
  • 作用:std::promise和std::future合作共同实现了多线程间通信
  • 注意事项
    • std::promise允许move语义(右值构造,右值赋值),但不允许拷贝(拷贝构造、赋值)
    • set_value只能被调用一次,多次调用会抛出std::future_error异常
    • 一个std::promise实例只能与一个std::future关联共享状态,当在同一个std::promise上反复调用get_future会抛出future_error异常
    • 通过std::promise让std::future抛出异常
      • std::promise虽然支持自定义异常,但它并不直接接受异常对象
      • 自定义异常可以通过位于头文件exception下的std::make_exception_ptr函数转化为std::exception_ptr。它表示异常的指针。这个指针可以用于在不同的上下文中传递和处理异常,例如在多线程环境中传递异常,或者在捕获异常后延迟处理。
    • std::promise此时std::promise.set_value不接受任何参数,仅用于通知关联的std::future.get()解除阻塞

单向通信

#include 
#include 
#include 
using namespace std;
//程序目的:想要把线程1中的iVal值传给线程2
void Thread_Fun1(std::promise &p)
{
    //为了突出效果,可以使线程休眠5s
    this_thread::sleep_for(std::chrono::seconds(5));

    int iVal = 233;
    cout<<"传入数据(int)"< &f)
{
    //阻塞函数,直到收到相关联的std::promise对象传入的数据
    auto iVal = f.get();//iVal=233

    cout<<"收到数据(int)"< prl;
    //声明一个std::future对象ful,并通过std::promise的get_future()函数与prl绑定
    futureful = prl.get_future();

    //创建一个线程t1,将函数Thread_Fun1及对象prl放在线程里面执行
    std::thread t1(Thread_Fun1,std::ref(prl));
    //创建一个线程t2,将函数Thread_Fun2及对象ful放在线程里面执行
    std::thread t2(Thread_Fun2,std::ref(ful));

    //阻塞至线程结束
    t1.join();
    t2.join();
    return 0;
}

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第13张图片

 双向通信

#include 
#include 
#include 
using namespace std;
//程序目的:想要把线程1中的iVal值传给线程2
void Thread_Fun1(std::promise &p,std::future &f)
{
    //为了突出效果,可以使线程休眠5s
    this_thread::sleep_for(std::chrono::seconds(5));

    int iVal = 233;
    cout<<"传入数据(int)"< &f,std::promise &p)
{
    //阻塞函数,直到收到相关联的std::promise对象传入的数据
    auto iVal = f.get();//iVal=233

    cout<<"收到数据(int)"< prl1;
    promise prl2;

    //声明一个std::future对象ful,并通过std::promise的get_future()函数与prl绑定
    futureful1 = prl1.get_future();
    futureful2 = prl2.get_future();

    //创建一个线程t1,将函数Thread_Fun1及对象prl放在线程里面执行
    std::thread t1(Thread_Fun1,std::ref(prl1),std::ref(ful2));
    //创建一个线程t2,将函数Thread_Fun2及对象ful放在线程里面执行
    std::thread t2(Thread_Fun2,std::ref(ful1),std::ref(prl2));

    //阻塞至线程结束
    t1.join();
    t2.join();
    return 0;
}

std::package_task

std::package_task它允许传入一个函数,并将函数计算的结果传给std::future,包括函数运行时产生的异常

注意事项(拓展)

  • std::package_task支持move,但不支持拷贝(copy)
  • get_future仅能调用一次,多次调用会触发std::future_error异常
  • std::packaged_task::valid该函数用于判断std::packaged_task对象是否是有效状态
  • std::packaged_task::reset()使std::packged_task可以执行多次

使用实例

package_task相对于一个包装器,如function:可以将要调用的对象包装成统一类型。比如我们想要获取线程退出的结果,再C接口中的int pthread_join(pthread_t thread,void **retval);中的retval就用于存放线程id退出的返回值。但使用C++的join无法获得线程退出的返回值。

我们可以使用package_task配合future进行返回值的绑定,头文件为

#include 
#include 
#include 
#include 
using namespace std;

int thread_fun1(int num)
{
    cout << "num = "<
    packaged_taskmt(thread_fun1);//packaged_task为类模板
    futureresult = mt.get_future(); //future也为类模板,要指定线程的返回值类型
    /*result和mt的返回值做了绑定,一旦thread_fun1执行完了,就会将返回值传给result*/
    //包装后需要按引用来传参
    thread t(std::ref(mt),5);
    
    t.join();

    cout<<"result count = "<

其中future的get方法会阻塞,直到线程结束为止,获取返回值

#include 
#include 
#include 
#include 
using namespace std;

int thread_fun1(int num)
{
    cout << "num = "<
    packaged_taskmt(thread_fun1);//packaged_task为类模板
    futureresult = mt.get_future(); //future也为类模板,要指定线程的返回值类型
    /*result和mt的返回值做了绑定,一旦thread_fun1执行完了,就会将返回值传给result*/
    //包装后需要按引用来传参
    thread t(std::ref(mt),5);
    
    cout<<"result count = "<

创建使用(对参数的具体讨论)

#include 
#include 
#include 
using namespace std;

void mythread(void)//线程函数可以随便写,参数和返回值不固定
{
    for(int i=0;i<3;i++)
    {
        cout<<"hello world"<

线程传参

对于线程传参,直接在对象后面添加传入参数

#include 
#include 
#include 
using namespace std;

void mythread(int num)//线程函数可以随便写,参数和返回值不固定
{
    for(int i=0;i<3;i++)
    {
        cout << "num = "<

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第14张图片

如果想要传入字符串,会报错,这是因为传入字符串,构造函数会默认转化成string类的对象

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第15张图片

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第16张图片

使用string,就不会报错

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第17张图片

或者使用传入字符串指针或者数组

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第18张图片

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第19张图片

lambda表达式作为线程执行单元

#include 
#include 
#include 
using namespace std;
int main(int argc, char const *argv[])
{
    auto F = [](int num,const char *str)
    {
        for(int i=0;i<3;i++)
        {
            cout << "num = "<

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第20张图片

函数对象作为线程执行单元

... ...
class Test
{
    public:
        void operator()(int num,const char *ptr)
        {
            for(int i=0;i<3;i++)
            {
                cout << "num = "<

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第21张图片

成员函数作为线程执行单元

线程构造函数第一个参数为成员函数时,由于成员函数的地址代表在类中的偏移量,因此第二个参数必须是对象的本身,可以是对象、对象的地址或者对象的引用

 

class MyThread
{
public:
    void myfun(int num, const char *ptr)
    {
        for(int i=0;i<3;i++)
        {
            cout << "num = "<

引用传参

如果参数传递引用

比如传入一个对象的左值引用,则会报错

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第22张图片

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第23张图片

在传入引用的时候,我们必须使用`std::ref `,它是 C++ 标准库中的一个函数模板,它的作用是将传递给它的对象包装成一个可以传递给函数的引用包装器。这样可以让那些原本不接受引用的函数接受引用参数,从而实现在函数中修改传入对象的值。

#include 
#include 
#include 
using namespace std;

void mythread(int &num,string& str)//线程函数可以随便写,参数和返回值不固定
{
    for(int i=0;i<3;i++)
    {
        cout << "num = "<

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第24张图片

C++线程不能直接传引用的原因是为了防止次线程还没来得及退出,主线程就结束了。比如我们忘记使用join(由主线程回收资源)或者使用了detach(主线程退出后,由系统回收线程资源),string &rs创建的rs对象当主线程退出时就会释放,rs作为次线程传入的参数,而次线程还没来得及退出,这就会导致安全性的问题。因此编译器默认引用传递存在危险,因此使用ref。

例:使用引用传递在线程中修改变量值

#include 
#include 
#include 
using namespace std;
void mythread2(int &num,string& str)//线程函数可以随便写,参数和返回值不固定
{

        cout << "num = "<

如果接收参数是右值引用,不能用ref

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第25张图片

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第26张图片

需要使用move强制转换为右值引用再传参

#include 
#include 
#include 
using namespace std;
void mythread3(int &&num,string& str)//线程函数可以随便写,参数和返回值不固定
{

        cout << "num = "<

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第27张图片

类成员作为执行单元的共享成员变量

当使用类成员函数作为执行单元,传递对象,无法共用成员变量,传递对象的引用或者地址可以共用成员变量。

如传递对象,则结果如下:

#include 
#include 
#include 
using namespace std;

class MyThread
{
public:
    int myfun1()
    {
        for(int i=0;i<3;i++)
        {
            m_count++;
            sleep(1);
        }
        return 0; 
    }

    int myfun2()
    {
        for(int i=0;i<3;i++)
        {
            cout << "count = "<

可以发现打印结果全为0

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第28张图片

如果使用地址或者引用,结果如下:

#include 
#include 
#include 
using namespace std;

class MyThread
{
public:
    int myfun1()
    {
        for(int i=0;i<3;i++)
        {
            m_count++;
            sleep(1);
        }
        return 0; 
    }

    int myfun2()
    {
        for(int i=0;i<3;i++)
        {
            cout << "count = "<

发现线程操作成员变量正常

C++多线程编程——thread线程创建与使用(2W字保姆级介绍)_第29张图片

你可能感兴趣的:(C++,c++,嵌入式,同步与互斥,多线程编程)