线程管理器(thread manager)是用于job线程管理的基础结构,但针对bmi、trove和dev三个主要组件提供不同的接口和实现。它们各有特点,也颇具共性,如下列表展示了三类各自的主要接口:
PINT_thread_mgr_bmi: _start _stop _cancel _getcontext _unexp_handler
PINT_thread_mgr_trove: _start _stop _cancel _getcontext
PINT_thread_mgr_dev: _start _stop _unexp_handler
左侧前缀和同行各个表项拼接起来就构成了相应的函数名(如PINT_thread_mgr_bmi_start),空缺表明没有该函数。它们均在src/io/job/thread-mgr.c文件中定义。
- 线程启动
函数1. PINT_thread_mgr_JOBTYPE_start
(JOBTYPE = bmi | trove | dev)
此函数用于开启bmi、trove或dev类型任务的主执行线程(第20行),该线程执行的函数是JOBTYPE_thread_function(即pthread_create函数的第三个参数),后文“线程执行”一节还会介绍这个线程中函数的主要工作。
在创建完新线程的同时,会将全局变量JOBTYPE_thread_running置一(第17行),表明相应任务类型的线程正在运行;并且将引用计数JOBTYPE_thread_ref_count加一(第18行)。这两组变量都是全局的,定义在thread-mgr.c中。
在执行创建之前,第5~13行则是首先判断JOBTYPE_thread_ref_count取值,如果非零,说明已经有线程在运行,不需重新开启,只需继续增加一个引用计数(第10行),并直接返回(第12行)。这几行代码意味着,每个类型的任务在线程管理器内至多有一个线程在执行JOBTYPE_thread_function函数。
另外,如果创建新的线程,都伴随着开辟新的上下文(第15行),并将新的上下文ID保存在global_JOBTYPE_context中(通过该open_context函数的输出参数),我们称这些上下文为全局上下文 。而变量global_JOBTYPE_context是在thread-mgr.c中定义的全局变量。
注意,对dev组件没有上下文(context)的概念,所以也就没有第15行open_contex的操作;同理,也没有如下对应的getcontext函数。
函数2. PINT_thread_mgr_JOBTYPE_getcontext
(JOBTYPE = bmi | trove)
顾名思义,这一组函数用于提供对应任务类型的全局上下文ID。其操作非常简单,参数输出的其实就是global_bmi_context或global_trove_context(第6行)。我们已经介绍过,它们各自记录着对应任务类型线程的全局上下文ID;全局上下文在线程启动时(函数1第15行)开辟。
- 线程执行
线程执行的主体是JOBTYPE_thread_function(JOBTYPE = bmi | trove | dev),也是管理整个job操作的核心。如果预定义了“__PVFS2_JOB_THREADED__”开启任务线程开关,则JOBTYPE_thread_function将在后台持续不断的执行(参见函数3第9~12行)。
我们以函数bmi_thread_function为例介绍此函数所做的主要工作,如函数3.
在第13~92行的while循环中,主要完成了两件事情:
- 检测和处理意外(unexpected)消息,第15~65行
- 检测和处理全局上下文中未处理的其他BMI操作,第67~91行
由于函数体较长,不再集中到函数后引用行数解说,而是将解说直接镶嵌在注释中,如下。
函数3. bmi_thread_function
除了注释之外,还要特别说明的是对互斥锁bmi_test_mutex和条件变量bmi_test_cond的使用,它们主要针对函数PINT_thread_mgr_bmi_cancel所在的线程,详见函数5后面的介绍。
第18行和第33行提到的PINT_thread_mgr_bmi_unexp_handler函数如下,同样是定义在本thread-mgr.c文件中。
函数4. PINT_thread_mgr_bmi_unexp_handler
该函数用于注册处理意外消息的函数句柄,也就是该类BMI操作结束时的回调函数。进行的操作主要是第11行,完成对全局变量bmi_unexp_fn的赋值。该函数在张贴接受意外消息的任务时被调用(在src/io/job/job.c文件定义的函数job_bmi_unexp中调用),所以每次会增加待处理的意外消息数(第12行);同时,需要保证每次通过该函数注册的回调函数相同,这个判断和错误处理在第5~10行完成。
- 取消操作
此类操作主要用于撤销尚未执行回调函数的BMI操作。
函数5. PINT_thread_mgr_bmi_cancel
主要逻辑参见注释。第23行判断要取消的操作是否已经完成,已完成的操作及其回调函数记录在全局变量stat_bmi_id_array和stat_bmi_user_ptr_array中(见函数3的第71~73行,由BMI_testcontext函数的输出参数赋值)。
下面我们重点看一下互斥锁bmi_test_mutex和条件变量bmi_test_cond的使用,以及函数3和函数5的执行关系。
首先罗列所有的互斥段如下:
- 互斥段1.1:函数3第59~62行,循环等待条件变量bmi_test_cond,直至计数变量bmi_test_cancel_waiter为零。
- 互斥段1.2:函数3第64行,置标识变量bmi_test_flag为一。
- 互斥段2:函数3第77~80行,置标识变量bmi_test_flag为零,向条件变量bmi_test_cond发信号。
- 互斥段3.1:函数5第9~17行,增加计数变量bmi_test_cancel_waiter;循环等待条件变量bmi_test_cond,直至置标识变量bmi_test_flag为零。
- 互斥段3.2:函数5第19~35行,减少计数变量bmi_test_cancel_waiter,实际执行BMI_cancel操作,而后向条件变量bmi_test_cond发信号。
注意函数调用pthread_cond_wait(&bmi_test_cond, &bmi_test_mutex)的含义是先释放互斥锁bmi_test_mutex,然后加入等待条件变量bmi_test_cond的队列,等到被触发时再重新获得互斥锁bmi_test_mutex并继续后续代码。
然后分析如上分段互斥和触发逻辑。关键点在于标识变量bmi_test_flag和计数变量bmi_test_cancel_waiter的作用:
互斥段1.2和2之间(函数3第67~73行)的主要工作是调用BMI_testcontext函数,会对全局变量stat_bmi_id_array数组赋值,而该数组被互斥段3.2使用,因此段1.2和2之间的部分与段3.2不可同时执行。为此,在1.2将标识变量bmi_test_flag置一,在2将其归零;并且在3.1检测标识变量不为一时才允许进入3.2,以保证段1.2~2之间部分和段3.2互斥。当然,这个要求也可通过让双方争抢一个互斥锁实现,则不必设置标识变量,但需引入一个新锁。
计数变量bmi_test_cancel_waiter记录了队列中的取消操作(互斥段3.2),在1.1段等待队列中的取消操作完成后才进行下一次BMI_testcontext函数调用。这里要注意触发顺序的问题,当某次BMI_testcontext函数调用完成后,执行互斥段2,因为不是广播所以只会触发队列中的一个挂起操作,此操作一定在3.1段,且可顺利进入3.2段;段末执行下一个触发,依此类推。【注意】队列中不可以是1.1段,否则可能死锁,因为如果队后还有3.1段,1.1段就会继续等待(排入队尾),而此段没有触发队列中的下一个。我们注意到函数1不论被调用多少次,至多开启一次线程,所以函数3线程只有一个,而互斥段1.1和2同属函数3,所以不会出现2段执行时有1.1段等待的情况。(如果避免这种特殊要求,可以在段1.1调用pthread_cond_wait函数前增加一次pthread_cond_signal调用。)
计数变量bmi_test_cancel_waiter保证每两次BMI_testcontext函数调用之间(可能有执行回调函数等耗时操作,见互斥段2后)积累的取消操作(函数5)都执行完。如果不使用该变量,仅由前所述设置段1.2~2之间部分和段3.2的互斥锁,则有可能BMI_testcontext函数比某些取消操作先争抢到锁,致使取消操作积压,执行很多本该取消的回调函数。
- 线程终止
函数6. PINT_thread_mgr_JOBTYPE_stop
(JOBTYPE = bmi | trove | dev)
回顾线程启动的函数,实际只有一个线程在运行,其他启动调用只是增加引用计数JOBTYPE_thread_ref_count;所以线程终止时,首先减少引用计数(第4行),如果还有其他引用则无需其他操作,而当引用为零时,说明此线程可以被销毁,执行实际线程终止操作(第11行),并关闭全局上下文(第13行)。注意dev没有close_context操作。