思维方式转变两点:
- 当前线程随时可能会被切换出去,或者被抢占。
- 多线程程序中事件的发生顺序不再有全局统一的先后关系。
基本线程原语的选用
Linux 平台 11 个最基本的 Pthreads 函数:
2个:线程的创建和等待结束(join)。封装为 muduo::Thread 4个:mutex 的创建、销毁、加锁、解锁。封装为 muduo::MutexLock 5个:条件变量的创建、销毁、等待、通知、广播。封装为 muduo::Condition
可以酌情使用的:
- pthread_once. 封装为 muduo::Singleton
。其实不如直接用全局变量 - pthread_key* ,封装为 muduo::ThreadLocal
。可以考虑用 __thread 替换之。
不建议使用的:
- pthread_rwlock, 读写锁通常应慎用。
- sem_*, 避免使用信号量(semaphore)。它的功能与条件变量重合,但容易出错。
- pthread_{cancel,kill}。程序出现他们,通常意味设计出了问题
C/C++ 系统库的线程安全性
对于标准而言,关键的不是定义线程库,而是规定内存模型(memory model)。特别是规定一个线程对某个共享变量的修改何时能被其他线程看到,这称为内存序(memory ordering)或者内存能见度(memory visibility)。
C/C++ 标准库中绝大多数泛型算法是线程安全的。
Linux 上的线程标识
pthread_t 不能作为线程标识,同一个进程先后不同的线程 pthread_t 可能相同
建议使用 gettid(2) 系统调用的返回值作为线程 id。好处:
- 类型 pid_t, 通常是小整数,便于日志输出。最大值是 /proc/sys/kernel/pid_max 默认 32768
- 直接标识内核的任务调度 id,因此 /proc 文件系统中可以找到对应项。 /proc/tid 或 /proc/pid/task/tid.
- 使用其他工具 top 等也能查看
- 任何时刻全局唯一。Linux 分配新 pid 采用递增轮回办法,短时间内启动的多个线程也会具有不同的线程 id
- 0 是非法值,因此操作系统的第一个进程 init 的 pid 是 1
线程数目可以从 /proc/pid/status 拿到
gettid(2) 为系统调用,每次都系统调用比较浪费, muduo::CurrentThread::tid() 采用 __thread 变量缓存,只有在线程第一次调用的时候才会系统调用。以后直接从 thread local 缓存线程 id 拿结果(可参考 glib)。
fork(2) 子进程看到 stale 缓存,通过 pthread_atfork() 注册一个回调,情况缓存id
线程的创建与销毁守则
- 程序库不应该在未提取告知的情况下创建自己的 “背景线程”
- 尽量用相同的方式创建线程
- 进入 main 函数之前不应该启动线程
- 程序中线程的创建最好能在初始化阶段全部完成。
C++ 保证在进入 main() 函数之前完成全局对象(包括 namespace 级全局对象、文件级静态对象、class 的静态对象,但不包括函数内的静态对象)的构造
线程的销毁方式:
- 自然死亡:从线程主函数返回,线程正常退出。
- 非自然死亡:从线程主函数抛出异常或线程触发 segfault 信号等非法操作
- 自杀:在线程中调用 pthread_exit() 来立刻退出线程
- 他杀:其他线程调用 pthread_cancel() 来强制终止某个线程
pthread_cancel 与 C++
pthread_cancel 意思是线程执行到这里有可能被终止
在 C++ 中,线程不是执行到此函数立刻终止,而是该函数抛出异常,这样可以有机会执行 stack unwind,析构栈上对象(特别是释放持有的锁)
exie(3) 在 C++ 中不是线程安全的
exit(3) 函数在 C++ 中的作用除了终止进程,还会析构全局对象和已经构造完的函数静态对象,有潜在死锁可能。
| |
如果必须退出,可以考虑 _exit(2) 系统调用,它不会试图析构全局对象,但是也不会执行其他任何清理工作。
善用 __thread 关键字
__thread 是 gcc 内置的线程局部存储设施。
__thread 只能用于修饰 POD 类型,不能修饰 class 类型,因为无法自动调用构造析构函数。
__thread 可以用于修饰全局变量、函数内的静态变量,但是不能用于修饰函数的局部变量或者 class 的普通成员变量。
__thread 变量的初始化只能用编译期常量。
__thread 变量是每个线程有一份独立实体,各个线程的变量值互不干扰。还可以修饰那些 “值可能会变,带有全局性,但是又不值得用全局锁保护” 的变量。
多线程与 IO
一个文件只由一个进程中的一个线程来读写,显然正确,多个线程操作同一块磁盘,在内核中也会排队。
多线程遵守的原则:每个文件描述符只由一个线程操作,从而轻松解决消息收发的顺序性问题,也避免了关闭文件描述符的各种race condition。一个线程可以操作多个文件描述符,但一个线程不能操作别的线程拥有的文件描述符。
对于磁盘文件,必要的时候多个线程可以同时调用 pread(2)/pwrite(2) 来读写同一个文件;对于 UDP,由于协议本身保证消息的原子性,在适当的条件下(比如消息之间彼此独立)可以多个线程同时读写同一个 UDP 文件描述符。
用 RAII 包装文件描述符
Linux 的文件描述符是小整数,程序刚刚启动的时候,0 是标准输入,1 是标准输出,2 是标准错误。这时新打开的文件描述符会是 3,因为 POSIX 标准要求每次打开文件(含 socket)的时候必须使用当前最小科研的文件描述符号码。
RAII,用 Socket 对象包装文件描述符,所有对此文件的描述符的读写操作都通过此对象进行,在对象的析构函数中关闭文件描述符。这样,只要 Socket 对象活着,就不会有其他 Socket 对象跟它一样的文件描述符。
为什么服务端程序不应该关闭标准输出和标准错误?因为第三方库在特殊紧急情况下会往 stdout 或 stderr 打印出错信息,如果我没程序关闭了标准输出和标准错误,这两个文件描述符有可能被网络连接占用,结果造成对方收到莫名奇妙的数据。正确做法是把 stdout 或 stderr 重定向到磁盘文件(最好不要 /dev/null),这样不至于丢失关键信息。当然这些应该由启动服务程序的看门狗进程完成(参考 http://github.com/chenshuo/muduo-protorpc 的 Zurg slave 示例)。对服务程序本身是透明的。
RAII 与 fork()
| |
如果 Foo class 封装了某个资源,而这个资源没有被子进程继承,doit() 后子进程中错乱的。
fork() 后,子进程继承父进程的几乎全部状态,但是有些例外。子进程会继承地址空间和文件描述符,因此管理动态内存和文件描述符用 RAII 即可。但是子进程不会继承:
- 父进程的内存锁,mlock(2)、mlockall(2)
- 父进程的文件锁,fcntl(2)
- 父进程的某些定时器, settimer(2)、alarm(2)、timer_create(2)…
- 见 man 2 fork
多线程与 fork()
linux 的 fork() 只能克隆当前线程的 thread of control,不能克隆其他线程。 fork() 之后,除了当前线程外,其他线程都消失了。也就是说不能一下子 fork() 出一个和父进程一样的多线程子进程。
fork() 之后,子进程不能调用:
- malloc(3)。因为 malloc() 在访问全局状态时几乎肯定会加锁。
- 任何可能分配或释放内存的函数,包括 new、map::insert()、snprintf….
- 任何 Pthreads 函数。 不能用 pthread_cond_singal() 去通知父进程,只能通过读些 pipe(2) 来同步(http://github.com/chensuo/muduo-protorpc 中 Zurg slave 示例的Process::start())
- printf() 系列函数,因为其他线程可能恰好持有 stdout/stderr 的锁。
- 除了 man 7 signal 中明确列出的 “signal 安全” 函数之外的任何函数
唯一安全的做法:fork() 之后立即调用 exec() 执行另一个程序。彻底隔断父子进程的联系。
多线程与 signal
多线程程序中,使用 signal 第一原则时不使用 signal。
- 不要用 signal 作为 IPC 手段。包括不要用 SIGUSR1 等信号来触发服务端的行为。如果需要参考 9.5 增加监听端口方式来实现双向的、可远程访问的进程控制
- 不要使用基于 signal 实现的定时函数,包括 alarm/ualarm/settimer/timer_create、sleep/usleep 。。。
- 不主动处理各种异常信号(SIGTERM、SIGINT。。)只用默认语义结束进程。
- 没有其他方法时,把异步信号改成同步的文件描述符。现代Linux 做法采用 signalfd(2) 把信号之间转换为文件描述符事件,从根本上避免使用 signal handler.(http://github.com/chenshuo/muduo-protorpc 中 Zurg slave 示例的 ChildManager class)
Reference
《Time,Clocks and the Ordering of Events in a Distributed System》 http://research.microsoft.com/en-us/um/people/lamport/pubs/time-clocks.pdf
《Threads Cannot be Implemented as a Library》 http://www.hpl.hp.com/techreports/2004/HPL-2004-209.pdf
http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_09
自调整数据结构,只读也会不安全 http://www.cs.au.dk/~gerth/aa11/slides/selfadjusting.pdf
http://blog.csdn.net/program_think/article/details/3991107
http://www.cppblog.com/lymons/archive/2008/12/19/69810.html
http://www.cppblog.com/lymons/archive/2008/12/25/70227.html
http://www.boost.org/doc/libs/1_34_0/doc/html/thread/faq.html
http://stackoverflow.com/questions/433989/posix-cancellation-points
http://pubs.opengroup.org/onlinepubs/000095399/functions/xsh_chap02_09.html#tag_02_09_05_02
Cancellation and C++ Exceptions http://udrepper.livejournal.com/21541.html
《ELF Handing For Thread-Local Storage》 http://www.akkadia.org/drepper/tls.pdf
__thread 使用规则 http://gcc.gnu.org/onlinedocs/gcc/Thread_002dLocal.html
http://chenshuo.com/book/errata.html
http://www.linuxprogrammingblog.com/threads-and-fork-think-twice-before-using-them
http://www.cppblog.com/lymons/archive/2008/06/01/51836.html
http://www.linuxprogrammingblog.com/all-about-linux-signals?page=11
http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03
http://en.wikipedia.org/wiki/lnterrupt_handler
http://www.gnu.org/software/libc/manual/html_mono/libc.html#Atomic-Data-Access
http://www.cppblog.com/lymons/archive/2008/06/01/51838.html 和 51837.html
Linux 新增系统调用启示 http://blog.csdn.net/solstice/article/details/5327881
《Secure File Descriptor Handling》 http://udrepper.livejournal.com/20407.html