并发与多线程

1. 基本概念

1.1 并发、进程、线程的基本概念与综述

1.1.1 并发
  • 并发:表示两个或多个任务同时发生,比如一边唱歌一边弹琴;在计算机领域中,即一个程序同时执行多个独立的任务;
  • 并行:以往计算机只有单核CPU的时候,某一时刻只能执行一个任务,实现多任务的方式是每秒钟进行多次“任务切换”,比如这个任务做10ms,再切换下个任务再做10ms,因为任务切换的速度很快,所以感觉像是多个任务在同时进行。这是一种并发的假象。这种任务之间的切换也需要一定的时间和空间开销,比如操作系统要保存任务切换时的各种状态、执行进度等信息;
  • 随着“多处理器计算机”的出现,早先都是单核CPU,现在在一块芯片上有多个CPU,CPU上还有多个核,这些多处理器计算机以及多核计算机能够真正实现并发。
  • 可以看到,使用并发的主要原因主要就是能够让多件事情同时做,从而提高整体做事情的效率,提高整体的运行性能。
  • 可执行程序
    • def:就是一个磁盘上的一个文件(也叫程序);
      • windows 上,后缀(扩展名)为.exe的程序一般是可执行程序;
      • linux 上,有可执行权限的文件的权限一般是-rwxrw-r--,这里的 x 就是可执行权限,有这种权限的文件一般是可执行程序;
  • 进程
    • 进程是运行起来了的可执行程序。
  • 线程
    • 一个进程只能有一个主线程;
    • 当运行一个可执行程序,产生了一个进程之后,这个主线程就默认启动了;
    • 线程可以理解为一条代码的执行通路
    • 线程并不是越多越好,每个线程都需要一个独立的堆栈空间(耗费内存),而且线程之间的切换也要保存很多中间状态等,这也涉及上面提到过的上下文切换。如果线程太多,上下文切换的就会很频繁,而且上下文切换是一种必须但是没有价值和意义的额外工作,会耗费本该属于程序运行的时间;
    • 多线程运行效率更高,但是到底多高,设置多少个线程,并不容易评估与量化,需在实际编程与项目中多体会多调整;
1.1.2 并发的实现方法
  • 通过多进程来实现并发,每个进程做一件事(只包含一个主线程的进程);
  • 在单独的一个进程中创建多个线程来实现并发;
1.1.3 c++新标准线程库
  • 以往写多线程程序,不同的os线程创建方法不同
    • windows操作系统用CreateThread函数创建线程;
    • linux操作系统用pthread_create创建线程;
    • 跨平台的多线程库如POSIX thread(pthread)是可以的,这样可以在不同的os平台上写相同的多线程相关程序代码;
      • 但为支持pthread,windows操作系统需配置一番;
      • 如果换成linux操作系统,也需配置一番;
      • 不同的操作系统配置方法多多少少会有所不同,所有pthread使用起来也不是很方便;
    • c++11新标准增加了对多线程的支持,实现了真正的跨平台,即在windows下开发的c++多线程程序可以不加修改直接拿到同样支持c++11新标准的linux平台的c++编译器上编译;

1.2 线程启动、结束与创建线程写法

1.2.1 范例线程运行的开始与结束
#include <iostream>
#include <thread>
void myPrint(){
    std::cout << "my thread start" << std::endl;
    std::cout << "my thread end" << std::endl;
    return ;
}
int main(){
    std::thread myObj(myPrint);		//创建子线程,参数是函数名,代办线程从myPrint这个函数(初始函数0)开始运行
    myObj.join();		//join会卡在这里,等待myPrint线程执行完毕,程序流程才会继续往下走;
    std::cout << "main thread end" << std::endl;		//主线程执行,主线程从main返回,则整个进程执行完毕;
}
1.2.1.1 thread
  • std::thread myObj(myPrint); thread是c++标准库中用来创造线程的类,用可调用对象(此处是函数myPrint)作为thread构造函数的实参来构造thread对象;
