期盼好久的书到了,听说这本书重新翻译后质量不错,花了 69 支持的正版,结果印刷的还不如影印版…有点失望。不过一想到能学到东西,就又开心了回来,hhh
这本书初看下对读者有一定要求,起码您要提前学过多线程,多基本概念有所涉及因为其不会解释的很清楚,并且对 C++ 语法要求较高,书中会用到 万能转发 和 简单的可变参模板 等概念
即介绍最基本的线程API,C++11后的线程被从语义层面支持了,这是一大好事。不用背 pthread 那套冗长的API 就可以方便地使用线程了。不过还是更喜欢 go 的协程,不知道 C++20 的协程怎么样。
发起线程
std::thread(func);
这样这个线程就启动了,相当简单
这里 func 是可调用(callable)类型,如 lambda 或 重载了的类、函数指针。见下
class func {
public:
int &i;
explicit func(int& i_) : i(i_) {}
void operator() () const {
int sum = 0;
for (int j = 0; j < i; ++j)
sum += j;
std::cout << sum << std::endl;
}
};
线程初始化,往往因函数声明造成二义性。
如:std::thread g(func())
这还有可能是以 g 为函数名,std::thread 为返回对象,func() 为参数的函数。
所以初始化线程,请这样:
std::thread g(( func() )) 或 std::thread g{func()}
在使用线程时应该格外注意生存期的问题。如上面的 func 中的成员是一个引用
std::thread build() {
// wrong !!! j is a local object
int j = 100;
std::thread t3{func(j)};
return t3;
}
这里最后 j 显然已经被释放了,但 t3 线程拿取的引用将是未定义的。
线程的完成
调用 join() 或 detach()
这引出一个问题,如果线程启动后抛出异常,则 join 将被略过。所以我们只能 try catch 捕获异常处理。然而,这样的话就有些复杂了,并且作用于域更加凌乱。
我们自然想到使用 RAII 来管理线程。这是一个无比常见的方法,在某个地方如果您担心某人会忘了干什么,您应该将其交给系统来管理。下图的 thread_guard 类便是这样一个类。
就算发生异常,C++也保证其一定会调用析构函数,从而正确 JOIN。
#include
#include
class func {
public:
int &i;
explicit func(int& i_) : i(i_) {}
void operator() () const {
int sum = 0;
for (int j = 0; j < i; ++j)
sum += j;
std::cout << sum << std::endl;
}
};
// RAII
class thread_guard {
std::thread &t;
public:
explicit thread_guard(std::thread &t_) : t(t_) {}
thread_guard(const std::thread &) = delete;
thread_guard& operator=(const std::thread &) = delete;
~thread_guard() {
// check whether the thread is joinable
if (t.joinable())
t.join();
}
};
std::thread build(int &j) {
// wrong !!! j is a local object
// int j = 100;
std::thread t3{func(j)};
return t3;
}
int main() {
int i = 10;
func myfunc(i);
// must be initialized by {} or (())
std::thread t1{myfunc};
std::thread t2 = std::move(t1);
// return value must be a rvalue
int j = 100;
std::thread t4 = build(j);
thread_guard g1(t2);
thread_guard g2(t4);
}
如上图,我们可以看到
直接在构造函数后面添加参数即可。但
参数会被复制到线程的内部存储空间,这些参数被当做右值传递
void f(std::string &a);
// 错!传入的是指针
std::thread t{f, "hello"};
// 对
std::thread t{f, std::string("hello")};
std::string temp = "hello";
//错,temp被当做右值传递,而非const左值引用不能绑定右值
std::thread t{f, temp};
//对
std::thread t{f, std::ref(temp)};
线程是 可移对象,其所有权可以灵活通过移动进行转换。
基于此,我们可以写出一个 scoped_thread 类,其可以直接构造线程,并管理线程。
#include
#include
class func {
public:
int &i;
explicit func(int& i_) : i(i_) {}
void operator() () const {
int sum = 0;
for (int j = 0; j < i; ++j)
sum += j;
std::cout << sum << std::endl;
}
};
// RAII
class scoped_thread {
std::thread t;
public:
template<typename Callable, typename ...Args>
explicit scoped_thread(Callable &&func, Args&&... args)
: t(std::forward<Callable>(func), std::forward<Args>(args)...) {}
explicit scoped_thread(std::thread &&t_) : t(std::move(t_)) {
if (!t.joinable())
throw std::logic_error("No thread");
}
scoped_thread(const scoped_thread &) = delete;
scoped_thread& operator=(const scoped_thread &) = delete;
~scoped_thread() {
t.join();
}
};
std::thread build(int &j) {
// wrong !!! j is a local object
// int j = 100;
std::thread t3{func(j)};
return t3;
}
int main() {
int i = 10;
func myfunc(i);
// return value must be a rvalue
int j = 100;
std::thread t4 = build(j);
scoped_thread g1(std::thread {myfunc});
scoped_thread g2(std::move(t4));
int k = 1000;
func myfunc1(k);
scoped_thread g3(myfunc1);
}
我的实现还添加了直接构造的构造函数,用转发进行构造。
C++ 20 引入 jthread
给线程的析构函数自带了 join,这个类类似上面的scoped_thread
。
我们还可以将多个线程放入 vector 管理
void do_word(unsigned id);
void f () {
std::vector<std::thread> threads;
for (unsigned i = 0; i < 20; ++i)
threads.emplace_back(do_work, i);
for (auto &entry : threads)
entry.join();
}
线程ID 类型:std::thread::id
调用 get_id()获取,这类 ID 是全序的。
#include
#include
class func {
public:
int &i;
explicit func(int& i_) : i(i_) {}
void operator() () const {
int sum = 0;
for (int j = 0; j < i; ++j)
sum += j;
std::cout << sum << std::endl;
}
};
// RAII
class scoped_thread {
std::thread t;
public:
template<typename Callable, typename ...Args>
explicit scoped_thread(Callable &&func, Args&&... args)
: t(std::forward<Callable>(func), std::forward<Args>(args)...) {
std::cout << "thread ID : #" << t.get_id() << std::endl;
}
explicit scoped_thread(std::thread &&t_) : t(std::move(t_)) {
if (!t.joinable())
throw std::logic_error("No thread");
std::cout << "thread ID : #" << t.get_id() << std::endl;
}
scoped_thread(const scoped_thread &) = delete;
scoped_thread& operator=(const scoped_thread &) = delete;
~scoped_thread() {
t.join();
}
};
int main() {
int i = 10;
func myfunc(i);
scoped_thread g1(std::thread {myfunc});
int k = 1000;
func myfunc1(k);
scoped_thread g2(myfunc1);
}
学完这一章简单的操作都掌握了,我们可以编写简单的资源管理类来保证线程正常完成。我们完全不能保证线程的顺序,这也导致考虑锁的时候将会很头大,hhh,不过这是后话了。