WINVNC(二)omni_thread

来源:互联网 发布:发膜一周用几次知乎 编辑:程序博客网 时间:2024/05/20 09:10

 

omni_thread是一个C++的跨平台的线程包装库。

接口文件omnithread.h

可以看出一般的跨平台编码的方式:

首先通过宏来判断目前所在的操作系统平台(如果你编译的时候出现了“No implementation header file”的提示,请在此文件查找对应的宏,我用的是WIN32系统,所以预定义了__WIN32__),然后包含不同的头文件(WIN32是nt.h头文件),在.cpp文件中则可以避免调用系统API和相关数据类型。比如SOCKET

 

比如在UNIX的头文件posix.h中

#define omni_socket int

而在nt.h头文件中

#define omni_socket SOCKET

X.CPP中

omni_socket s = socket( domain, type, portocol);

 

也就是把不同系统环境的相同点提取出来,这能提高代码重用率。omni_thread的相同点是类名和函数接口(统一写在omnithread.h文件作为调用者的接口头文件),如果类中有部分成员变量是特有类型(比如WINDOWS的HANDLE)或者为了实现特有的需求(比如为在WINDOWS实现condition variable而添加的cond_semaphore等)将定义到跟系统相关的头文件(nt.h)。一般来说类的实现如果区别不是太多可以通过#if之类的宏来集成到一个cpp文件,但omni_thread针对WINDWOS专门写了一个nt.cpp,也许是量太大了,没必要特意地撮合在一起。

 

omni_mutex和omni_semaphore两个类分别包装了两种性质的锁,在WINDWOS中分别是CRITICAL_SECTION和semaphore。关于它们的区别网上有很多,这里不介绍。

 

omni_mutex_lock和omni_semaphore_lock利用在对象生命周期结束的时候自动调用析构函数的特点对上面两个类又作了个小包装。这样可以不用写代码显性地释放锁,对付在很多返回点或者异常处理非常有用。这个方法相信大家都广为使用了,但我觉得这样做有点点风险,忽略了返回值并且对代码阅读也有点影响。

 

 

omni_thread::init_t

库里面定义了一个satic omni_thread::init_t omni_thread_init;的变量。

这样可以保证在进入main函数之前,其结构函数就被调用,在结构函数中作初始化。

 

 

 

构造函数里展示了一些很重要的信息:怎么创建一个omni_thread类与当前线程绑定。

关键字一:当前,我们并没有调用start() or start_undetached()来创建线程,而是new一个对象,给_state(线程状态)和nt_id(线程ID)赋值。

关键字二:绑定,给_state和nt_id赋值看起来像是绑定了,但这并不方便,我们需要随时能够在线程中访问这个对象,那就得考虑这个对象指针的保存问题。TLS(Thread Local Storage )能达到我们想要的效果。

关于TLS,MSDN有说明和一段简介的代码,但一句话让我很是费解:TlsSetValue函数的dwTlsIndex必须是TlsAlloc分配的。

在网上游了很久后,我是这样理解的:

我们可以想象一个公司给员工发了一组柜子用来保存各自的私人物品(这里公司是进程,每个员工是一个线程,员工一进公司就能获得柜子,线程一创建就分配了位数组)。但公司的老大(编程者)规定一号柜用来放衣服,二号柜放文件,这样子管理起来就不会乱了。老大下次觉得要个柜子专门放食物就调用TlsAlloc查询当前是否有空柜未被定义使用,发现3号柜没有规定存放物品,于是发文通知大家用3号柜来存放食物。老大也可以用TlsFree来公告取消食物柜的使用。当然有些员工可能不小心或者故意用一号柜用来放文件,二号柜放衣服,当公司某个部门急需某些文件而去二号柜拿的时候,这可能就悲剧。

关于这个我写了些代码求证,在主线程(::TlsAlloc并不一定要在主线程调用,但必须保证在其它线程使用该索引之前)g_tlsUsedSlot = ::TlsAlloc();正常的使用是在子辈线程中调用::TlsSetValueg_tlsUsedSlot, 1);我故意写成了::TlsSetValueg_tlsUsedSlot+1, 1);发现一切运行良好,于是我有点偷着乐,但当我写成::TlsSetValueg_tlsUsedSlot-1, 1);的时候,灾难发生了,程序崩溃。异常处理函数很幽默的注释(Should never reach here)。但我只调用了一次TlsAlloc,返回的索引号居然是4。TlsAlloc应该是从第一个元素开始遍历,谁在抢在了我前面?传闻那是一个更大的老板==编译器。至于大老板占用这几个索引干了啥我也没能力去追究了。

 