1.2.1.2 join
  • join成员函数的功能:一旦执行了join这行代码,主线程就阻塞到这一行,等待其(此处是myObj)对象所代表的线程执行完毕;
    • 如果把myObj.join()注释掉,程序运行会有异常,输出也是乱序的;
    • 考虑这样一种情况:子线程还没执行完成,主线程先执行完了,导致整个进程退出,这样的代码是不合格的;
  • tips:主线程等待子线程执行完毕后,自己才能退出,这也是join语句的必要性所在;
1.2.1.3 detach
  • 主线程有义务等待所有线程执行完毕之后,才能退出,这是传统的多线程程序写法;
  • 但是detach提供了新功能——主线程不和子线程汇合了,主线程执行主线程的,子线程执行子线程的,主线程不必等子线程运行结束,可以先执行结束,这不影响子线程的进行;
  • 当一个线程被detach后,相当于被c++运行时库接管了(被分离的线程称为守护进程),当这个线程执行完后,由运行时库负责清理该线程相关资源;
  • 可以把上述代码中myObj.join()换成myObj.detach(),可以观测到不同的执行结果,这是因为主线程执行完毕后,myPrint的显示也中断了,但是myPrint代表的子线程并没有执行完毕,而是转到后台继续执行(输出结果的窗口是和主线程相关联的);
  • detach会导致程序员失去对线程的控制,多数情况下程序员需要控制线程的生命周期,所以join更常用;
  • 类与detach结合可能会带来意外问题;
1.2.1.4 joinable
  • 判断某个线程是否可以调用join或detach;
1.2.2 其他创建线程的写法
  • 用类来创建线程
    • 要通过重载 operator() 使类变成一个可调用对象(callable object),在创建了重载之后的对象后,可以像调用函数一样调用对象实例;
    • std::thread的构造函数要求传入一个可调用对象(函数指针、函数对象、lambda 表达式,或重载了 operator() 的对象等),当std::thread myobj(ta);时,std::thread会尝试将ta变成一个可调用对象来调用,会隐式调用ta.operator()来启动线程;
#include <iostream>
#include <thread>

class TA{
public:
    // TA(){
    //     std::cout << "constructor" << std::endl;
    // }
    TA(int i): m_i_(i){
        std::cout << "constructor--,m_i="<<m_i_<<",this="<<this<<std::endl; 
    }
    ~TA(){
        std::cout << "deconstructor--,m_i="<<m_i_<<",this="<<this<<std::endl; 
    }
    TA(const TA& ta):m_i_(ta.m_i_){
        std::cout << "copy constructor--,m_i="<<m_i_<<",this="<<this<<std::endl; 
    }
    // TA(const TA& ta){
    //     std::cout << "copy constructor" << std::endl;
    // }
    // int operator()
	// const means the function wouldn't change any class member
    void operator()() const{
        std::cout << "m_i_1 's value: " << m_i_ << std::endl;
        std::cout << "m_i_2 's value: " << m_i_ << std::endl;
        std::cout << "m_i_3 's value: " << m_i_ << std::endl;
        std::cout << "m_i_4 's value: " << m_i_ << std::endl;
        std::cout << "m_i_5 's value: " << m_i_ << std::endl;
        std::cout << "m_i_6 's value: " << m_i_ << std::endl;
    }
    // void operator()(int x){
    //     std::cout << "x=" << x << std::endl;
    // }

    int m_i_; 
    // reference member must be bound in constructor's initializer list, 
    // because it is an alias of another existed object.
};

int main(){
    // TA ta;
    int my_i = 6;
    TA ta(my_i);
    std::thread myObj(ta);
    myObj.join();
    std::cout << "main end" << std::endl;
}
  • 用lambda表达式创建线程
#include <iostream>
#include <thread>
int main(){
    auto myLambdaThread = []{
        std::cout << "my lamda thread start" << std::endl;
        std::cout << "my lambda thread end" << std::endl;
    };
    std::thread myObj2(myLambdaThread);
    myObj2.join();
    std::cout << "main end" << std::endl;
}

