Muduo库之CurrentThread、Types

CurrentThread

CurrentThread 命名空间中实现了有关线程 id 的管理和优化。其主要用于获取当前线程的 id,并将线程 id 保存为 C 语言风格的字符串:

extern __thread int t_cachedTid;		// 线程id
extern __thread char t_tidString[32];		// 线程id的字符串形式
extern __thread int t_tidStringLength;		// tidString的长度
extern __thread const char* t_threadName;	// 线程名称

在如上代码中,我们可以看到,变量均使用 __thread 进行修饰,它是 GCC 内置的线程局部存储设施。由该关键字进行修饰的变量在每一个线程中都具有一份独立实体,各个线程的值互不干扰。它通常用来修饰那些带有全局性且值可能变,但是又不值得用全局变量保护的变量。

简单来说,就是用该修饰符修饰的变量在每个线程中都存在,但是各个线程之间又互不干扰。

这里使用 __thread 修饰符缓存了线程id、字符串用于日志中,避免可能每次输出 log 时都需要通过系统调用获取一次线程id,然后再转化为字符串的操作,从而提高了效率。

inline int tid() {
  if (__builtin_expect(t_cachedTid == 0, 0)) { // t_cachedTid==0 大概率为假
    cacheTid();
  }
  return t_cachedTid;
}

函数 tid() 的目的就是为了获取线程id,如果是第一次获取,则将其缓存到 t_cachedTid 变量中,后续获取则直接返回该缓存值。其中的 __builtin_except 关键字是 GCC 引入的,作用是向编译器提供分支预测信息,从而帮助编译器进行代码优化。即该关键字所表示的条件会满足的可能性较大,比不使用该关键字直接进行真假判断的效率要高。其格式为:

__builtin_except(EXP, N) // 表明 EXP == N 的概率很大

其中的 分支预测 是说高级编程语言在编译成汇编指令后,由于 CPU 进行流水线式的执行,汇编过程中将多个条件判断分支按需进行优化,最近的条件语句执行效率更高,其他的需要进行 jmp 跳转。由于此处的 t_cachedTid 变量只在初始化时为0,在第一次获取线程id后该变量便不再为0,因此使用分支预测来表明 t_cachedTid 大概率不为0,以提高效率。

代码中的 cacheTid() 则定义于 Thread.cc 文件中:

pid_t gettid() {
  return static_cast<pid_t>(::syscall(SYS_gettid));
}

void CurrentThread::cacheTid() {
  if (t_cachedTid == 0) {
    t_cachedTid = detail::gettid();
    t_tidStringLength = snprintf(t_tidString, sizeof(t_tidString), "%5d ", t_cachedTid);
  }
}

其内部通过系统调用 SYS_gettid 来获取线程的tid,然后通过 snprintf() 将其格式化为 C 风格的字符串存储到 t_tidString 变量值以供使用。

在 Linux 下每个进程都有一个进程id,类型为 pid_t,通过函数 getpid() 获取。而 POSIX 线程也有线程id,类型为 pthread_t,通过函数 pthread_self() 获取,该id由线程库维护。

但是不同进程之间相互独立,因此在不同的线程中,可能存在线程id相同的情况,这就难以在不同进程的两个线程之间进行通信。为此,可以通过系统调用 syscall(SYS_gettid) 来获取真实的线程id唯一标识。

除此之外,不使用 pthread_t 的原因还有:

  • 不同的平台之间其实现不同,难以跨平台;
  • 难以比较大小,不能作为关联容器的 Key;
  • 无法定义无效值来表示一个不存在的线程;
  • 线程销毁后,很可能再次创建相同 id 的线程;

而使用 pid_t 的优点如下:

  • 所占字节大小要小于 pthread_t;
  • 任何时候全局唯一,且短时间内反复创建多个线程不会出现相同id;
  • 0为非法值,init 进程的 id 是 1;
  • 在主线程中,getpid()gettid() 返回值相同,则可用于确认当前线程是否为主线程。

而在 isMainThread() 函数的实现中,便是使用了上述方法,其实现在 Thread.cc 文件中:

bool CurrentThread::isMainThread() {
  return tid() == ::getpid();
}

Types

Types.h 中定义了一个内存清零的函数 memZero(),其内部调用的是 memset() 函数:

inline void memZero(void* p, size_t n) {
  memset(p, 0, n);
}

除此之外,还包括两个转换函数:implicit_cast<T>down_cast<T>

其中,implicit_cast<T>static_cast<T>const_cast<T> 的安全版本,用于在类型层次结构中向上转换,即将指向子类的指针转换为指向父类的指针,或者将指向对象的非常量指针转换为常量指针。而 static_cast<T> 则是同时支持向上转型和向下转型。

考虑如下继承关系:

class Parent {};
class ChildA : public Parent {};
class ChildB : public Parent {};
class Grandson : public ChildA, ChildB {};

然后:

void func(ChildA& obj);
void func(ChildB& obj);
void test(Grandson& arg) {
  func(arg);
}

int main() {
  Grandson son;
  test(son);
  return 0;
}

由于函数 func() 的重载解析出现歧义,所以编译不会通过,这时候我们使用 static_cast 指定要使用的重载函数:

void test(Grandson& arg) {
  func(static_cast<ChildA&>(arg));
}

这个时候编译通过,参数 arg 被 static_cast 向上转型为 ChildA 类型。

在过了一段时间之后,我将代码修改为如下样式:

void test(Parent& arg) {
  func(static_cast<ChildA&>(arg));
}

int main() {
  Parent pa;
  test(pa);
  return 0;
}

此时的代码编译可以通过,参数 arg 被 static_cast 向下转型为 ChildA 类型。

但是代码运行时就会出现问题(在函数中访问了对象并不存在的子类成员变量),为此为了摒弃 static_cast 这种强大的向上向下转型均可的机制,特地设计出 implicit_cast 以应对那些只需向上转型的场合。

同理,down_cast 则只适用于向下转型的时机。而且在代码中,如果是 Debug 模式下,则使用 dynamic_cast 来仔细检查向下转型是否合法,如果是非 Debug 模式,则使用高效的 static_cast。其中的:

if (false) {
  implicit_cast<From*, To>(0);
}

仅仅只是在编译期进行类型检查,判断 To 是否是 Form* 的子类型,是否满足向下转型条件。

posted @ 2022-10-14 13:44  Leaos  阅读(98)  评论(0)    收藏  举报