接下来一个技术细节
是线程的局部存储 叫TLS
我们用它
来存储每个线程的独有的数据
或者有时候也称线程的特定数据
前面不是讲过了吗
进程的多个线程是通过全局的堆
来共享全局数据对象的
一个进程 它在全局堆里
分配了一些特定的数据对象
那么这些数据对象
会被这个进程的所有的线程所共享
都能看得见 都能用
那么如果有几个线程
想要设定这个全局数据对象的值呢
那到底哪一个为准呢
所以很多时候每个单独的线程
都想访问自己的
那个全局对象的唯一的一个副本
那么怎么保证这一点呢
这就叫线程的局部存储
我们想让这个线程
拥有一个全局数据对象独立的副本
那么就必须使用线程局部存储技术
在这里那些全局数据对象
是不可以简单地赋值和读取的
那么我们怎么设置线程局部存储呢
有这样的线程局部存储管理函数
第一个pthread_key_create()
它会构造一个特定的键值
把它和线程局部存储的那个数据对象
关联起来
那么我们接下来就可以通过这个键值
对这个线程局部存储数据对象
进行控制和管理
两个函数pthread_setspecific()
和pthread_getspecific()就专门用来做这个
我们在这里定义一个静态的量
pthread_key_t tlk
这就是关联线程日志文件的
一个特定的键
当然关联的是那个文件指针
做的就是线程局部存储
我们这个例子要做什么事情呢
就是我们要启动一系列的线程
在这个线程里边
每一个线程会做一些特定的处理
并且把它处理的事务做个记录
也就是每个线程
都有自己独立的日志文件
你启动了很多个线程
这些线程共用一个日志文件
显然不太好
我要区分哪个线程是哪一个吗 对吧
单独使用线程日志文件显然更方便
数据不就不共享了嘛
但是这个日志文件本身作为一个对象
在我们的程序内部一开始
这是独一的
当你创建很多个线程的时候
显然不能访问那个独一的文件
因为每个特定的文件那个文件的名字
都应该和这个线程的ID是相关的
所以我们就需要使用线程局部存储
来处理这个问题
在我们的线程函数里
我们会构造跟这个线程相匹配的文件
文件名字怎么来的呢
就是看这个线程的ID
得到这个线程的ID
构造这个线程日志文件的名字
然后我们打开这个文件
就得到了那个文件指针
你注意 因为对于不同的线程来讲
哪怕使用的是同样一段代码
它访问的那个线程的日志文件
名字都是不一样的
跟它的ID相关嘛
怎么可能一样呢 肯定不一样
所以这个文件指针针对于不同的线程
应该指向不同的文件
这就是线程局部存储了
那么普通的访问手段就不成了
我们就必须使用线程局部存储的技术
我们要定义static pthread_key_t
这样型的一个对象 tlk
我们就用它这个特定的键值tlk
和每个线程的日志文件指针关联在一起
函数WriteToThreadLog()
这就是向线程日志文件里边写入数据
传的参数就是一个写入的消息
我们就通过pthread_getspecific()
这个函数的调用
通过tlk这个键值
得到它所保护的那个文件指针
然后向那个文件里写入数据
回过头来看线程函数
我们得到这个文件指针以后
我们一定要调用pthread_setspecific()这个函数
将这个文件指针和这个键值关联起来
一旦有了这个函数调用
那么这个线程局部存储
就被你构造出来了
每一个线程都可以使用tlk这个键值
来访问它自己所独有的
那个文件指针对象
我们这里面提供了几个线程
8个线程
所以每个线程都会有自己的
独有的那一个文件指针fp
它们都指向不同的地方
这就是线程的局部存储
特别需要注意的是
在我们的主函数里
你要想让你的pthread_getspecific()、
pthread_setspecific()能够工作
那么你必须要完成这个线程的创建
要在线程的创建之前
完成这个线程的局部存储键值的创建
所以要先调用pthread_key_create()
要创建这个线程的对应的键
创建的就是tlk
要传递一个清除这个
线程局部存储的那个属性对象的
那个函数
我们传的是CloseThreadLog
就关闭这个线程 要把它清除掉
用完了以后 记得pthread_key_delete()
销毁这个键值
这一小节最后一个知识点
就是线程的清除
线程的清除函数
它就是一个典型的回调函数
单void*参数 没有返回值
它的最主要的目的
就是销毁线程退出或被撤销时
没有来得及释放的资源
你写了这样的一个线程清除函数
然后你就可以调用pthread_cleanup_push()
把这个线程清除函数注册进去
当线程结束以后
你可以通过调用pthread_cleanup_pop()
取消这个线程的注册
你传递一个非零值
那么它就在取消注册的时候
替你完成线程最后的清除工作
你不pop 它实际上是不清除的
你一pop 传一个非零值
它实际上替你完成这个
线程的清除工作
你传0它实际上也不清除
在这里我有一个函数AllocateBuffer()
要分配一个缓冲区
然后有一个对应的清除函数
DeallocateBuffer()
要销毁这个缓冲区里面的数据
我们有一个函数DoSomeWork()
它要做一些特定的事务处理
那么在这里我们会分配一段内存
分配一段动态存储区
分配完了以后呢
你要保证这个资源能够被释放嘛
所以我们就注册一个线程清除函数
pthread_cleanup_push()
把这个DeallocateBuffer
这个函数给它注册进去
我们注册这个清除函数
然后当它执行完了以后
调用pthread_cleanup_pop()
传一个非零的值
它就会取消这个函数的注册
执行那个函数
把这个线程分配的这个缓冲区给销毁
简单吧 就是一个回调函数的实现
注册、弹出
就这两件事情 做完就行
在线程的清除的过程中
有一个非常重要的问题
可能用到了C++的部分特性
但是写的主要是C的代码呀
那么真要是用面向对象来实现呢
可能就会有一些问题
对象的析构函数在线程退出的时候
可能没有机会去获得执行
那么这个函数没做 没析构
那这个对象怎么办呢
这不就有一个问题吗
线程栈上的这个数据
就有可能没有被清除
那么你怎么才能够保证
这样的线程资源能够被正确地释放呢
有一个解决方案就是这样
你可以定义一个异常类
在线程 在它准备退出的时候
就会触发这个异常
然后你在异常处理里边
完成这个资源的释放
你引发异常的时候
C++会确保那些对象的析构函数
会被调用的
很不同寻常的解决方案
但是能够解决我们的问题
我们看代码
我们定义一个异常类EThreadExit
退出线程异常
实现非常简单
就是构造函数
一个简单的值 构造
有一个公有的成员函数DoThreadExit()
调用pthread_exit()
退出这个线程就完了
线程函数 try
当线程需要立即退出的时候
我们throw 抛出这个异常
然后我们catch 抓住这个异常
抓住这个异常以后做什么事情呢
就做我们线程退出 Over
干嘛要这么写
最重要的就是在这里
我们可以在DoThreadExit()这个函数内部
完成资源的清除工作
你也可以在catch这个子句里
完成这个线程的清除工作
这样的话 我们保证在任何时候
线程的里边所分配的资源
都会被正确地释放
从而能够完成全部资源的清除