一句话omni_thread_init的构造函数就把主线程和一个包装类绑定了,并且用static DWORD self_tls_index保存了索引号,指定用这个柜子来装包装类指针。

 

 

 

omni_thread

线程包装类,这里把线程分成两类:detached和undetached thread。成员变量int detached记录了这一点。区别在于detached会自己释放(delete),看起来就像你派一个人去帮你作点小事,你觉得他可以十拿九稳地完成任务,甚至用不着他回复。

构造函数有三个,我们先看前两个:

 

 

回调函数参数fn如果没有返回值,这个时候默认你是打算创建detached thread,起个线程就让它自生自灭,哪怕它发生了严重的错误你也不想理(detached自动置1)。另一个则相反。

所有的结构函数都调用了common_constructor:

 

 

 

在这个函数里我们看到了全局变量static int next_id;。这是个线程计数器。_id保存的是我们自定义的计数ID。cond_semaphore这些我觉得在分析omni_condition的时候再聊比较好。

 

成员函数void start(void):

首先发现了成员变量mutex的作用,保护状态_state的切换和读取。调用C++运行时库函数_beginthreadex来创建线程。

回调函数omni_thread_wrapper:

 

 

因为回调函数在创建线程中运行,所以这里我们终于可以使用柜子保存指针了。然后调用包装类构造函数传入的回调指针或者虚函数。这里利用了多态,我们看下第三个构造函数:omni_thread(void* arg = NULL, priority_t pri = PRIORITY_NORMAL);这个构造函数是给子类用的,子类如果在构造函数中调用第三个构造函数,那么void (*fn_void)(void*); void* (*fn_ret)(void*);这两个指针值为0,会根据detached来决定运行run还是run_undetached。也就是子类中重载run和run_undetached取代前两个构造函数的回调指针参数的方式。

 

 

omni_thread::exit(void* return_value)是个静态函数,这就是TLS的好处,在这里detached thread会释放自己,并且切换线程状态。

 

 

static void get_time_now(unsigned long* abs_sec, unsigned long* abs_nsec)这个时间函数把WINDOWS的1601.1.1基准的时间换成了UNIX的1970.1.1的时间,关于时间相关的以后想在另外的帖子写,这里不啰嗦了。

 

 

_internal_omni_thread_dummy

 

继承自omni_thread,多了个成员变量next,明显这是个omni_thread链表的结点类。cache就是这样的一个链表,作用是空闲内存管理。

 

 

 

_internal_omni_thread_helper

程序中并不是所有线程都是包装类创建的,condition variable的实现必须借助TLS才可以做得更好,_internal_omni_thread_helper的构造函数中首先通过omni_thread::self();判断当前线程有无在柜子里保存包装类指针,如果没有,查看cache中有无空闲的对象,有则取之,无则创建一个。_internal_omni_thread_helper的析构函数是将_internal_omni_thread_helper放回cache链表。如果在一个线程中创建一个_internal_omni_thread_helper对象,那么在这个对象的生存期内(如果对象摧毁了,千万别去拿了,这个时候指针也许指向了其它线程的包装类,因为cache也许把它分配给了其它线程),我们都可以在它的柜子中找到包装类指针。

 

 

 

omni_condition

我学习编程基本都是VC编程,以前都没有condition variable这个概念,看了网上关于它的一些解释,好像挺方便的,看下omni的实现先要分析下_internal_omni_thread_helper,由于我忽略了  inline operator omni_thread* () { return t; }

  inline omni_thread* operator->() { return t; }两个操作符重载,所以近两个小时头是晕的,居然还有这样写的。。。

omni_condition::wait(void)函数:

 

把当前线程通过其包装类的成员变量cond_next和cond_prev链接起来,意思是每个调用同一个omni_condition对象wait接口的线程都会加入到一个链表中。然后等待当前线程对象的cond_semaphore。

omni_condition::signal(void):

 

则是从该链表中删除一个线程对象。然后释放该对象的cond_semaphore。

也许是没用过condition variable,感觉没啥用处,这就是一个简单的事件通知,确硬与一个MUTEX挂上了钩(我感觉它并不能精准地即时通知,因为signal后,并不能保证wait线程是之后第一个进入MUTEX),它唯一的亮点也许是可以自己来决定在wait队列中的线程获取事件的顺序。貌似在VNC里它就是一个简单的事件通知应用。