1.3 线程传参详解、detach坑与成员函数作为线程函数

1.3.1 传递临时对象作为线程参数
  • tips:
    • 如果传递 int 这种简单类型参数,建议都使用值传递,不要使用引用类型,以免节外生枝;
    • 如果传递类对象作为参数,则避免隐式类型转换(如char*转string,把int转成类A对象),全部都在创建线程这一行就构建出临时对象来,然后线程入口函数的形参位置使用引用来作为形参;
    • 建议只使用join,不适用detach,这样就不存在局部变量失效导致线程内对内存非法引用的问题;
    • c++编译器有临时对象不能作为非const引用的语义限制;
  • 智能指针作为形参传递到线程入口函数:
#include <iostream>
#include <memory>

void myPrint2(std::unique_ptr<int> ptr){
    return;
}
int main(){
    std::unique_ptr<int> my_ptr = std::make_unique<int>(10);
    std::thread my_obj(myPrint2, std::move(my_ptr));
    my_obj.join();
    std::cout << "end main" << std::endl;
}

1.3.2 用成员函数作为线程入口函数
#include <iostream>
#include <thread>
#include <memory>

class A{
public:
    A(int a): m_i_(a){
        std::cout << "A(int a) constructor, this = " << this << ", threadid = " << std::this_thread::get_id() << std::endl;
    }
    A(const A& a){
        std::cout << "A(Const A) copy constructor, this = " << this << ", threadid = " << std::this_thread::get_id() << std::endl;
    }
    void threadWork(int num){
        std::cout << "threadWork, this = " << this << ", thread id = " << std::this_thread::get_id() << std::endl;
    }
    ~A(){
        std::cout << "~A() deconstructor, this = " << this << ", threadid = " << std::this_thread::get_id() << std::endl;
    }

    void operator()(int num){
         std::cout << "operator, this = " << this << ", thread id = " << std::this_thread::get_id() << std::endl;
    }
    int m_i_;
};

void myPrint(const A& p_my_buf){
    std::cout << "address of p_my_buf: " << &p_my_buf << ", threadid = " << std::this_thread::get_id() << std::endl;
}

void myPrint2(std::unique_ptr<int> ptr){
    return;
}

int main(){
    // std::unique_ptr<int> my_ptr = std::make_unique<int>(10);
    // std::thread my_obj(myPrint2, std::move(my_ptr));
    // my_obj.join();
    // std::cout << "end main" << std::endl;

    A my_obj(10);
    std::thread my_t_obj(&A::threadWork, &my_obj, 15);
    //  std::thread my_t_obj(&A::threadWork, my_obj, 15); // also ok
    std::thread my_t_obj2(std::ref(my_obj), 16);
    my_t_obj2.join();
    my_t_obj.join();
    std::cout << "end main" << std::endl;
}
  • 用类创建线程
    • 用类的成员函数——std::thread(&A::threadWork, &my_obj, param);
    • 重载operator()——std::thread(std::ref(my_obj), param);通过std::ref()来管理引用,&是传地址,相当于传A*,但实际上是没定义的;
  • 调试时,可以在类的构造函数、拷贝构造函数、析构函数以及线程入口函数甚至主线程中增加各种输出语句,把对象的this指针值、对象或变量的地址、线程的id等各种重要信息输出以供查看;

1.4 创建多个线程、数据共享问题分析与案例代码

1.4.1 创建和等待多个线程
  • 实际的工作中,要创建的线程可能不止一个,也许有多个;
#include <iostream>
#include <vector>
#include <thread>
void myPrint(int num){
    std::cout << "my print thread, start, thread id = " << num << std::endl;
    std::cout << "my print thread, end, thread id = " << num << std::endl;
    return ;
}

