我在写一个多线程的网络服务器,想在主线程之外创建一些线程执行特定的逻辑。我还想把创建线程的代码、线程执行的逻辑、线程自己拥有的或者和主线程共享的资源都封装在一个类里。在具体实现时,我遇到了语言层面的问题。
题外话:把线程相关的逻辑和资源封装在一个类里是个不错的设计,这样就把线程可能访问的资源和只有主线程会访问的资源在代码上清晰的区分开了。还可以把主线程访问线程资源的操作封装在类的方法里,这样访问线程资源的操作就也和访问一般资源的操作区分开了。
以下是我用于封装线程的类的声明
cpp
class worker {
public:
worker();
void add_client(int fd_sock);
private:
void run();
private:
std::thread m_thread;
int m_id;
};
我遇到的问题是 1. 如何初始化 m_thread。2. 如何在 m_thread 里跑 run(),而且是 m_thread 所在的那个对象的 run(),不是其他对象的 run(),如何用 cpp 表达这种逻辑。
关于初始化 m_thread
网上的资料都会交你怎么初始化 thread,并在 thread 里跑一些函数,大概是这样的
cpp
std::thread t2(f1, n + 1); // pass by value
但我现在遇到的情况是,定义 m_thread 和初始化 m_thread 是分开的,上述调用构造函数的语法就没法套用了。其实不仅仅是初始化 thread,初始化其他的作为类的成员存在的类对象都会有这个问题:在类声明里定义了这个成员对象,却只能在类的构造函数里初始化它。
这里就要用到调用构造函数的另一种语法:类名()
比如上述场景下,我可以在 worker 的构造函数里这么写(... 中的内容稍后揭秘)。
cpp
worker::worker() {
m_thread = std::thread(...);
}
这里其实是构造了一个匿名的临时变量,再移动或者复制赋值给 m_thread。这种构造匿名临时对象的方式也适用于构造参数或者其他不想起名字的情况。
优化
上述方法是正确的,但先构造一个临时对象,再赋值给我们真正想初始化的对象岂不是很麻烦?所以 C++ 专门为这种问题提供了一种语法:成员初始化列表。
cpp
worker::worker() : m_thread(...) {
...
}
这样就可以"直接初始化" m_thread,与 m_thread 作为普通变量时类似。
补充
我在不熟悉 C++ 时经常写出两种糊涂的初始化的代码。
cpp
// 糊涂写法,这个语法其实是函数声明
A a(); // 改正为 A a;
// 糊涂写法,正确但是多此一举
A a = A(xxx); // 改正为 A a(xxx);
关于如何在 m_thread 里跑特定对象的 run
这里要提到一个概念叫成员函数指针。(这个概念我居然今天才知道)
成员函数指针的类型是这样的
cpp
// 返回值类型 (类名::*) (参数列表)
// 一般的函数指针是没有 "类名::" 这一部分的
// 以 run 为例
void (worker::*) ()
// 对于一般的函数指针,使用方式如下
f(args)
// 对于成员函数指针,使用时需要给出调用他的对象
// 同时也指明了这个成员函数是在哪个对象上下文中执行的
A.*f(args)
A_ptr->*f(args)
获取成员函数指针的方式很固定:&类名::成员函数名,比如 &worker::run
回到本篇设定的场景,thread 的构造函数和成员函数指针类型具有独特的配合,cppreference 中就给出了例子。其中 &foo::bar 是成员函数指针,&f 是用于调用成员函数的对象。此时 &f 不再是前面函数的参数,在 thread 里会被以 (&f)->*bar 这种方式使用。
cpp
std::thread t5(&foo::bar, &f);
利用 thread 构造函数的这种特性,我可以在 worker 的构造函数里这么写,使得构造出来的 m_thread 运行的是当前 worker 的 run 函数。下面的代码中还给出了其他不同写法(包括有关 lambda 而无关成员函数指针的特殊写法,不再赘述)
cpp
// 1. 成员初始化列表的初始化方式
worker::worker() : m_thread(&worker::run, this) {
static int id = 0;
m_id = ++id;
// 2. 构造临时匿名对象再赋值
// 这种写法是正确的,&worker::run 这种语法【&类名::方法名】取出成员函数指针
// thread 构造函数能判断第一个参数是成员函数指针,此时第二个参数作为调用成员函数的对象,而非一般的函数参数
// m_thread = std::thread(&worker::run, this);
// 3.
// 这种写法也是正确的,绕开了成员函数指针的问题
// m_thread = std::thread([this] () {
// this->run();
// });
// 4.
// 这种写法也是正确的,怎么知道 run 是成员函数的?
// 捕获 this 的 lambda 比较特殊,捕获了 this 的 lambda 内部就仿佛类的一般的成员函数内部一样
// 可以直接访问类的(包括私有的)成员和函数
// m_thread = std::thread([this] () {
// run();
// });
}