muduo网络库采用了基于对象的编程思想来封装线程类。
类图如下:
变量numCreated_表示创建的线程个数,类型为AtomicInt32,用到了我们上篇所说的原子性操作。
Thread类中还用到了CurrentThread类。
代码要点如下:
(1)线程标识符
Linux中,每个进程有一个pid,类型为pid_t,由getpid()取得。Linux下的POSIX线程也有一个id,类型为pthread_t,由pthread_self()取得,该id由线程库维护,其id空间是各个进程独立的(即不同进程中的线程可能有相同的id)。Linux中的POSIX线程库实现的线程其实也是一个进程(LWP:轻量级进程),只是该进程与主进程(启动线程的进程)共享一些资源而已,比如代码段,数据段等。
有时候我们可能需要知道线程的真实pid。比如进程P1要向另外一个进程P2中的某个线程发送信号时,既不能使用P2的pid,更不能使用线程的pthread id,而只能使用该线程的真实pid,称为tid。
函数gettid()可以得到tid,但glibc并没有实现该函数,只能通过Linux的系统调用syscall来获取。
return syscall(SYS_gettid)
因为使用系统调用开销很大,所以我们需要对所获取的tid做一个缓存,防止每次都使用系统调用,从而提高获取tid的效率。
代码片段:缓存tid
文件名:CurrentThread.h
......
extern __thread int t_cachedTid; // 线程真实pid(tid)的缓存
......
inline int tid()
{
if (t_cachedTid == 0)
{
cacheTid();
}
return t_cachedTid;
}
代码片段:cacheTid()
文件名:Thread.cc
void CurrentThread::cacheTid()
{
if (t_cachedTid == 0)
{
t_cachedTid = detail::gettid();
int n = snprintf(t_tidString, sizeof t_tidString, "%5d ", t_cachedTid);
// (void) n; 的用法是为了防止未使用变量n而出现编译错误
assert(n == 6); (void) n;
}
}
(2)__thread关键字和POD类型
__thread是GCC内置的线程局部存储设施,存取效率可以和全局变量相比。__thread变量在每一个线程有一份独立实体,各个线程的值互不干扰。可以用来修饰那些带有全局性且值可能变,但是又不值得用全局变量保护的变量。用一个例子来理解它的用法。
#include
#include
#include
using namespace std;
//__thread int var = 5;
int var = 5;
void *worker1(void* arg);
void *worker2(void* arg);
int main()
{
pthread_t p1, p2;
pthread_create(&p1, NULL, worker1, NULL);
pthread_create(&p2, NULL, worker2, NULL);
pthread_join(p1, NULL);
pthread_join(p2, NULL);
return 0;
}
void *worker1(void* arg)
{
cout << ++var << endl;
}
void *worker2(void* arg)
{
cout << ++var << endl;
}
/**
* 使用__thread关键字,输出为:
* 6
* 6
*
* 不使用__thread关键字,输出为:
* 6
* 7
* /
**注:**__thread只能修饰POD类型,不能修饰class类型,因为无法自动调用构造函数和析构函数。
POD类型(plain old data)是指与C兼容的原始数据类型,例如,结构体和整型等C语言中的类型就是 POD 类型,但带有用户定义的构造函数或虚函数的类则不是:
__thread可以用于修饰全局变量、函数内的静态变量,但是不能用于修饰函数的局部变量或者class的普通成员变量。
另外,__thread变量的初始化只能用编译器常量。
__thread string t_obj1(“hello”); // 错误,不能调用对象的构造函数
__thread string* t_obj2 = new string; // 错误,初始化必须用编译期常量
__thread string* t_obj3 = NULL; // 正确,但是需要手工初始化并销毁对象
(3)pthread_atfork()函数
#include
int pthread_atfork(void (*prepare)(void),
void (*parent)(void),
void (*child)(void));
用法:调用fork时,内部创建子进程前在父进程中会调用prepare,内部创建子进程成功后,父进程会调用parent ,子进程会调用child。
(4)多线程与fork()
对于编写多线程程序来说,最好不要再调用fork(),即不要编写多线程多进程程序。因为Linux的fork()只克隆当前线程的thread of control ,不克隆其他线程。fork()之后,除了当前线程之外,其他线程都消失了,也就是说,不能一下子fork()出一个和父进程一样的多线程子进程。
fork()之后子进程中只有一个线程,其他线程都消失了,这就造成一个危险的局面。其他线程可能正好位于临界区之内,持有了某个锁,而它突然死亡,再也没有机会去解锁了。如果子进程试图再对同一个mutex加锁,就会立刻死锁。
一个在多线程程序里fork造成死锁的例子:
/*
死锁的原因:
1. 线程里的doit()先执行
2. doit执行的时候会给互斥量mutex加锁
3. mutex的内容会原样拷贝到fork出来的子进程中(在此之前,mutex变量的内容已经被线程改写成锁定状态)
4. 子进程再次调用doit的时候,在给互斥量mutex加锁的时候会发现它已经被加锁,所以就一直等待,直到拥有该互斥体的进程释放它(实际上没有人拥有这个mutex锁)
5. 线程的doit执行完成之前会把自己的mutex释放,但这时的mutex和子进程里的mutex已经是两份内存.所以即使释放了mutex锁也不会对子进程里的mutex造成什么影响
*/
#include
#include
#include
#include
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* doit(void* arg)
{
printf("%d begin doit\n",static_cast<int>(getpid()));
pthread_mutex_lock(&mutex);
struct timespec ts = {2, 0};
nanosleep(&ts, NULL);
pthread_mutex_unlock(&mutex);
printf("%d end doit\n",static_cast<int>(getpid()));
return NULL;
}
int main(void)
{
printf("%d enter main\n", static_cast<int>(getpid()));
pthread_t tid;
pthread_create(&tid, NULL, doit, NULL);
struct timespec ts = {1, 0};
nanosleep(&ts, NULL);
if (fork() == 0)
{
doit(NULL);
}
pthread_join(tid, NULL);
printf("%d exit main\n",static_cast<int>(getpid()));
return 0;
}