美文网首页C/C++编程
C++并发编程:线程管理

C++并发编程:线程管理

作者: Sivin | 来源:发表于2021-03-28 16:47 被阅读0次

    1. 线程启动

    线程在std::thread对象创建时启动,即,在构造std::thread对象时启动,为了能让编译器识别std::thread类,需要包含<thread>头文件。每一个线程都需要一个入口函数,因此构造线程的一个必不可少的参数,就是指明线程的入口函数。

    1.1 普通函数作为线程入口

    void thread_run();
    std::thread my_thread{thread_run};
    

    1.2 类成员函数作为线程入口

    class background_task {
    public:
        void thread_run() {
            std::cout << __FUNCTION__<<std::endl;
        }
    }
    
    void thread_test() {
        background_task task{};
        std::thread my_thread{&background_task::thread_run, &task};
        //注意:不要忘了对task对象取地址操作
        std::thread my_thread2{&background_task::thread_run, task};// 1
    }
    

    这里需要注意,虽然①在一般情况下也能正常运行,但task作为线程参数被复制到新的线程内存空间中执行,因此,执行当前线程的background_task实例并不是原来的入参task

    1.3 可调用类型对象作为线程入口

    class background_task {
    public:
        void operator()() const {
            std::cout << __FUNCTION__ << std::endl;
        }
    }
    void thread_test() {
        backgourn_task task{};
        std:: thread my_thread {task}
    }
    

    1.4 使用 lambda表达式作为线程入口

      std::thread my_thread{ [] {
        std::cout << __FUNCTION__ << std::endl;
      } };
    

    代码中提供的函数对象会复制到新线程的存储空间中,函数对象的执行和调用都在线程的内存空间中进行。函数对象的副本应与原函数保持一致,否则得到的结果与我们的期望不同。

    启动了线程,你还需要明确的是:要等待线程执行结束,还是让其自主运行,如果std::thread对象销毁前还没有做出决定,程序就会终止(std::thread的析构函数会调用std::terminate())。因此,即便是有异常存在,也需要保证线程能够正确的join或者detached

    如果不等待线程,就必须保证线程结束之前,可访问的数据有效性,这不是一个新问题,即便是在单线程代码中,对象销毁后再去访问,也会产生未定义的行为。不过线程的生命周期增加了这个问题的发生几率。

    这种情况很可能发生在线程还没结束,函数已经退出的时候,这时线程函数还持有函数的局部变量或者引用。

    1.5 重载函数作为函数入口

    当线程的入口函数存在多个重载时,按着上面的方式,编译器无法确认应该将那个版本的函数作为入口函数,因此我们需要明确的告诉编译器。

    void fun_run() {
    }
    void fun_run(int a, const std::string& name) {
    }
    
    void test() {
      std::thread t1{ static_cast< void (*)() >(fun_run) };
      std::thread t2{ static_cast< void (*)(int, const std::string&) >(fun_run) ,10, std::string("abc")};
    }
    

    2. 向线程函数传递参数

    2.1 传递普通变量

    void thread_run(int a) {
      std::cout << " a =" << a << std::endl;
    }
    void thread_test() {
      int n = 3;
      std::thread my_thread{ thread_run, n };
    }
    

    需要注意的是,参数是拷贝到线程独立内存中,即使是引用,也是如此。

    2.2 传递引用

    下面的代码是编译不过的

    void thread_run(int& a) { //1
        a = 2;
    }
    void thread_test() {
      int n = 3;
      std::thread my_thread{ thread_run, n }; //2
    }
    

    虽然thread_run()的参数是引用类型,但当编译器解析到①处代码时,并没有信息告知编译器thread_run()函数所需要的参数类型;因为此处执行是std::thread构造函数,my_thread()对于std::thread构造函数而言只是一个函数指针,因此当将变量n直接传入时,编译器认为thread_run()函数接受的是一个普通变量,因此就将n直接拷贝到线程空间中;而thread_run()在定义处明确表明其参数的是一个引用②,它告诉编译器:通过改变该参数a的值,可以回传给入参变量n。①和②向编译器传递的信息存在明显的矛盾,因此编译不允许存在这样的代码。

    通过上面的分析,知道了矛盾的所在,既然无法通过改变a回传给入参变量n,那直接将传出特性禁用掉,修改代码后,下面代码将正确执行。

    void thread_run(const int& a) {
    }
    void thread_test() {
      int n = 3;
      std::thread my_thread{ thread_run, n };
    }
    

    如果我们真的需要在线程中实现引用传递该怎么做呢?在参数传递时,使用std::ref明确指定,我要传递的是引用

    void thread_run(int& a) {
      a = 2;
    }
    void thread_test() {
      int n = 3;
      std::thread my_thread{ thread_run, std::ref(n) }; //1
    }
    

    2.3 传递指针

    虽然指针也是复制到新的线程空间中,但是其复制的是内存地址。

    void thread_run(int* i) {
      *i = 2;
    }
    void thread_test() {
      int a = 1;
      std::thread t{ thread_run, &a };
      std::cout << "a = " << a << std::endl;
      t.join();
    }
    

    3.转移线程的所有权

    C++标准库中有很多资源占有类型,例如std::ifstream,std::unique_ptr,以及本篇中的std::thread,他们的对象不能够拷贝,但是可以移动。下面将展示一个例子,例子中创建了两个线程,并且在std::thread实例之间转移所有权。

    void thread_run1(int i);
    void thread_run2(int y);
    void thread_test() {
      int a = 10;
      std::thread t1{ thread_run1, a };
      std::thread t2 = std::move(t1);    // 1
      t1 = std::thread{ thread_run2, a }; // 2
      std::thread t3{};         //3
      t3 = std::move(t2);       //4
      t1 = std::move(t3);       //5 赋值操作将使程序崩溃
    }
    

    当显示使用std::move创建t2后①,t1关联的线程所有权就转移给了t2t1和线程执行已经没有关联了;执行thread_run1的线程现在与t2关联。

    然后创建了一个临时的std::thread对象②,启动了一个新线程,由于所有者是一个临时的对象,因此不需要显示的调用std::move(),移动操作将会隐式的调用。

    t3使用默认构造的方式进行构造,没有与任何执行线程关联③,调用std::move()将线程t2的所有权转移到t3中④,因为t2是一个命名对象,需要显示的调用std::move(),移动操作完成后,t1与执行thread_run2()的线程相关联,t3与执行thread_run1()的线程相关联。

    最后一个移动操作,将t3关联的线程所有权转移给t1,由于t1已经有了一个关联的线程,所以这里系统直接调用std::terminate(),终止程序继续运行。这样做是为了保证与std::thread的析构函数行为一致。之前说过,需要在线程对象被析构前,显示的等待线程执行完成,或者将其分离;进行赋值时也需要满足这些条件(说明不能通过赋一个新值给std::thread()对象的方式来丢弃一个线程)。

    std::thread支持移动操作,就意味着线程的所有权可以在函数外进行转移。

    std::thread getThread1() {
      void thread_run();
      return std::thread{ thread_run };
    }
    
    std::thread getThread2() {
      void thread_run(int a);
      std::thread t { thread_run, 1 };
      return t;
    }
    

    当线程所有权可以在函数内部转移,就允许std::thread实例可作为函数参数进行传递,代码如下:

    void thread_run();
    void trans_thread(std::thread t);
    void test4() {
      trans_thread(std::thread{ thread_run });
      std::thread t{ thread_run };
      trans_thread(std::move(t));
    }
    

    当线程所有权被转移走后,就不能再对该std::thread实例执行join()或者detach()操作,否则将引发运行时异常。

    4.获取线程并发数

    std::thread::hardware_concurrency()这个函数将 返回能同时并发在一个程序中的线程数量,例如,在多核系统中返回CPU线程的数量。返回值也仅仅是一个提示,当系统信息无法获取时,函数也会返回0。但是,这无法掩盖这个函数对启动线程数量的帮助。

    unsigned int num = std::thread::hardware_concurrency()
    

    5. 获取线程标识

    std::thread::id id = std::this_thread::get_id();
    

    相关文章

      网友评论

        本文标题:C++并发编程:线程管理

        本文链接:https://www.haomeiwen.com/subject/xqnphltx.html