线程管理器(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)
int PINT_thread_mgr_JOBTYPE_start(void) { int ret = -1; gen_mutex_lock(&JOBTYPE_mutex); if(JOBTYPE_thread_ref_count > 0) { /* nothing to do, thread is already started. Just increment * reference count and return */ JOBTYPE_thread_ref_count++; gen_mutex_unlock(&JOBTYPE_mutex); return(0); } /* if we reach this point, then we have to start the thread ourselves */ ret = JOBTYPE_open_context(/*...*/ &global_JOBTYPE_context); // ...... JOBTYPE_thread_running = 1; JOBTYPE_thread_ref_count++; #ifdef __PVFS2_JOB_THREADED__ ret = pthread_create(&JOBTYPE_thread_id, NULL, JOBTYPE_thread_function, NULL); // ...... #endif gen_mutex_unlock(&JOBTYPE_mutex); return(0); }
此函数用于开启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)
int PINT_thread_mgr_JOBTYPE_getcontext(PVFS_context_id *context) { gen_mutex_lock(&JOBTYPE_mutex); if(JOBTYPE_thread_ref_count > 0) { *context = global_JOBTYPE_context; gen_mutex_unlock(&JOBTYPE_mutex); return(0); } gen_mutex_unlock(&JOBTYPE_mutex); return(-PVFS_EINVAL); }
顾名思义,这一组函数用于提供对应任务类型的全局上下文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循环中,主要完成了两件事情:
由于函数体较长,不再集中到函数后引用行数解说,而是将解说直接镶嵌在注释中,如下。
函数3. bmi_thread_function
static void *bmi_thread_function(void *ptr) { int ret = -1; int quick_flag = 0; int incount, outcount; int i=0; int test_timeout = thread_mgr_test_timeout; struct PINT_thread_mgr_bmi_callback *tmp_callback; #ifdef __PVFS2_JOB_THREADED__ PINT_event_thread_start("BMI"); while (bmi_thread_running) #endif { /* 1. 检测和处理意外(unexpected)消息 */ gen_mutex_lock(&bmi_mutex); if(bmi_unexp_count) //bmi_unexp_count为全局变量,用于计数待处理的意外消息。 //每次调用PINT_thread_mgr_bmi_unexp_handler注册意外消息处理函数时,都会增加该计数变量。 { incount = bmi_unexp_count; //待处理的意外消息数 if(incount > THREAD_MGR_TEST_COUNT) incount = THREAD_MGR_TEST_COUNT; //每次至多处理特定数量的意外消息 gen_mutex_unlock(&bmi_mutex); ret = BMI_testunexpected( incount, &outcount, stat_bmi_unexp_array, 0 ); //outcount获得本次调用实际处理的意外消息数 if (outcount > 0) //如果有意外消息到达并得到处理 { gen_mutex_lock(&bmi_mutex); for (i = 0; i < outcount; i++) //对所有处理完的意外消息调用回调函数 { bmi_unexp_fn(&stat_bmi_unexp_array[i]); //全局变量bmi_unexp_fn保存用于处理意外消息的函数句柄 //该句柄通过调用PINT_thread_mgr_bmi_unexp_handler进行注册 bmi_unexp_count--; //处理完成后减少待处理的意外消息计数 } gen_mutex_unlock(&bmi_mutex); } //如果接收并处理的意外消息数已经达到本次可检测的最大数目,则设定第二部分检测时不再等待 if (outcount == THREAD_MGR_TEST_COUNT) quick_flag = 1; } else { gen_mutex_unlock(&bmi_mutex); } //设定进一步检测上下文时等待的时间 if(quick_flag) { quick_flag = 0; test_timeout = 0; } else { test_timeout = thread_mgr_test_timeout; } //获得bmi_test_mutex锁,与函数PINT_thread_mgr_bmi_cancel互斥 gen_mutex_lock(&bmi_test_mutex); #ifdef __PVFS2_JOB_THREADED__ while (bmi_test_cancel_waiter) { //全局变量,记录等待中的取消BMI操作的线程数 //当有取消操作的线程在等待时,释放bmi_test_mutex锁,以允许取消操作进行,直到条件变量被触发 pthread_cond_wait(&bmi_test_cond, &bmi_test_mutex); } #endif bmi_test_flag = 1; //用于标识处于检测环节,以下与函数PINT_thread_mgr_bmi_cancel中的取消操作互斥 gen_mutex_unlock(&bmi_test_mutex); //释放bmi_test_mutex锁,允许增加bmi_test_cancel_waiter的操作 /* 2. 检测和处理全局上下文(ID=global_bmi_context)中未处理的其他BMI操作 */ incount = THREAD_MGR_TEST_COUNT; bmi_test_count = 0; memset(stat_bmi_user_ptr_array, 0, (THREAD_MGR_TEST_COUNT * sizeof(void *))); ret = BMI_testcontext(incount, stat_bmi_id_array, &bmi_test_count, stat_bmi_error_code_array, stat_bmi_actual_size_array, stat_bmi_user_ptr_array, test_timeout, global_bmi_context); //bmi_test_count获得该上下文中已完成的BMI操作数 //获得bmi_test_mutex锁,触发一个在等待中的取消BMI操作的线程 gen_mutex_lock(&bmi_test_mutex); bmi_test_flag = 0; //表明不在检测环节,允许函数PINT_thread_mgr_bmi_cancel中的取消操作 #ifdef __PVFS2_JOB_THREADED__ pthread_cond_signal(&bmi_test_cond); //向条件变量bmi_test_cond发出信号,触发等待线程 #endif gen_mutex_unlock(&bmi_test_mutex); //释放bmi_test_mutex锁 //...... for(i = 0; i < bmi_test_count; i++) //对所有已完成的BMI操作调用回调函数 { tmp_callback = (struct PINT_thread_mgr_bmi_callback*) stat_bmi_user_ptr_array[i]; //...... tmp_callback->fn(tmp_callback->data, stat_bmi_actual_size_array[i], stat_bmi_error_code_array[i]); } } //while #ifdef __PVFS2_JOB_THREADED__ PINT_event_thread_stop(); #endif return (NULL); }
除了注释之外,还要特别说明的是对互斥锁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
int PINT_thread_mgr_bmi_unexp_handler( void (*fn)(struct BMI_unexpected_info* unexp)) { gen_mutex_lock(&bmi_mutex); if(bmi_unexp_count > 0 && fn != bmi_unexp_fn) { gossip_lerr("Error: bmi_unexp_handler already set./n"); gen_mutex_unlock(&bmi_mutex); return(-PVFS_EALREADY); } bmi_unexp_fn = fn; bmi_unexp_count++; //每次设置意外消息处理句柄时增加意外消息数目 gen_mutex_unlock(&bmi_mutex); return(0); }
该函数用于注册处理意外消息的函数句柄,也就是该类BMI操作结束时的回调函数。进行的操作主要是第11行,完成对全局变量bmi_unexp_fn的赋值。该函数在张贴接受意外消息的任务时被调用(在src/io/job/job.c文件定义的函数job_bmi_unexp中调用),所以每次会增加待处理的意外消息数(第12行);同时,需要保证每次通过该函数注册的回调函数相同,这个判断和错误处理在第5~10行完成。
此类操作主要用于撤销尚未执行回调函数的BMI操作。
函数5. PINT_thread_mgr_bmi_cancel
int PINT_thread_mgr_bmi_cancel(PVFS_id_gen_t id, void* user_ptr) { int i; int ret; /* wait until we can guarantee that a BMI_testcontext() is not in * progress */ gen_mutex_lock(&bmi_test_mutex); //与函数bmi_thread_function所在线程争抢该锁 ++bmi_test_cancel_waiter; //全局变量,计数在等待状态的取消操作的线程 //首先将本线程视为等待状态,增加等待线程数 while(bmi_test_flag == 1) //当其他线程在进行检测环节时 { #ifdef __PVFS2_JOB_THREADED__ //等待检测线程触发条件变量bmi_test_cond pthread_cond_wait(&bmi_test_cond, &bmi_test_mutex); #endif } //直到没有其他线程进行检测时,取消本线程作为等待状态,开始执行取消操作 --bmi_test_cancel_waiter; //首先判断是否有取消的必要或可能 for(i = 0; i < bmi_test_count; i++) { if(stat_bmi_id_array[i] == id && stat_bmi_user_ptr_array[i] == user_ptr) { //如果要取消的操作已经完成且回调函数相同,则不再执行取消,直接返回 gen_mutex_unlock(&bmi_test_mutex); return(0); } } //实际调用下层取消操作 ret = BMI_cancel(id, global_bmi_context); //...... #ifdef __PVFS2_JOB_THREADED__ //唤醒等待状态的bmi_thread_function线程,或其他取消操作的线程 pthread_cond_signal(&bmi_test_cond); #endif gen_mutex_unlock(&bmi_test_mutex); return(ret); }
主要逻辑参见注释。第23行判断要取消的操作是否已经完成,已完成的操作及其回调函数记录在全局变量stat_bmi_id_array和stat_bmi_user_ptr_array中(见函数3的第71~73行,由BMI_testcontext函数的输出参数赋值)。
下面我们重点看一下互斥锁bmi_test_mutex和条件变量bmi_test_cond的使用,以及函数3和函数5的执行关系。
首先罗列所有的互斥段如下:
注意函数调用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)
int PINT_thread_mgr_JOBTYPE_stop(void) { gen_mutex_lock(&JOBTYPE_mutex); JOBTYPE_thread_ref_count--; if(JOBTYPE_thread_ref_count <= 0) { assert(JOBTYPE_thread_ref_count == 0); /* sanity check */ JOBTYPE_thread_running = 0; #ifdef __PVFS2_JOB_THREADED__ gen_mutex_unlock(&JOBTYPE_mutex); pthread_join(JOBTYPE_thread_id, NULL); #endif JOBTYPE_close_context(/*...*/ global_JOBTYPE_context); } else { gen_mutex_unlock(&JOBTYPE_mutex); } return(0); }
回顾线程启动的函数,实际只有一个线程在运行,其他启动调用只是增加引用计数JOBTYPE_thread_ref_count;所以线程终止时,首先减少引用计数(第4行),如果还有其他引用则无需其他操作,而当引用为零时,说明此线程可以被销毁,执行实际线程终止操作(第11行),并关闭全局上下文(第13行)。注意dev没有close_context操作。