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

1. 线程启动

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

1.1 普通函数作为线程入口

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

//带有参数的普通函数,作为线程入口函数
void thread_run12(int i, std::string msg);
std::thread my_thread12{ thread_run12, 3, "msg"};

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

class background_task {
public:
    void thread_run(int i) {
        std::cout << __FUNCTION__< (1)
  
  //内部会拷贝构造出一个新的task对象,然后获取地址,传递给this
  std::thread t2{&background_task::thread_run, task, 3};//------> (2)
  
  //这里告诉thread传递的是一个引用,如果明确传递的是引用,其实内部和取地址是一样的同(1)
  std::thread t3{&background_task::thread_run, std::ref(task), 3}; 
}

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

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

class background_task {
public:
    void operator()() const {
        std::cout << __FUNCTION__ << std::endl;
    }
}

void thread_test() {
    backgourn_task task{};
  
  //使用可调用对象和使用类对象原理差不多,不需要传递入口函数,默认的入口函数时operator()()
  //这里的task依然会复制到新的线程空间中执行
    std::thread my_thread{task};
  std::thread my_thread{&task}; //错误,因为被认为传递的是一个整数。
  
  //如果想要当前的类对象传递进线程空间可以使用引用
  std::thread my_thread{std::ref(task)};//正确
  
}

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

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

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

 std::thread t2{[]()->void {
        
 }};

启动了线程,你还需要明确的是:要等待线程执行结束,还是让其自主运行,如果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(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

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

通过上面的分析,知道了矛盾的所在,既然无法通过改变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();
}

2.4 传递类对象

class A {
    int a = 0;
public:
    A(int a_) : a(a_) {
         std::cout << "构造函数:" << this << "threadId =" << std::this_thread::get_id()<< std::endl;
    }

    A(const A &other) {
        std::cout << "拷贝构造函数:this =" << this << " other =" << &other << std::endl;
    }

    ~A() {
        std::cout << "析构函数:" << this << std::endl;
    }

    int get() const {
        return a;
    }
};

void func(int i, const A &a) {
     std::cout << "func 子线程Id:"<

从这里我们可以看出,子线程的创建过程,仅仅是从一个线程中需要的函数入口地址,函数参数等需要的条件复制到另一个线程空间中,然后启动该线程,剩下的事情就完全由子线程自己负责完成。在上述代码中,func需要一个A的类对象实例,而在线程入口处,传递的是一个整数,当b复制到新的线程空间后,调用匿名转换,构造出对象A

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(1)t1关联的线程的所有权就转移给了t2t1和线程执行已经没有关联了;执行thread_run1的线程现在与t2关联。

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

t3使用默认构造的方式进行构造,没有与任何执行线程关联(3),调用std::move()将线程t2的所有权转移到t3(4),因为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++并发编程:线程管理)