int main(){
    std::vector<std::thread> my_threads;
    for(int i = 0; i < 5; ++i){
        // three writing ways are ok !!!???

        // my_threads.push_back(std::thread(std::ref(myPrint), i));
        // my_threads.push_back(std::thread(&myPrint, i));
        my_threads.push_back(std::thread(myPrint, i));
    }
    for(auto iter = my_threads.begin(); iter != my_threads.end(); ++iter){
        iter->join();
    }
    std::cout << "main end " << std::endl;
}
  • 以上例子说明:
    • 多个线程之间的执行顺序是乱的,先创建的线程不见得就比后创建的线程块;
    • 主线程等待所有子线程执行结束,最后才结束;
    • 把thread对象放到容器里进行管理,看起来像创建了一个thread对象数组,是方便管理的;
1.4.2 数据共享问题分析
  • 只读的数据:多少个线程都去读都行;
  • 有读有写:比较麻烦
1.4.3 共享数据的保护案例
  • 以一个简化的网络游戏服务器开发为例,游戏服务器程序包含两个线程,其中一个线程用来从玩家那里收集发送来的命令(数据),并把这些数据写到一个队列(容器)中,另一个线程用来从这个队列中取命令,进行解析,然后执行命令对应的动作;假设玩家每次发送的命令是一个数字;
#include <iostream>
#include <thread>
#include <list>         // double linked list
#include <mutex>        // mutex lock

class A{
public:
    //received message enqueue
    void inMsgRecvQueue(){
        for(int i = 0; i < 100000; ++i){
            std::cout << "insert a element: " << i << std::endl;
            {
                std::lock_guard<std::mutex> sb_guard(my_mutex_);
                // my_mutex_.lock();
                // surpose number is received command, make it enqueue
                msg_recv_queue_.push_back(i);
                // my_mutex_.unlock();   
            }
        }
    }

    bool outMsgLULProc(int& command){
        std::lock_guard<std::mutex> sb_guard(my_mutex_);
        // my_mutex_.lock();
        if(!msg_recv_queue_.empty()){
            command = msg_recv_queue_.front();
            msg_recv_queue_.pop_front();
            // my_mutex_.unlock();
            return true;
        }
        // my_mutex_.unlock();
        return false;
    }

    void outMsgRecvQueue(){
        int command = 0;
        for(int i = 0; i < 100000; ++i){
            bool result = outMsgLULProc(command);
            if(result == true){
                std::cout << "take a element from a vector " << command << std::endl;
            }else{
                std::cout << "now zero element" << std::endl;
            }
        }
        std::cout << "end" << std::endl;
    }
private:
    std::list<int> msg_recv_queue_;
    std::mutex my_mutex_;
};

int main(){
    A my_obj_a;
    std::thread my_out_msg_obj(&A::outMsgRecvQueue, &my_obj_a); // second param also could use std::ref
    std::thread my_in_msg_obj(&A::inMsgRecvQueue, std::ref(my_obj_a)); // second param must be reference
    my_out_msg_obj.join();
    my_in_msg_obj.join();
    std::cout << " main end " << std::endl;
}

1.5 互斥量的概念、用法、死锁演示与解决

1.5.1 互斥量基本概念
  • 可以理解为一把锁,只有一个线程可以加锁成功,其他在其解锁前必须等待;
  • 互斥量的使用原则为保护需要保护的数据,保护的共享数据少了会出现异常,保护的多了影响程序运行效率;
1.5.2 互斥量的用法
  • std::mutex my_mutex;在需要保护的数据前 my_mutex.lock(); 不需要保护时 my_mutex.unlock();理解成三明治;
  • 手动上/解锁有时容易忘记解锁,它更灵活,但是难用,替代方案是lock_guard,忘了unlock不要紧,这个可以替开发者unlock;
  • std::lock_guard<std::mutex> my_gaurd(my_mutex);,保护一段代码,在其作用域内生效,调用这句话时它的构造函数给my_mutex上锁,离开作用域时调用析构函数给my_mutex解锁;
