标签:
Please indicate the source: http://blog.csdn.net/gaoxiangnumber1
Welcome to my github: https://github.com/gaoxiangnumber1
第4章C++多线程系统编程精要
?学习多线程编程面临的思维方式的转变有两点:
1.当前线程可能随时会被切换出去,或者说被抢占(preempt)了。
2.多线程程序中事件的发生顺序不再有全局统一的先后关系1。
?当线程被切换回来继续执行下一条语句(指令)的时候,全局数据(包括当前进程在操作系统内核中的状态)可能已经被其他线程修改了。例如,在没有为指针p加锁的情况下,if(p && p->next) { /* … */}有可能导致segfault,因为在逻辑与(&&)的前一个分支evaluate为true之后的一刹那,p可能被其他线程置为 NULL或是被释放,后一个分支就访问了非法地址。
?在单CPU系统中,理论上我们可以通过记录CPU上执行的指令的先后顺序来推演多线程的实际交织(interweaving)运行的情况。
在多核系统中,多个线程是并行执行的,我们没有统一的全局时钟来为每个事件编号。在没有适当同步的情况下,多个CPU上运行的多个线程中的事件发生先后顺序是无法确定的2。在引入适当同步后,事件之间才有了happens-before关系3。
?多线程程序的正确性不能依赖于任何一个线程的执行速度,不能通过原地等待(sleep())来假定其他线程的事件已经发生,而必须通过适当的同步来让当前线程能看到其他线程的事件的结果。无论线程执行得快与慢(被操作系统切换出去得越多,执行越慢),程序都应该能正常工作。例如下面这段代码就有这方面 的问题。
bool running = false; // 全局标志
void threadFunc()
{
while(running)
{
// get task from queue
}
}
void start()
{
Thread thread(threadFunc);
thread.start();
running= true; // 应该放到thread.start()之前。
}
?这段代码假定线程函数的启动慢于running变量的赋值,因此线程函数能进入while循环执行我们想要的功能。如果测试这段代码,十有八九会按我们预期的那样工作。但是,如果系统负载很高,Thread::start()调用pthread_create()陷入内核后返回时,内核决定换另外一个就绪任务来执行。于是running的赋值就推迟了,这时线程函数就可能不进入while循环而直接退出了。
?有人认为在while之前加一小段延时(sleep)就能解决问题,但这是错的,无论加多大的延时,系统都有可能先执行while的条件判断,然后再执行running的赋值。正确的做法是把running的赋值放到thread.start()之前,这样借助 pthread_create()的happens-before语意来保证running的新值能被线程看到。
4.1 基本线程原语的选用
?POSIX threads的函数有110多个,真正常用的不过十几个。而且在C++程序中通常会有更为易用的wrapper,不会直接调用Pthreads函数。这11个最基本的Pthreads函数是:
1.2个:线程的创建和等待结束(join)。封装为muduo::Thread。
2.4个:mutex的创建、销毁、加锁、解锁。封装为muduo::MutexLock。
3.5个:条件变量的创建、销毁、等待、通知、广播。封装为muduo::Condition。
?这些封装class构成了多线程编程的全部必备原语。用这三样东西(thread、mutex、condition)可以完成任何多线程编程任务。当然一般不会直接使用它们(mutex除外),而是使用更高层的封装,如mutex::ThreadPool和mutex::CountDownLatch等,见第2章。
?Pthreads还提供了其他一些原语,有些是可以使用的,有些则是不推荐使用的。可以酌情使用的有:
1.pthread_once,封装为muduo::Singleton。不如直接用全局变量。
2.pthread_key*,封装为muduo::ThreadLocal。可以用__thread替换之。
?不建议使用:
1.pthread_rwlock,读写锁通常应慎用。muduo有意没有封装读写锁。原因是它造成提高性能的错觉(允许多个线程并发读),实际上在很多情况下,与使用最简单的mutex相比,它实际上降低了性能。另外,写操作会阻塞读操作,如果要求优化读操作的延迟,用读写锁是不合适的。
2.sem_*,避免用信号量(semaphore)。它的功能与条件变量重合,但容易用错。
3.pthread_{cancel, kill}。程序中出现了它们,通常表明设计出了问题。
4.2 C/C++系统库的线程安全性
?现行的C/C++标准(C89/C99/C++03)没有涉及线程,新版的C/C++标准(C11和C++11)规定了程序在多线程下的语意,C++11还定义了一个线程库std::thread。
?对于标准而言,关键的不是定义线程库,而是规定内存模型(memory model)。特别是规定一个线程对某个共享变量的修改何时能被其他线程看到,这称为内存序(memory ordering)或者内存能见度(memory visibility)。从理论上讲,如果没有合适的内存模型,编写正确的多线程程序属于投机行为,见论文《Threads Cannot be Implemented as a Library》7。
?不必担心这篇文章提到的问题,可以认为每个支持多线程的操作系统上自带的C/C++编译器对本平台的多线程支持都足够好。
?Unix系统库(libc和系统调用)的接口风格是在20世纪70年代早期确立的,而第一个支持用户态线程的Unix操作系统出现在20世纪90年代早期。线程的出现立刻给系统函数库带来了冲击,破坏了20年来一贯的编程传统和假定。如:
1.errno不再是一个全局变量,因为每个线程可能会执行不同的系统库函数。
2.有些“纯函数”不受影响,如memset/strcpy/snprintf等等。
3.有些影响全局状态或者有副作用的函数可以通过加锁来实现线程安全,如malloc/free、printf、fread/fseek等等。
4.有些返回或使用静态空间的函数不可能做到线程安全,因此要提供另外的版本,例如asctime_r/ctime_r/gmtime_r、 stderror_r、strtok_r等等。
5.传统的fork()并发模型不再适用于多线程程序(§4.9)。
?现在Linux glibc把errno定义为一个宏,errno是一个lvalue,因此不能简单定义为某个函数的返回值,而必须定义为对函数返回指针的dereference。
extern int *__errno_location(void);
#define errno (*__errno_location())
?另外,操作系统支持多线程已有近20年,早先一些性能方面的缺陷都基本被弥补了。例如最早的SGI STL自己定制了内存分配器,而现在g++自带的STL已经直接使用malloc来分配内存,std::allocator已经变成了鸡肋(§12.2)。原先Google tcmalloc相对于glibc 2.3中的ptmalloc2有很大的性能提升,现在最新的glibc中的ptmalloc3已经把差距大大缩小了。
?我们不必担心系统调用的线程安全性, 因为系统调用对于用户态程序来说是原子的。但要注意系统调用对于内核状态的改变可能影响其他线程,见§4.6。
?POSIX标准列出一份非线程安全的函数的黑名单8(All functions defined by this volume of POSIX. 1-2008 shall be thread-safe,except that the following functions need not be thread-safe)。在这份黑名单中,system、getenv/putenv/setenv等等函数都是不安全的。
?因此,现在glibc库函数大部分都是线程安全的。特别是FILE*系列函数是安全的,glibc甚至提供了非线程安全的版本9以应对某些特殊场合的性能需求。
?尽管单个函数是线程安全的,但两个或多个函数放到一起就不安全了。如fseek()和fread()都是安全的,但是对某个文件“先seek再read”这两步操作中间有可能会被打断,其他线程有可能趁机修改了文件的当前位置,让程序逻辑无法正确执行。在这种情况下,我们可以用flockfile(FILE*)和funlockfile(FILE*)函数来显式地加锁。并且由于FILE*的锁是可重入的,加锁之后再调用fread()不会造成死锁。
?如果程序直接使用lseek(2)和read(2)这两个系统调用来随机读取文件,也存在“先seek再read”这种race condition,但是我们无法高效地对系统调用加锁。解决办法是改用pread(2)系统调用,它不会改变文件的当前位置。
?由此可见,编写线程安全程序的一个难点在于线程安全是不可组合的10,一个函数foo()调用了两个线程安全的函数,而这个foo()函数本身很可能不是线程安全的。即便现在大多数glibc库函数是线程安全的,我们也不能像写单线程程序那样编写代码。
?例如,在单线程程序中,如果我们要临时转换时区,可以用tzset()函数,这个函数会改变程序全局的“当前时区”。
// 获取伦敦的当前时间
string oldTz = getenv("TZ"); // save TZ, assuming non-NULL
putenv("TZ=Europe/London"); // set TZ to London
tzset(); // load London time zone
struct tm localTimeInLN;
time_t now = time(NULL); // get time in UTC
localtime_r(&now, &localTimeInLN); // convert to London local time
setenv("TZ", oldTz.c_str(), 1); // restore old TZ
tzset(); // local old time zone
?但是在多线程程序中,这么做不是线程安全的,即便tzset()本身是线程安全的。因为它改变了全局状态(当前时区),这有可能影响其他线程转换当前时间,或者被其他进行类似操作的线程影响。
?解决办法是使用muduo::TimeZone class,每个immutable instance对应一个时区,这样时间转换就不需要修改全局状态了。例如:
class TimeZone
{
public:
explicit TimeZone(const char* zonefile);
struct tm toLocalTime(time_t secondsSinceEpoch) const;
time_t fromLocalTime(const struct tm&) const;
// default copy ctor/assignment/dtor are okay.
// ...
};
const TimeZone kNewYorkTz("/usr/share/zoneinfo/America/New_York");
const TimeZone kLondonTz("/usr/share/zoneinfo/Europe/London");
time_t now= time(NULL);
struct tm localTimeInNY= kNewYorkTz.toLocalTime(now);
struct tm localTimeInLN= kLondonTz.toLocalTime(now);
?对于C/C++库的作者来说,如何设计线程安全的接口成了一大考验。一个基本思路是尽量把class设计成immutable的,这样用起来就不必为线程安全操心了。
?尽管C++03标准没有明说标准库的线程安全性,但我们可以遵循一个基本原则:凡是非共享的对象都是彼此独立的,如果一个对象从始至终只被一个线程用到,那么它就是安全的。
另外一个标准是:共享的对象的read-only操作是安全的11,前提是不能有并发的写操作。例如两个线程各自访问自己的局部vector对象是安全的;同时访问共享的const vector对象也是安全的,但是这个vector不能被第三个线程修改。一旦有writer,那么read-only操作也必须加锁,例如vector::size()。
?根据§1.1.1对线程安全的定义,C++的标准库容器和std::string都不是线程安全的,只有std::allocator保证是线程安全的。一个原因是为了避免不必要的性能开销,另一个原因是单个成员函数的线程安全并不具备可组合性(composable)。
?假设有safe_vector class,它的接口与std::vector相同,不过每个成员函数都是线程安全的。但是用safe_vector并不一定能写出线程安全的代码。例如:
safe_vector<int> vec; // 全局可见
if (!vec.empty()) // 没有加锁保护
{
int x = vec[0]; // 这两步在多线程下是不安全的
}
?在if语句判断vec非空之后,别的线程可能清空其元素,从而造成vec[0]失效。
?C++标准库中的绝大多数泛型算法是线程安全的12,因为这些都是无状态纯函数。只要输入区间是线程安全的,那么泛型函数就是线程安全的。
?C++的iostream不是线程安全的:
std::cout << “Now is ” << time(NULL);
等价于两个函数调用
std::cout.operator<<(“Now is “) .operator<<(time(NULL));
?即便ostream::operator<<()做到了线程安全,也不能保证其他线程不会在两次函数调用之前向stdout输出其他字符。
?对于“线程安全的stdout输出”这个需求,我们可以改用printf,以达到安全性和输出的原子性。但是这等于用了全局锁,任何时刻只能有一个线程调用printf,不高效。在多线程程序中高效的日志需要特殊设计,见第5章。
4.3 Linux上的线程标识
?POSIX threads库提供了pthread_self函数用于返回当前进程的标识符,其类型为pthread_t。pthread_t不一定是一个数值类型(整数或指针),也有可能是一个结构体,因此Pthreads提供了pthread_equal函数用于比较两个线程标识符是否相等。这带来的问题:
1.无法打印输出pthread_t,因为不知道其确切类型。也就没法在日志中用它表示当前线程的id。
2.无法比较pthread_t的大小或计算其hash值,因此无法用作关联容器的key。
3.无法定义一个非法的pthread_t值,用来表示绝对不可能存在的线程id,因此MutexLock class没有办法判断当前线程是否已经持有本锁。
4.pthread_t值只在进程内有意义,与操作系统的任务调度之间无法建立有效关联。比方说在/proc文件系统中找不到pthread_t对应的task。
?glibc的Pthreads实现把pthread_t用作一个结构体指针(它的类型是unsigned long),指向一块动态分配的内存,而且这块内存是反复使用的。这就造成pthread_t的值很容易重复。Pthreads只保证同一进程之内,同一时刻的各个线程的id不同;不能保证同一进程先后多个线程具有不同的id,更不要说一台机器上多个进程之间的id唯一性了。下面代码中先后两个线程的标识符是相同的:
int main()
{
pthread_t t1, t2;
pthread_create(&t1, NULL, threadFunc, NULL);
printf("%lx\n", t1);
pthread_join(t1, NULL);
pthread_create(&t2, NULL, threadFunc, NULL);
printf("%lx\n", t2);
pthread_join(t2, NULL);
}
$ ./a.out
7fad11787700
7fad11787700
?因此,pthread_t不适合用作程序中对线程的标识符。
?在Linux上,建议使用gettid(2)系统调用的返回值作为线程id,这么做的好处有:
1.它的类型是pid_t,其值通常是一个小整数13,便于在日志中输出。
2.在现代Linux中,它直接表示内核的任务调度id,因此在/proc文件系统中容易找到对应项:/proc/tid或/prod/pid/tasktid。
3.在其他系统工具中也容易定位到具体某一个线程,例如在top(1)中可以按线程列出任务,然后找出CPU使用率最高的线程id,再根据程序日志判断是哪个线程在耗用CPU。
4.任何时刻都是全局唯一的,并且由于Linux分配新pid采用递增轮回办法,短时间内启动的多个线程也会具有不同的线程id。
5.0是非法值,因为操作系统第一个进程init的pid是1。
?但是glibc没有封装这个系统调用,需要我们自己实现。封装gettid(2)很简单, 但是每次都执行一次系统调用有些浪费,如何更高效?
?muduo::CurrentThread::tid()采取的办法是用__thread变量来缓存gettid(2)的返回值,这样只有在本线程第一次调用的时候才进行系统调用,以后都是直接从thread local缓存的线程id拿到结果,效率无忧。多线程程序在打日志的时候可以在每一条日志消息中包含当前线程的id,不必担心有效率损失。
?还有一个小问题,万一程序执行了fork(2),那么子进程会不会看到stale的缓存结果呢?解决办法是用pthread_atfork()注册一个回调,用于清空缓存的线程id。具体代码见muduo/base/CurrentThread.h和Thread.cc。
4.4 线程的创建与销毁的守则
?线程的创建和销毁是编写多线程程序的基本要素,线程创建要遵循的原则:
1.程序库不应该在未提前告知的情况下创建自己的“背景线程”。
2.尽量用相同的方式创建线程,例如muduo::Thread。
3.在进入main()函数之前不应该启动线程。
4.程序中线程的创建最好能在初始化阶段全部完成。
?线程是稀缺资源,一个进程可以创建的并发线程数目受限于地址空间的大小和内核参数,一台机器可以同时并行运行的线程数目受限于CPU的数目。因此在设计服务端程序的时候要规划好线程的数目,特别是根据机器的CPU数目来设置工作线程的数目,并为关键任务保留足够的计算资源。如果程序库在背地里使用了额外的线程来执行任务,我们这种资源规划就漏算了。可能会导致高估系统的可用资源,结果处理关键任务不及时,达不到预设的性能指标。
?还有一个重要原因,一旦程序中有不止一个线程,就很难安全地fork()了(§4.9)。因此“库”不能偷偷创建线程。如果确实有必要使用背景线程,至少应该让使用者知道。另外,如果有可能,可以让使用者在初始化库的时候传入线程池或event loop对象,这样程序可以统筹线程的数目和用途,避免低优先级的任务独占某个线程。
?理想情况下,程序里的线程都是用同一个class创建的(muduo::Thread),这样容易在线程的启动和销毁阶段做一些统一的记录(bookkeeping)工作。比如说调用一次muduo::CurrentThread::tid()把当前线程id缓存起来,以后再取线程id就不会陷入内核了。也可以统计当前有多少活动线程15,进程一共创建了多少线程,每个线程的用途分别是什么。C/C++的线程没有名字,但是可以通过Thread class实现类似的效果。如果每个线程都是通过muduo::Thread启动的,这些都可以做到。必要的话可以写一个ThreadManager singleton class,用它来记录当前活动线程,可以方便调试与监控。
?但是这不是总能做到的,有些第三方库(C语言库)会自己启动线程,这样的“野生”线程就没有纳入全局的ThreadManager管理之中。muduo::CurrentThread::tid() 必须要考虑被这种“野生”线程调用的可能,因此它必须每次都检查缓存的线程id是否有效,而不能假定在线程启动阶段已经缓存好了id,直接返回缓存值就行了。如果库提供异步回调,一定要明确说明会在哪个(哪些)线程调用用户提供的回调函数,这样用户可以知道在回调函数中能不能执行耗时的操作,会不会阻塞其他任务的执行。
?在main()函数之前不应该启动线程,因为这会影响全局对象的安全构造。C++保证在进入main()之前完成全局对象16的构造。同时,各个编译单元之间的对象构造顺序是不确定的,我们也有一些办法来影响初始化顺序,保证在初始化某个全局对象时使用到的其他全局对象都是构造完成的。但无论如何这些全局对象的构造是依次进行的,都在主线程中完成,无须考虑并发与线程安全。
?如果其中一个全局对象创建了线程,那就危险了。因为这破坏了初始化全局对象的基本假设。万一将来代码改动之后造成该线程访问了未经初始化的全局对象,那么这种错误查起来很费劲。或许你想用锁来保证全局对象初始化完成,但是怎么保证这个全局的锁对象的构造能在线程启动之前完成呢?因此,全局对象不能创建线程。如果一个库需要创建线程,那么应该进入main()函数之后再调用库的初始化函数去做。
?不要为了每个计算任务,每次请求去创建线程。一般也不会为每个网络连接创建线程,除非并发连接数与CPU数相近。一个服务程序的线程数目应该与当前负载无关,而应该与机器的CPU数目有关,即load average有比较小(最好不大于CPU数目)的上限。这样尽量避免出现thrashing,不会因为负载急剧增加而导致机器失去正常响应。这么做的重要原因是,在机器失去响应期间,我们无法探查它究竟在做什么,也没办法立刻终止有问题的进程,防止损害进一步扩大。如果有实时性方面的要求,线程数目不应该超过CPU数目,这样可以基本保证新任务总能及时得到执行,因为总有CPU是空闲的。
?最好在程序的初始化阶段创建全部工作线程,在程序运行期间不再创建或销毁线程。借助muduo::ThreadPool和muduo::EventLoop,我们能把计算任务和IO任务分配到已有的线程,代价只有新建线程的几分之一。
?线程的销毁的方式有17:
1.自然死亡。从线程主函数返回,线程正常退出。
2.非正常死亡。从线程主函数抛出异常或线程触发segfault信号等非法操作18。
3.自杀。在线程中调用pthread_exit()来立刻退出线程。
4.他杀。其他线程调用pthread_cancel()来强制终止某个线程。
?pthread_kill()是往线程发信号,见§4.10。
?线程正常退出的方式只有一种,即自然死亡。任何从外部强行终止线程的做法和想法都是错的19 20。因为强行终止线程的话(无论是自杀还是他杀),它没有机会清理资源。也没有机会释放已经持有的锁,其他线程如果再想对同一个mutex加锁,那么就会立刻死锁。因此不用去研究cancellation point这种概念。
?如果确实需要强行终止一个耗时很长的计算任务,而又不想在计算期间周期性地检查某个全局退出标志,那么可以考虑把那一部分代码fork()为新的进程,kill(2)一个进程比杀本进程内的线程要更安全。fork()的新进程与本进程的通信方式,最好用文件描述符(pipe(2)/socket-pair(2)/TCP socket)来收发数据,而不要用共享内存和跨进程的互斥器等IPC,因为这样仍然有死锁的可能。
?muduo::Thread不是传统意义上的 RAII class,因为它析构的时候没有销毁持有的Pthreads线程句柄(pthread_t),也就是说Thread的析构不会等待线程结束。一般而言,我们会让Thread对象的生命期长于线程,然后通过Thread::join()来等待线程结束并释放线程资源。如果Thread对象的生命期短于线程,那么就没有机会释放pthread_t了。
?如果能做到“程序中线程的创建最好能在初始化阶段全部完成”,则线程是不必销毁的,伴随进程一直运行,彻底避开了线程安全退出可能面临的各种困难,包括Thread对象生命期管理、资源释放等等。
4.4.1 pthread_cancel与C++
?POSIX threads有cancellation point概念,意思是线程执行到这里有可能会被终止(cancel)(如果别的线程对它调用了pthread_cancel()的话)。POSIX标准列出了必须或者可能是cancellation point的函数22 23。
?在C++中,cancellation point的实现与C语言不同,线程不是执行到此函数就立刻终止,而是该函数会抛出异常。这样可以有机会执行stack unwind,析构栈上对象(特别是释放持有的锁)。如果一定要使用cancellation point,建议读Cancellation and C++ Exceptions24。不过不建议从外部杀死线程。
4.4.2 exit(3)在C++中不是线程安全的
?exit(3)函数在C++中除了终止进程,还会析构全局对象和已经构造完的函数静态对象。这有潜在的死锁可能,考虑下面这个例子。
void someFunctionMayCallExit()
{
exit(1);
}
class GlobalObject // : boost::noncopyable
{
public:
void doit()
{
MutexLockGuard lock(mutex_);
someFunctionMayCallExit();
}
~GlobalObject()
{
printf("GlobalObject:~GlobalObject\n");
MutexLockGuard lock(mutex_); // 此处发生死锁
// clean up
printf("GlobalObject:~GlobalObject cleanning\n");
}
private:
MutexLock mutex_;
};
GlobalObject g_obj;
int main()
{
g_obj.doit();
}
?GlobalObject::doit()函数调用了exit(),从而触发了全局对象g_obj的析构。 GlobalObject的析构函数会试图加锁mutex_,而此时mutex_已经被GlobalObject::doit()锁住了,于是造成了死锁。
?举一个调用纯虚函数导致程序崩溃的例子。假如有一个策略基类,在运行时我们会根据情况,使用不同的无状态策略(派生类对象)。由于策略是无状态的,因此可以共享派生类对象,不必每次都新建。这里以日历(Calendar)基类和不同国家的假期(AmericanCalendar和BritishCalendar)为例,factory函数返回某个全局对象的引用,而不是每次都创建新的派生类对象。
class Calendar : boost::noncopyable
{
public:
virtual bool isHoliday(muduo::Date d) const= 0; // 纯虚函数
virtual ~Calendar() {}
};
class AmericanCalendar : public Calendar
{
public:
virtual bool isHoliday(muduo::Date d) const;
};
class BritishCalendar : public Calendar
{
public:
virtual bool isHoliday(muduo::Date d) const;
};
AmericanCalendar americanCalendar; // 全局对象
BritishCalendar britishCalendar;
// factory method returns americanCalendar or britishCalendar
Calendar& getCalendar(const string& region);
?通常的使用方式是通过factory拿到具体国家的日历,再判断某一天是不是假期:
void processRequest(const Request& req)
{
Calendar& calendar= getCalendar(req.region);
// 如果别的线程在此时调用了exit() ……
if (calendar.isHoliday(req.settlement_date))
{
// do something
}
}
?如果我们想主动退出这个服务程序,于是某个线程调用了exit(),析构了全局对象,结果造成另一个线程在调用Calendar::isHoliday时发生崩溃:
pure virtual method called
terminate called without an active exception
Aborted (core dumped)
?这只是举例说明“用全局对象实现无状态策略”在多线程中析构可能有危险。在真实的项目中,Calendar应该在运行的时候从外部配置读入,而不能写死在代码中。
?这其实不是exit()的过错,而是全局对象析构的问题。C++标准没有照顾全局对象在多线程环境下的析构,似乎也没有更好的办法。如果确实需要主动结束线程,则可以考虑用_exit(2)系统调用。它不会试图析构全局对象,但是也不会执行任何清理工作,比如flush标准输出。
?安全地退出一个多线程程序不是一件容易的事情。这里还没有涉及如何安全地退出其他正在运行的线程,这需要精心设计共享对象的析构顺序,防止各个线程在退出时访问已失效的对象。在编写长期运行的多线程服务程序的时候,可以不必追求安全地退出,而是让进程进入拒绝服务状态,然后就可以直接杀掉(§9.3)。
4.5 善用__thread关键字
?__thread是GCC内置的线程局部存储设施(thread local storage)。它的实现非常高效,比pthread_key_t快很多,见《ELF Handling For Thread-Local Storage》26。
?__thread变量的存取效率可与全局变量相比:
?__thread使用规则27:
1.只能用于修饰POD类型,不能修饰class类型,因为无法自动调用构造函数和析构函数。
2.__thread可以用于修饰全局变量、函数内的静态变量,但是不能用于修饰函数的局部变量或者class的普通成员变量。
3.__thread变量的初始化只能用编译期常量。
__thread string t_obj1("Chen Shuo"); // 错误,不能调用对象的构造函数
__thread string* t_obj2= new string; // 错误,初始化必须用编译期常量
__thread string* t_obj3= NULL; // 正确,但是需要手工初始化并销毁对象
?__thread变量是每个线程有一份独立实体,各个线程的变量值互不干扰。除了这个主要用途,它还可以修饰“值可能会变,带有全局性,但是又不值得用全局锁保护”的变量。muduo代码用到的__thread:
1.muduo/base/Logging.cc
缓存最近一条日志时间的年月日时分秒,如果一秒之内输出多条日志,可避免重复格式化。另外,muduo::strerror_tl把strerror_r(3)做成如同strerror(3)一样好用,而且是线程安全的。
2.muduo/base/ProcessInfo.cc
用线程局部变量来简化::scandir(3)的使用。
3.muduo/base/Thread.cc
缓存每个线程的id。
4.muduo/net/EventLoop.cc
用于判断当前线程是否只有一个EventLoop对象。
?以上例子都是__thread修饰POD类型的变量。如果要用到thread local的class对象,考虑使用muduo::ThreadLocal和muduo::ThreadLocalSingleton,它能在线程退出时销毁class对象。例如examples/asio/chat/server_threaded_highperformance.cc用ThreadLocalSingleton来保存每个EventLoop线程所管辖的客户连接,以实现高效的消息转发(§7.11)。
Please indicate the source: http://blog.csdn.net/gaoxiangnumber1
Welcome to my github: https://github.com/gaoxiangnumber1
标签:
原文地址:http://blog.csdn.net/gaoxiangnumber1/article/details/51285025