自学内容网 自学内容网

《C++并发编程实战》笔记(一、二)

一、简介

抽象损失:对于实现某个功能时,可以使用高级工具,也可以直接使用底层工具。这两种方式运行的开销差异称为抽象损失。

二、线程管控

2.1 线程的基本控制

1. 创建线程

线程相关的管理函数和类在头文件:

#include <thread>

创建一个线程使用如下方法:

std::thread t(callable);
  • callable:线程函数,可以是任意的可调用对象
  • 线程对象创建后会立即启动线程运行

2. 控制线程的结束

线程启动后,必须显式指定线程结束的方式:

阻塞等待其结束(汇合),使用如下方法:

t.join();
  • 只要调用了join(),主线程会阻塞等待该线程执行结束,join()执行结束后,隶属于该线程的任何存储空间都会被清除,且线程对象不再关联到结束的线程
  • 成员函数t.joinable()返回线程对象t是否关联到某个线程,当join()执行结束后t.joinable()会返回false
  • 只有关联到某个线程的对象才能调用join()

让线程后台运行(分离),使用如下方法:

t.detach();
  • 线程对象必须关联某个线程(joinable()true时)才能调用detach,调用后线程的归属权和控制权都转移给C++运行时库,它独立于主线程继续运行,直至线程函数运行结束,且C++运行时库会保证线程退出后与之关联的资源都被正确回收
  • detach的线程不再关联实际的执行线程(joinable()会返回false),所以不可再调用join()

注意:如果线程对象销毁时都没有指定结束方式,则std::thread的析构函数会调用std::terminate()终止整个程序

3. 异常时保证线程正常结束

创建线程对象后,如果在指定线程结束方式前,因为执行其他代码造成了异常,可能会导致指定线程结束方式的代码被略过,从而导致程序终止。即:

void func() {
  try {
    // 线程 t 去执行 do_something() 函数
    std::thread t(do_something);
    // 此时如果发生异常等,下面的join可能无法被调用,退出func时程序会被中断
    do_other_thing();
  } catch (...) {
    throw;
  }
  t.join();
  return;
}

解决方法:

  1. 一种解决方法是保证在程序的所有执行路径都会指定线程结束方式
    void func() {
      try {
        std::thread t(do_something);
        do_other_thing();
        
      } catch (...) {
        // 保证一定会指定线程结束方式
        t.join();
        throw;
      }
      t.join();
      return;
    }
    
  2. 使用RAII方法,利用对象管理线程,在构造函数中创建线程,在析构函数中指定线程的结束方式
    class RaiiThreadGuard {
    
    public:
        explicit RaiiThreadGuard(std::thread &t) : t_(t) {} 
    
        ~RaiiThreadGuard() {
            // 类对象离开作用域时一定会调用析构,保证一定会指定线程对象的结束方式
            t_.join();
        }
    
        // 将拷贝构造和拷贝赋值都定义为删除的,避免编译器优化造成重复调用析构
        RaiiThreadGuard(const RaiiThreadGuard &) = delete;
        RaiiThreadGuard& operator=(const RaiiThreadGuard &) = delete;
    
    private:
        std::thread& t_;
    };
    
    // main 函数中通过如下方式使用
    int main() {
    
        // 创建线程
        std::thread t(HelloFunction);
        // 使用 RAII 保证线程对象一定会调用 join
        RaiiThreadGuard thread_guard(t);
    
        // 即使后续再执行其他代码造成退出作用域,编译器会保证执行 thread_guard 的析构指定线程的结束方式
        return 0;
    }
    
    

2.2 向线程函数传递参数

线程函数所需要的参数可以直接紧跟在std::thread的线程函数实参后:

  • 线程具有内部存储空间,线程函数的实参会先使用拷贝构造函数,将std::thread的实参复制到创建的线程;在创建好的线程中,新复制出来的实参被当成临时量,以右值形式传递给新线程中的线程函数
  • 根据参数的传递过程,如果线程函数包含非const引用形参,为避免在线程内执行时收到右值,需要通过std::refstd::thread传递实参(与std::bind函数的使用相同)
  • 类的非静态成员函数第一个参数是指向对象的隐式this指针,如果想在线程中执行某个成员函数,需要将对象地址传递给成员函数的隐式this指针
class TestClass {
public:
    /** @brief  默认构造函数 */
    TestClass(){
        std::cout << "TestClass default constructor." << std::endl;
        std::cout << "std::thread::id: " << std::this_thread::get_id() << std::endl;
    }

    /** @brief  拷贝构造函数 */
    TestClass(const TestClass& ohter) {
        std::cout << "TestClass copy constructor." << std::endl;
        std::cout << "std::thread::id: " << std::this_thread::get_id() << std::endl;
    }

    /** @brief  移动构造函数 */
    TestClass(TestClass&& ohter) {
        std::cout << "TestClass move constructor." << std::endl;
        std::cout << "std::thread::id: " << std::this_thread::get_id() << std::endl;
    }
    
    /** @brief  类的内部函数 */
    void InnerFunction() {
        std::cout << "TestClass Inner Function." << std::endl;
        std::cout << "std::thread::id: " << std::this_thread::get_id() << std::endl;
    }
};

void func(int num, std::string &str, TestClass obj) {
    str += std::to_string(num);
}

// main 函数
std::string str("The num: ");
TestClass test_class;
// 参数直接作为 std::thread 的后续参数传入
// 1. 对象会先调用拷贝构造复制到线程,再通过std::move()以右值复制给线程内的函数形参
// 2. 线程函数的引用形参要通过 std::ref() 传递
std::thread t(func, 3, std::ref(str), test_class);
t.join();
std::cout << str << std::endl;

// 在线程中运行类的成员函数,需要将对象的地址传递给成员函数的隐式this指针
std::thread t2(&TestClass::InnerFunction, &test_class);
t2.join();

/* 输出
TestClass default constructor.
std::thread::id: 140737348195264
TestClass copy constructor.
std::thread::id: 140737348195264
TestClass move constructor.
std::thread::id: 140737348179520
The num: 3
TestClass Inner Function.
std::thread::id: 140737348179520
*/

2.3 转移线程归属权

在某些情况下,如想要指定特定的函数等待线程结束,可能需要转移线程的归属权。

std::threadstd::unique类似,由于独占资源,其对象不能被复制,只支持移动

std::thread t1(some_function);
std::thread t2 = std::move(t1);

2.4 运行时确定线程数量

标准库的静态函数std::thread::hardware_concurrency()函数返回程序在执行时可以真正并发的线程数量:

  • 若信息无法获取,返回0
  • 否则返回支持并发的线程数

2.5 标识不同线程

每个线程都有一个唯一的ID,该ID是一个std::thread::id类型的变量:

  • 可以使用线程对象的std::thread::get_id()函数返回线程对象的ID
  • 在程序中可以使用std::this_thread::get_id()函数获取运行当前程序的线程ID
  • 默认构造创建的std::thread::id类型变量表示线程不存在

std::thread::joinable()函数会利用线程对象的ID确定返回值,即:

  • this.get_id() != std::thread::id() 则返回 true(判断当前线程ID和默认构造的线程ID类型变量是否相同)
  • 否则返回false(表示线程对象没有关联到任何线程,线程不存在)

原文地址:https://blog.csdn.net/qq_36793268/article/details/140388009

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!