1.5.3 死锁
  • 死锁的出现至少需要两个互斥量(也至少需要两个线程同时运行);
  • 死锁的解决方案
    • 出现多个互斥量(需要多个锁),在使用mutex.lock()与mutex.unlock()时要注意加锁顺序,也可以直接std::lock_guard;
    • std::lock函数模板,可以一次锁住两个或以上的互斥量(不可以是1个),它的特点是要么都锁住,要么都没锁住;
//改造上述例子
std::lock(my_mutex1, my_mutex2);  //此时谁先谁后无所谓;
msgRecvQueue.push_back(i);
my_mutex1.unlock();
my_mutex2.unlock();
  • 不建议使用,互斥量最好一个一个锁
  • std::lock_guard的std::adopt_lock参数:这是一个标记,用于通知系统其中的互斥量已经被lock过了,不需要通过std::lock_guardstd::mutex在构造函数中再次lock,只需要在析构函数中unlock这个互斥量就行了;
//改造上述例子
std::lock(my_mutex1, my_mutex2);  //此时谁先谁后无所谓;
std::lock_guard<std::mutex>(my_mutex1, std::adopt_lock);
(my_mutex2, std::adopt_lock);
msgRecvQueue.push_back(i);

1.6 unique_lock详解

  • 是一个类模板,用法与lock_guard相似,但比其灵活,日常工作中一般使用lock_guard即可;
  • lock了,就要unlock,使用unique_lock对互斥量lock之后,可以随时unlock,这是其灵活性所在;
  • 锁的粒度:
    • 锁住的代码少,粒度就细,程序执行效率就高;
    • 锁住的代码多,粒度就粗,程序的执行效率就低;

1.7 单例模式共享数据分析、解决与call_once

1.7.1 设计模式简单谈
  • 设计模式就是开发程序的一些代码写法,用这些代码写法的程序写起来会比较灵活(增加或减少某些功能不好牵一发而动全身),但是完全用设计模式书写项目会很痛苦(常规一个类能实现的现在要3-5个类才可以);
  • 设计模式其实是国外的开发者应付特别大的项目时把项目的开发经验、模块划分经验总结起来构成的一系列开发技巧(先有开发需求,后有理论总结和整理);一个小小的项目不必非要弄几个设计模式进去,本末倒置;
1.7.2 单例设计模式
  • 单例模式的使用频率比较高,什么叫单例?就是整个项目有某个或某些特殊的类,属于该类的对象,只能创建一个;
#include <iostream>
#include <thread>
#include <mutex>

std::mutex resource_mutex;

class MyCAS{
private:
    MyCAS(){}                           // private constructor
private:
    static MyCAS* m_instance_;
public:
    static MyCAS* getInstance(){
        // double check or double lock
        if(m_instance_ == nullptr){
            std::unique_lock<std::mutex> my_mutex(resource_mutex);
            if(m_instance_ == nullptr){
                m_instance_ = new MyCAS();
                static CGarhuishou c1;      // life cycle until program quit
            }
        }
        return m_instance_;
    }
    class CGarhuishou{                  // for release objects
    public: 
        ~CGarhuishou(){
            if(MyCAS::m_instance_){
                delete MyCAS::m_instance_;
                MyCAS::m_instance_ = nullptr;
            }
        }
    };
    void func(){                        // for test
        std::cout << "test" << std::endl;
    }
};

/*
* static member variables belong to class, 
* not belong to the specific object instance,
* 
*/ 

MyCAS* MyCAS::m_instance_ = nullptr;    

void myThread(){
    std::cout << "my thread start to execute" << std::endl;
    MyCAS* p_a = MyCAS::getInstance();
    std::cout << "my thread end" << std::endl;
    return;
}

int main(){
    // MyCAS* MyCAS::m_instance_ = nullptr;
    // MyCAS* p_a  = MyCAS::getInstance();
    // p_a->func();
    // MyCAS::getInstance()->func();

    std::thread myObject1(myThread);
    std::thread myObject2(myThread);
    myObject1.join();
    myObject2.join();

}

1.8 condition_variable、wait、 notify_one 与 notify_all

