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