第二章 线程管理
2.1 线程的基本操作
2.1.1 启动线程
-
每个程序至少有一个执行
main()
函数的线程,其他线程与主线程同时运行。 -
和主线程执行完
main()
会退出一样,其他线程执行完也会退出。-
简单的启动线程,就是构造
thread
对象 -
除了用函数进行构造,还可以用函数对象,也就是仿函数进行构造
-
#include <iostream>
#include <thread>
using namespace std;
void fun() {
cout << "fun" << endl;
}
class fun_2 {
public:
void operator()() const {
cout << "fun_2" << endl;
}
};
int main() {
//用函数来构造
thread my_thread_01(fun);
//也可以用函数对象来构造
fun_2 my_fun;
thread my_thread_02(my_fun);
}
- 但是用函数对象构造需要注意,不要加
()
,加上之后会变成“声明了一个名为my_thread的函数,这个函数带有一个参数(函数指针指向没有参数并返回 background_task对象的函数),返回一个 std::thread 对象的函数。”
thread my_thread_02(my_fun); //正确
thread my_thread_02(my_fun()); //错误
- 还可以使用lambda表达式
thread my_thread_03([]() {
cout << "lambda" << endl;
});
- 线程启动后,可以选择是等待线程执行结束,或者让其自主运行,也就是
汇入join
和分离detach
。- 让其自主运行的时候,需要小心访问数据的有效性
- 通常传递给线程的是值,让其复制到自己的线程中去,但是如果传递的是引用和指针,就需要谨慎考虑数据是否已经在主程序结束后被销毁。
#include <iostream>
#include <thread>
using namespace std;
struct func {
int& i;
func(int& i_) : i(i_) {}
void operator() () {
for (unsigned j = 0; j < 100; ++j) {
i++; // 1 潜在访问隐患:空引用
}
}
};
int main() {
int some_local_state = 0;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.detach(); // 2 不等待线程结束
} // 3 新线程可能还在运行
2.1.2 等待线程完成
- 使用
join()
等待线程完成 - 当你需要对等待中的线程有更灵活的控制时,比如:看一下某个线程是否结束,或者只等待一段时间(超过时 间就判定为超时)。想要做到这些,需要使用其他机制来完成,比如条件变量和
future
。 - 调用
join()
,还可以清 理了线程相关的内存,这样std::thread
对象将不再与已经完成的线程有任何关联 - 只能对一个线 程使用一次
join()
2.1.3 特殊情况下的等待
- 如果想要分离线程,可以在线程启动 后,直接使用
detach()
进行分离。 - 如果等待线程,则需要细心挑选使用
join()
的位置。
2.2 传递参数
- 为带参数的函数创建进程,函数的默认实参会被忽略
#include <thread>
void f(int i = 1) {}
int main() {
std::thread t(f);// 报错,因为f需要一个参数,并且默认的值会被忽略
std::thread t(f,42);//正确
}
- 参数的引用类型也会被忽略,因此需要使用
std::ref()
#include <iostream>
#include <thread>
void f(int& i) {
i++;
}
int main() {
int i = 0;
//报错,因为thread会忽略引用类型,会把i拷贝一个临时对象,作为右值传递给f(),但是f()是接受非常量引用,即左值的,编译器报错
//std::thread t(f, i);
std::thread t(f, std::ref(i));
t.join();
std::cout << i << std::endl;
}
- 如果对一个实例的
non-static
成员函数创建线程,第一个参数类型为成员函数指针,第二个参数类型为实例指针,后续参数为函数参数- 第一种传地址,则不会有构造函数的调用
- 第二种传对象,则会先以对象为参数进行拷贝构造,得到一个临时对象传入thread在进行构造一个新的A对象,也就是会调用两次构造函数,但是这里在VS中会优化掉右值构造的那次隐式构造。
#include <iostream>
#include <thread>
class A {
public:
int i;
A(int _i) : i(_i) {}
A(const A& other) {
i = other.i + 2;
std::cout << i << std::endl;
}
void fun(int x) {
std::cout << "i + x = " << i + x << std::endl;
}
};
int main() {
A a(42);
std::thread t_1(&A::fun, &a, 20);
std::thread t_2(&A::fun, a, 20);
t_1.join();
t_2.join();
}
- 如果要为参数是 move-only 类型的函数创建线程,则需要使用
std::move()
传入参数- 因为
thread
会构造一个参数类型的临时对象传入,如果参数只能移动构造的话,需要用move
把变量变为右值传入
- 因为
#include <iostream>
#include <thread>
void fun(std::unique_ptr<int> x) {
std::cout << *x << std::endl;
}
int main() {
std::unique_ptr<int> p = std::make_unique<int>(10);
std::thread t(fun, std::move(p));
t.join();
if (p == nullptr) std::cout << "p = nullptr" << std::endl;
}
2.3 转移所有权
thread
是move-only
类型,不能拷贝,只能通过移动转移所有权,但不能转移所有权到joinable == true
的线程,即如果还没执行完手上的线程,无法接受其他线程的所有权
#include <iostream>
#include <thread>
#include <cassert>
using namespace std;
void f() {}
void g() {}
int main() {
thread a(f);
thread b(move(a));
//断言 a不可汇入 b可汇入
//即a没有在运行 b还在运行
assert(!a.joinable());
assert(b.joinable());
a = std::thread{ g };
assert(a.joinable());
assert(b.joinable());
// a = std::move(b); 报错 因为a还没有执行完毕,joinable = true
a.join();
a = std::move(b);
assert(a.joinable());
assert(!b.joinable());
a.join();
}
- 移动操作同样适用于支持移动的容器
vector
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
int main() {
vector<thread> threads;
for (int i = 0; i < 10; i++) {
threads.emplace_back([] {});
}
for (auto i = threads.begin(); i != threads.end(); i++) {
i->join();
}
}
thread
可以作为函数返回值
#include <thread>
using namespace std;
thread f(){
return thread([]{});
}
int main() {
thread t(f());
t.join();
}
thread
可以作为函数参数,但是必须作为右值传入
#include <thread>
using namespace std;
void f(thread t){
t.join();
}
int main() {
thread t([]{}]);
f(move(t));
}
- 实现一个可以自动清理线程的类
scoped_thread
#include <thread>
#include <stdexcept>
using namespace std;
class scoped_thread {
private:
thread m_thread;
public:
//只能以右值进行构造
scoped_thread(thread _thread) : m_thread(move(_thread)) {
if (!_thread.joinable()) {
throw logic_error("no threads");
}
}
//拷贝赋值以及拷贝构造设置为 delete
scoped_thread(const scoped_thread& other) = delete;
scoped_thread& operator=(const scoped_thread& other) = delete;
//在析构函数中 回收线程
~scoped_thread() {
m_thread.join();
}
};
int main() {
//对象在释放之前会先保证线程执行完毕
scoped_thread thread(thread([] {}));
}
- C++17标准给出一个建议,就是添加一个
joining_thread
的类型,这个类型与std::thread
类似,不同是的添加 了析构函数,就类似于scoped_thread
。 - 委员会成员们对此并没有达成统一共识,所以这个类没有添加入 C++17标准中(C++20仍旧对这种方式进行探讨,不过名称为
std::jthread
)
#include <thread>
#include <iostream>
using namespace std;
class Jthread {
public:
Jthread() noexcept = default;
template <typename T, typename... Ts>
explicit Jthread(T&& f, Ts... args) noexcept
: _t(forward<T>(f), forward<Ts>(args)...) {}
explicit Jthread(thread x) noexcept : _t(move(x)) {}
Jthread(Jthread&& x) noexcept : _t(move(x._t)) {}
Jthread& operator=(Jthread&& x) noexcept {
if (joinable()) {
join();
}
_t = move(x._t);
return *this;
}
Jthread& operator=(thread x) noexcept {
if (joinable()) {
join();
}
_t = move(x);
return *this;
}
~Jthread() {
if (joinable()) {
join();
}
}
bool joinable() const noexcept { return _t.joinable(); }
void join() noexcept { _t.join(); }
void detach() noexcept { _t.detach(); }
void swap(Jthread&& x) noexcept { _t.swap(x._t); }
thread::id get_id() { return _t.get_id(); }
thread& as_thread() noexcept { return _t; }
const thread& as_thread() const noexcept { return _t; }
private:
thread _t;
};
void fun(int x, int y) noexcept {
cout << x + y << endl;
}
int main() {
Jthread t(fun, 1, 10);
}
2.4 查看硬件支持的线程数量
hardware_concurrency
可以返回硬件支持的并发线程数- 例如,多核系统中, 返回值可以是CPU核芯的数量。返回值也仅仅是一个标识,当无法获取时,函数返回0。
#include <thread>
#include <iostream>
using namespace std;
int main() {
int n = thread::hardware_concurrency();
cout << n << " concurrent threads are supported." << endl;
}
2.5 线程标识
- 线程标识为
std::thread::id
类型,可以通过两种方式进行检索。- 可以通过调用
std::thread
对象的成 员函数get_id()
来直接获取。如果std::thread
对象没有与任何执行线程相关联,get_id()
将返 回std::thread::type
默认构造值,这个值表示“无线程”。 - 当前线程中调 用
std::this_thread::get_id()
(这个函数定义在头文件中)也可以获得线程标识。
- 可以通过调用
#include <thread>
#include <iostream>
using namespace std;
thread::id master_thread_id = this_thread::get_id();
void fun() {
if (this_thread::get_id() == master_thread_id) {
cout << "master_thread" << endl;
}
else {
cout << "Its not a master thread: " << this_thread::get_id() << endl;
}
}
int main() {
thread t(fun);
t.join();
}