1.8.1 条件变量std::condition_variable、wait与notify_one;
  • 条件变量的作用:在线程A中等待一个条件满足,当条件满足时,线程B通知线程A,则线程A会从等待这个条件的地方往下继续执行;
#include <iostream>
#include <thread>
#include <list>         // double linked list
#include <mutex>        // mutex lock
#include <condition_variable>

class A{
public:
    //received message enqueue
    // void inMsgRecvQueue(){
    //     for(int i = 0; i < 100000; ++i){
    //         std::cout << "insert a element: " << i << std::endl;
    //         {
    //             std::lock_guard<std::mutex> sb_guard(my_mutex_);
    //             // my_mutex_.lock();
    //             // surpose number is received command, make it enqueue
    //             msg_recv_queue_.push_back(i);
    //             // my_mutex_.unlock();   
    //         }
    //     }
    // }

    void inMsgRecvQueue(){
        for(int i = 0; i < 1000000; ++i){
            std::cout << "inMsgRecvQueue() execute, insert an element " << i << std::endl;
            std::unique_lock<std::mutex> sb_guard1(my_mutex_);
            msg_recv_queue_.push_back(i);
            // try to awake the wait()'s thread, but it's not enough to awake
            // here must unlock the mutex and the other thread's wait() will continue working
            my_cond_.notify_one();
        
        }
        producer_done_ = true;
        my_cond_.notify_one();
    }

    // bool outMsgLULProc(int& command){
    //     // double lock or double check
    //     if(!msg_recv_queue_.empty()){
    //         std::lock_guard<std::mutex> sb_guard(my_mutex_);
    //         // my_mutex_.lock();
    //         if(!msg_recv_queue_.empty()){
    //             command = msg_recv_queue_.front();
    //             msg_recv_queue_.pop_front();
    //             // my_mutex_.unlock();
    //             return true;
    //         }
    //     }
    //     // my_mutex_.unlock();
    //     return false;
    // }

    // void outMsgRecvQueue(){
    //     int command = 0;
    //     for(int i = 0; i < 100000; ++i){
    //         bool result = outMsgLULProc(command);
    //         if(result == true){
    //             std::cout << "take a element from a vector " << command << std::endl;
    //         }else{
    //             std::cout << "now zero element" << std::endl;
    //         }
    //     }
    //     std::cout << "end" << std::endl;
    // }
    void outMsgRecvQueue(){
        int command = 0;
        while(true){
            std::unique_lock<std::mutex> sb_guard1(my_mutex_);
            // wait() is used to wait for something
            // if wait()'s second parameter---lambda expression, the return value of it is true, return directly;
            // if wait()'s second parameter---lambda expression, the return value of it is false, so wait() will 
            // unlock mutex and block the line, until some other thread call notify_one()
            // if do not use wait()'s second parameter, its default value is the same with the later one---unlock
            // mutex and block the line, until some other thread call notify_one();
            my_cond_.wait(sb_guard1, [this]{
                return !msg_recv_queue_.empty() 
                || producer_done_;
            });
            if(msg_recv_queue_.empty() && producer_done_) 
                break;
            command = msg_recv_queue_.front();      // return the first element
            msg_recv_queue_.pop_front();            // remove the first element
            sb_guard1.unlock();                     // because of the flexibilities of unique_lock, it could unlock at anytime 
            std::cout << "outMsgRecvQueue() execute, take out element "<< command << std::endl;
        }
    }

private:
    std::list<int> msg_recv_queue_;
    std::mutex my_mutex_;
    std::condition_variable my_cond_;
    bool producer_done_ = false;
};

int main(){
    A my_obj_a;
    std::thread my_out_msg_obj(&A::outMsgRecvQueue, &my_obj_a); // second param also could use std::ref
    std::thread my_in_msg_obj(&A::inMsgRecvQueue, std::ref(my_obj_a)); // second param must be reference
    
    my_in_msg_obj.join();
    my_out_msg_obj.join();
    std::cout << " main end " << std::endl;
}

