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