1.9 async、future、packaged_task 与 promise

1.9.1 std::async和std::future创建后台任务并返回值
  • std::async 和 std::future 的用法
    • 以往多线程编程中,用std::thread创建线程,用join来等待线程;
    • 现在有一个需求,希望线程返回一个结果;可以把线程执行结果赋值给一个全局变量,但是还有更好的方法;
    • std::async 是一个函数模板,通常的说法是用来启动一个异步任务,启动这个异步任务后,会返回一个std::future对象(std::future是一个类模板);
    • 启动一个异步任务,即std::async会自动创建一个新线程并开始执行对应的线程入口函数;它返回一个std::future对象,这个对象里含有线程入口函数的返回结果;可以通过future对象的成员函数get来获取结果;
    • std::future提供了一种访问异步操作结果的机制,就是说这个结果可能没办法马上拿到,但不久的将来等线程执行完了,就可以拿到(未来的值)。可以这么理解:future中会保存一个值,在将来某个时刻能够拿到;
#include <iostream>
#include <future>
#include <thread>

int myThread(){
    std::cout << "thread start," << "thread id = " << std::this_thread::get_id() << std::endl;
    std::chrono::milliseconds dura(2000);
    std::this_thread::sleep_for(dura);
    std::cout << "thread end, " << "thread id = " << std::this_thread::get_id() << std::endl;
    return 5;
}

int main(){
    std::cout << "main" << " thread id = " << std::this_thread::get_id() << std::endl;
    std::future<int> result = std::async(myThread); 
    std::cout << "continue..." << std::endl;
    std::cout << result.get() << std::endl;         // stop here until thread finish, only get() for once 
    std::cout << "main end" << std::endl;
    return 0;
}
  • std::async 额外参数详解
    • std::launch::deferred:线程入口函数的执行被延迟到std::future的wait或get函数调用时;
    • std::launch::async:调用async函数时就开始创建并执行线程(强制这个异步任务在新线程上执行),这意味着系统必须要创建出新线程来执行;
    • std::launch::async | std::launch::deferred
    • 不用任何额外的参数(相当于std::launch::async|std::launch::defferred作为额外参数),系统自行决定是以同步(不创建新线程)或异步(创建新线程)的方式来执行任务;
  • std::async 和 std::thread 的区别
    • 创建线程一般用std::thread方法,但是如果在一个进程中创建的线程太多导致系统资源紧张,继续调用std::thread可能会导致创建线程失败,程序也随之崩溃;
    • 如果线程返回一个值,用std::thread这种创建线程的方式,这个值想拿到手也不容易;
    • 这时就想到了std::async,std::async其实是叫创建异步任务,也就是说std::async可能创建线程,也可能不创建,同时还有一个独特的优点:这个异步任务返回的值程序员可以通过std::future对象在将来某个时刻(线程执行完)直接拿到手;
int myThread(){
  return 1;
}

int main(){
  std::future<int> result = std::async(myThread);
  std::cout << result.get() << std::endl;
}
  • 根据经验来讲,一个程序(进程)里面创建的线程数量,要根据具体情况测试,太少了不好,太多了也不好,线程调度、切换线程运行都要消耗系统资源和时间;
1.9.2 std::packaged_task
  • 这是一个类模板,它的模板参数是各种可调用对象,通过packaged_task把各种可调用对象包装起来,方便将来作为线程入口函数来调用;

    • 打包一个单独的函数
#include <iostream>
#include <thread>
#include <future>

int myThread(int my_par){
    std::cout << my_par << std::endl;
    std::cout << "my thread() start" << " thread id = " << std::this_thread::get_id() << std::endl;
    std::chrono::milliseconds dura(2000);
    std::this_thread::sleep_for(dura);
    std::cout << "my thread() end" << " thread id = " << std::this_thread::get_id() << std::endl;
    return 5;
}
int main(){
    std::cout << "main" << " thread id = " << std::this_thread::get_id() << std::endl;
    // <int(int)> means that the first int is the task's return type, 
    // the second int is the accepted param type
    std::packaged_task<int(int)> my_p_t(myThread);  // packaged by packaged_task
    std::thread t1(std::ref(my_p_t), 1);
    t1.join();
    
    std::future<int> result = my_p_t.get_future();
    std::cout << result.get() << std::endl; 
    std::cout << "main end" << std::endl;
    return 0;   
}
  • 用std::packaged_task包装一个lambda表达式
#include <iostream>
#include <thread>
#include <future>
int main(){
    // a lambda way---anonymous function way
    std::cout << "main" << " thread id = " << std::this_thread::get_id() << std::endl;
    std::packaged_task<int(int)> my_p_t([](int my_par){
        std::cout << my_par << std::endl;
        std::cout << "lambda mythread() start" << " thread id = " << std::this_thread::get_id() << std::endl;
        std::chrono::milliseconds dura(5000);
        std::this_thread::sleep_for(dura);
        std::cout << "lambda mythread() end" << " thread id = " << std::this_thread::get_id() << std::endl;
        return 15;
    });
    std::thread t1(std::ref(my_p_t), 1);
    t1.join();
    std::future<int> result = my_p_t.get_future();
    std::cout << result.get() << std::endl;
    std::cout << "main end" << std::endl;
    return 0;
}
  • 包装起来的对象可以直接调用,从这个角度来讲,packaged_task对象也是一个可调用对象;
#include <iostream>
#include <thread>
#include <future>
int main(){
    // packaged_task as a callable object
    std::cout << "main" << " thread id = " << std::this_thread::get_id() << std::endl;
    std::packaged_task<int(int)> my_p_t([](int my_par){
        std::cout << my_par << std::endl;
        std::cout << "lambda mythread() start" << " thread id = " << std::this_thread::get_id() << std::endl;
        std::chrono::milliseconds dura(5000);
        std::this_thread::sleep_for(dura);
        std::cout << "lambda mythread() end" << " thread id = " << std::this_thread::get_id() << std::endl;
        return 15;
    });
    my_p_t(105);
    std::future<int> result = my_p_t.get_future();
    std::cout << result.get() << std::endl;
    std::cout << "main end" << std::endl;
    return 0;
    // std::thread t1(std::ref(my_p_t), 1);
    // t1.join();
    // std::future<int> result = my_p_t.get_future();
    // std::cout << result.get() << std::endl;
    // std::cout << "main end" << std::endl;
    // return 0;
}
  • 放到容器里
#include <iostream>
#include <thread>
#include <future>
#include <vector>

std::vector<std::packaged_task<int(int)>> my_tasks;

int main(){
    std::cout << "main" << " thread id = " << std::this_thread::get_id() << std::endl;
    std::packaged_task<int(int)> my_p_t([](int my_par){
        std::cout << my_par << std::endl;
        std::cout << "lambda mythread() start" << " thread id = " << std::this_thread::get_id() << std::endl;
        std::chrono::milliseconds dura(5000);
        std::this_thread::sleep_for(dura);
        std::cout << "lambda mythread() end" << " thread id = " << std::this_thread::get_id() << std::endl;
        return 15;
    });
    // enter the vector
    my_tasks.push_back(std::move(my_p_t)); // move semantics
    // leave the vevtor
    std::packaged_task<int(int)> my_p_t_2;
    auto iter = my_tasks.begin();
    my_p_t_2 = std::move(*iter);
    my_tasks.erase(iter);
    my_p_t_2(123);
    std::future<int> result = my_p_t_2.get_future();
    std::cout << result.get() << std::endl;
    std::cout << "main end" << std::endl;
}

1.9.3 std::promise

  • 也是一个类模板,作用是:能够在某个线程里为其赋值,然后可以在其他的线程中,把这个值取出来使用;
posted @ 2024-11-02 15:45  正明小佐  阅读(1)  评论(0编辑  收藏  举报