linux基础编程:多线程基础总结:线程创建 终止 线程同步/互斥量/条件变量/信号灯

看了很多关于linux下面多线程开发的博客和书籍,最后还是觉得杨沙洲在IBM developerWorks 中国发表的关于Posix线程编程的专栏差不多是最好的吧,本文就以该专栏来一步一步来学习多线程开发。


第一篇线程的创建与取消


相对进程而言,线程是一个更加接近于执行体的概念,与其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护。

线程的创建

Linux线程在核内是以轻量级进程的形式存在的,拥有独立的进程表项,而所有的创建、同步、删除等操作都在核外pthread库中进行。Linux的线程实现是在核外进行的,核内提供的是创建进程的接口do_fork()。do_fork()提供了很多参数,包括CLONE_VM(共享内存空间)、CLONE_FS(共享文件系统信息)、CLONE_FILES(共享文件描述符表)、CLONE_SIGHAND(共享信号句柄表)和CLONE_PID(共享进程ID,仅对核内进程,即0号进程有效)。当使用fork系统调用创建进程时,内核调用do_fork()不使用任何共享属性,进程拥有独立的运行环境,而使用pthread_create()来创建线程时,则最终设置了所有这些属性来调用do_fork(),从而创建的"进程"拥有共享的运行环境,只有栈是独立的。
 #include <pthread.h>
 int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg)
使用多线程时候,必须包含头文件pthread.h。并且在编译过程中需要使用-lpthread来链接线程库。第一个参数用来返回创建的线程的线程标识符。第二个参数为线程属性字段,一般设为NULL来使用默认值。第三个为线程的执行体,参数为void 指针,返回的也是void 指针。最后参数用来传递参数给指向体的参数。函数调用成功返回0,失败了,返回错误代码。

线程的注销

结束一个线程有两种方法:正常终止和非正常终止。
正常终止指的是在线程内部主动结束线程的执行。一般情况下,主体函数return退出的时候会自动终止;也可以在函数中通过调用pthread_exit函数终止执行,就如同进程在结束时调用exit函数。
#include <pthread.h>

void pthread_exit(void *retval);
int pthread_join(pthread_t thread, void **retval);

在线程内部通过pthread_exit终止线程的执行,并返回一个指向某个对象的指针(注意:绝对不能用它来返回一个指向局部变量的指针)。在“父线程”中通过pthread_join函数阻塞等待"子线程"执行结束,并且通过retval指针指向进程退出时候返回的对象的指针,执行成功时返回0,错误返回错误代码。

非正常终止指的是在线程外部通过向目标线程发Cancel信号,但如何处理Cancel信号则由目标线程自己决定,或者忽略、或者立即终止、或者继续运行至Cancelation-point(取消点),由不同的Cancelation状态决定,缺省处理是继续运行至取消点。

 #include <pthread.h>

int pthread_cancel(pthread_t thread) 
//发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着thread会终止。
int pthread_setcancelstate(int state, int *oldstate) 
//设置本线程对Cancel信号的反应,state有两种值:PTHREAD_CANCEL_ENABLE(缺省)和PTHREAD_CANCEL_DISABLE,分别表示收到信号后设为CANCLED状态和忽略CANCEL信号继续运行;old_state如果不为NULL则存入原来的Cancel状态以便恢复。
int pthread_setcanceltype(int type, int *oldtype) 
//设置本线程取消动作的执行时机,type由两种取值:PTHREAD_CANCEL_DEFFERED和PTHREAD_CANCEL_ASYCHRONOUS,仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出和立即执行取消动作(退出);oldtype如果不为NULL则存入运来的取消动作类型值。
void pthread_testcancel(void) 
//检查本线程是否处于Canceld状态,如果是,则进行取消动作,否则直接返回。

关于取消点在该文章中也描述的比较清楚:“根据POSIX标准,pthread_join()、pthread_testcancel()、pthread_cond_wait()、pthread_cond_timedwait()、sem_wait()、sigwait()等函数以及read()、write()等会引起阻塞的系统调用都是Cancelation-point,而其他pthread函数都不会引起Cancelation动作。但是pthread_cancel的手册页声称,由于LinuxThread库与C库结合得不好,因而目前C库函数都不是Cancelation-point;但CANCEL信号会使线程从阻塞的系统调用中退出,并置EINTR错误码,因此可以在需要作为Cancelation-point的系统调用前后调用pthread_testcancel(),从而达到POSIX标准所要求的目标”。

线程属性

在pthread_create函数中创建线程时候,通过第二个参数来指定线程的属性,线程属性是一个pthread_attr_t对象。对该对象的初始化的销毁都可以通过函数进行操作:
#include <pthread.h>

int pthread_attr_init(pthread_attr_t *attr);//对象初始化
int pthread_attr_destroy(pthread_attr_t *attr);//对象清理和回收
初始化一个线程对象以后,可以通过一系列的getter/setter进行操作该对象,拿脱离状态为例子格式如下:
 #include <pthread.h>

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);//设置属性
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);//获得属性
下面介绍个常见的属性:
detachedstate:设置线程和创建者之间是否可以通过pthread_join进行组合。有两种标志PTHREAD_CREATE_JOINABLE和PTHREAD_CREATE_DETACHED。默认为JOINABLE,即允许两个线程进行组合。如果为DETACHED,相当于创建一个“独立”的线程,不允许调用pthread_join来组合两个线程。
还有很多属性是针对线程调度优化。有需要再去翻阅吧。

第二篇线程的私有数据


在单线程程序中,我们经常要用到"全局变量"以实现多个函数间共享数据。在多线程环境下,由于数据空间是共享的,因此全局变量也为所有线程所共有。但有时应用程序设计中有必要提供线程私有的全局变量,仅在某个线程中有效,但却可以跨多个函数访问,比如程序可能需要每个线程维护一个链表,而使用相同的函数操作,最简单的办法就是使用同名而不同变量地址的线程相关数据结构。这样的数据结构可以由Posix线程库维护,称为线程私有数据(Thread-specific Data,或TSD)。
在linux中TSD变量标识为pthread_key_t,该类型的变量可以和const void *的指针相关联。对该变量的使用包括创建,使用和销毁几个步骤。
#include <pthread.h>

int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));//创建
int pthread_key_delete(pthread_key_t key);//销毁
void *pthread_getspecific(pthread_key_t key);//从key中获得相关联的指针
int pthread_setspecific(pthread_key_t key, const void *value);//设置key相关联的指针

下面是原文的一个简单例子:

#include <stdio.h>
#include <pthread.h>
pthread_key_t   key;
void echomsg(int t)
{
        printf("destructor excuted in thread %d,param=%d\n",pthread_self(),t);
}
void * child1(void *arg)
{
        int tid=pthread_self();
        printf("thread %d enter\n",tid);
        pthread_setspecific(key,(void *)tid);
        sleep(2);
        printf("thread %d returns %d\n",tid,pthread_getspecific(key));
        sleep(5);
}
void * child2(void *arg)
{
        int tid=pthread_self();
        printf("thread %d enter\n",tid);
        pthread_setspecific(key,(void *)tid);
        sleep(1);
        printf("thread %d returns %d\n",tid,pthread_getspecific(key));
        sleep(5);
}
int main(void)
{
        int tid1,tid2;
        printf("hello\n");
        pthread_key_create(&key,echomsg);
        pthread_create(&tid1,NULL,child1,NULL);
        pthread_create(&tid2,NULL,child2,NULL);
        sleep(10);
        pthread_key_delete(key);
        printf("main thread exit\n");
        return 0;
}

//给例程创建两个线程分别设置同一个线程私有数据为自己的线程ID,为了检验其私有性,程序错开了两个线程私有数据的写入和读出的时间,从程序运行结果可以看出,两个线程对TSD的修改互不干扰。同时,当线程退出时,清理函数会自动执行,参数为tid。


第三篇述线程同步


互斥锁

在Posix Thread中定义了另外一套专门用于线程同步的互斥锁,它由一个pthread_mutex_t 类型的所变量和一系列的互斥锁函数:主要包含创建,加锁,解锁和锁删除。
 #include <pthread.h>
//锁创建和初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//删除锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
从上面我们看到有两种方式来创建和初始化一个pthread_mutex_t类型的互斥锁:利用pthread_mutex_init()函数和利用结构常量PTHREAD_MUTEX_INITIALIZER。
对于pthread_mutex_init()第二个参数为创建的锁的属性,如果为NULL就使用默认属性。在linux中只有一个锁属性,即标识锁的类型,常见锁类型有:
  • PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
  • PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
  • PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
  • PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
pthread_mutex_destroy()用于注销一个互斥锁,即释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。
#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
锁操作主要包括加锁pthread_mutex_lock()、解锁pthread_mutex_unlock()和测试加锁pthread_mutex_trylock()三个,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。对于普通锁和适应锁类型,解锁者可以是同进程内任何线程;而检错锁则必须由加锁者解锁才有效,否则返回EPERM;对于嵌套锁,文档和实现要求必须由加锁者解锁,但实验结果表明并没有这种限制,这个不同目前还没有得到解释。在同一进程中的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁。
注意:POSIX线程锁机制的Linux实现都不是取消点,因此,延迟取消类型的线程不会因收到取消信号而离开加锁等待。值得注意的是,如果线程在加锁后解锁前被取消,锁将永远保持锁定状态,因此如果在关键区段内有取消点存在,或者设置了异步取消类型,则必须在退出回调函数(见第四篇的线程终止)中解锁。

条件变量

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
linux中提供了一个pthread_cond_t类型来标识一个条件变量和一套完整的条件变量函数:创建,等待,激活,注销
#include <pthread.h>

//创建初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//注销
int pthread_cond_destroy(pthread_cond_t *cond);
和互斥锁一样,对条件变量的初始化也有两种方式:利用pthread_cond_init函数和常量PTHREAD_COND_INITIALIZER。尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常为NULL,且被忽略。
注销一个条件变量需要调用pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。
 #include <pthread.h>

//等待
int pthread_cond_timedwait(pthread_cond_t *restrictcond,pthread_mutex_t *restrictmutex,const struct timespec *restrictabstime);
int pthread_cond_wait(pthread_cond_t *restrictcond,pthread_mutex_t *restrictmutex);
//激活
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
等待条件有两种方式:无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(),其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。

无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()的竞争条件。mutex互斥锁必须是普通锁或者适应锁,且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

这是博文的原话,不是很清楚,这样解释:

pthread_cond_wait是一个阻塞函数,函数传入的互斥量mutex是用于保护条件。因为我们在调用pthread_cond_wait时,如果条件不成立我们就进入阻塞,但是进入阻塞这个期间,如果条件变量改变了的话,那我们就漏掉了这个条件。因为这个线程还没有放到等待队列上,所以调用pthread_cond_wait前要先锁互斥量,即调用pthread_mutex_lock(),pthread_cond_wait在把线程放进阻塞队列后,自动对mutex进行解锁,使得其它线程可以获得加锁的权利。这样其它线程才能对临界资源进行访问并在适当的时候唤醒这个阻塞的进。当pthread_cond_wait返回的时候又自动给mutex加锁。pthread_cond_wait()的使用方法如下:

pthread_mutex_lock(&qlock);    /*lock*/
pthread_cond_wait(&qready, &qlock); /*block-->unlock-->wait() return-->lock*/
pthread_mutex_unlock(&qlock); /*unlock*/

激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。使用的方法:

pthread_mutex_lock(&qlock);    /*lock*/
pthread_cond_signal(&qready); 
pthread_mutex_unlock(&qlock); /*unlock*/

pthread_cond_wait()和pthread_cond_timedwait()都被实现为取消点,因此,在该处等待的线程将立即重新运行,在重新锁定mutex后离开pthread_cond_wait(),然后执行取消动作。也就是说如果pthread_cond_wait()被取消,mutex是保持锁定状态的,因而需要定义退出回调函数来为其解锁。

注意:pthread_cond_wait()和pthread_cond_timedwait()都被实现为取消点,因此,在该处等待的线程将立即重新运行,在重新锁定mutex后离开pthread_cond_wait(),然后执行取消动作。也就是说如果pthread_cond_wait()被取消,mutex是保持锁定状态的,因而需要定义退出回调函数来为其解锁。在原文有一个例子很好的解释了条件变量的使用。

信号灯

信号灯与互斥锁和条件变量的主要不同在于"灯"的概念,灯亮则意味着资源可用,灯灭则意味着不可用。如果说后两中同步方式侧重于"等待"操作,即资源不可用的话,信号灯机制则侧重于点灯,即告知资源可用;没有等待线程的解锁或激发条件都是没有意义的,而没有等待灯亮的线程的点灯操作则有效,且能保持灯亮状态。当然,这样的操作原语也意味着更多的开销。
信号灯的应用除了灯亮/灯灭这种二元灯以外,也可以采用大于1的灯数,以表示资源数大于1,这时可以称之为多元灯。
linux 中信号灯是定义在semaphore.h头文件中,每一个信号灯都是一个sem_t类型的变量,对信号灯的操作包括:创建/初始化,点灯/灭灯/获得灯的数目,销毁灯
#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
POSIX信号灯标准定义了有名信号灯和无名信号灯两种,但LinuxThreads的实现仅有无名灯,同时有名灯除了总是可用于多进程之间以外,在使用上与无名灯并没有很大的区别,因此下面仅就无名灯进行讨论。
sem_init()创建信号灯,其中value为信号灯的初值,pshared表示是否为多进程共享而不仅仅是用于一个进程。LinuxThreads没有实现多进程共享信号灯,因此所有非0值的pshared输入都将使sem_init()返回-1,且置errno为ENOSYS。初始化好的信号灯由sem变量表征,用于以下点灯、灭灯操作。

sem_destroy()被注销的信号灯sem要求已没有线程在等待该信号灯,否则返回-1,且置errno为EBUSY。除此之外,LinuxThreads的信号灯注销函数不做其他动作。

#include <semaphore.h>

//点灯
int sem_post(sem_t *sem);
//灭灯
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
//获得灯的值
int sem_getvalue(sem_t * sem, int * sval)
sem_post()点灯操作将信号灯值原子地加1,表示增加一个可访问的资源。

sem_wait()为等待灯亮操作,等待灯亮(信号灯值大于0),然后将信号灯原子地减1,并返回。sem_trywait()为sem_wait()的非阻塞版,如果信号灯计数大于0,则原子地减1并返回0,否则立即返回-1,errno置为EAGAIN。sem_timedwait加了等待时间。

sem_getvalue()读取sem中的灯计数,存于*sval中,并返回0。

注意:sem_wait()被实现为取消点。

第四篇线程中止


在第一篇中,我们介绍了线程了两种退出方式:正常终止和非正常终止。我们知道这两种方式都会存在资源释放的问题,在不考虑因运行出错而退出的前提下,如何保证线程终止时能顺利的释放掉自己所占用的资源,特别是锁资源,就是一个必须考虑解决的问题。(最经常出现的情形是资源独占锁的使用:线程为了访问临界资源而为其加上锁,但在访问过程中被外界取消,如果线程处于响应取消状态,且采用异步方式响应,或者在打开独占锁以前的运行路径上存在取消点,则该临界资源将永远处于锁定状态得不到释放。外界取消操作是不可预见的,因此的确需要一个机制来简化用于资源释放的编程。

在POSIX线程API中提供了一个pthread_cleanup_push()/pthread_cleanup_pop()函数对用于在线程内部自动释放资源--从pthread_cleanup_push()的调用点到pthread_cleanup_pop()之间的程序段中的终止动作(包括调用pthread_exit()和取消点终止)都将执行pthread_cleanup_push()所指定的清理函数。API定义如下:
#include <pthread.h>

void pthread_cleanup_push(void (*routine)(void *),void *arg);//
void pthread_cleanup_pop(int execute);
pthread_cleanup_push()/pthread_cleanup_pop()采用先入后出的栈结构管理,void routine(void *arg)函数在调用pthread_cleanup_push()时压入清理函数栈,多次对pthread_cleanup_push()的调用将在清理函数栈中形成一个函数链,在执行该函数链时按照压栈的相反顺序弹出。execute参数表示执行到pthread_cleanup_pop()时是否在弹出清理函数的同时执行该函数,为0表示不执行,非0为执行;这个参数并不影响异常终止时清理函数的执行。
pthread_cleanup_push()/pthread_cleanup_pop()是以宏方式实现的。在宏定义中,pthread_cleanup_push()带有一个"{",而pthread_cleanup_pop()带有一个"}",因此这两个函数必须成对出现,且必须位于程序的同一级别的代码段中才能通过编译。在下面的例子里,当线程在"do some work"中终止时,将主动调用pthread_mutex_unlock(mut),以完成解锁动作。
pthread_cleanup_push(pthread_mutex_unlock, (void *) &mut);
pthread_mutex_lock(&mut);
/* do some work */
pthread_mutex_unlock(&mut);
pthread_cleanup_pop(0);
必须要注意的是,如果线程处于PTHREAD_CANCEL_ASYNCHRONOUS状态,上述代码段就有可能出错,因为CANCEL事件有可能在pthread_cleanup_push()和pthread_mutex_lock()之间发生,或者在pthread_mutex_unlock()和pthread_cleanup_pop()之间发生,从而导致清理函数unlock一个并没有加锁的mutex变量,造成错误。因此,在使用清理函数的时候,都应该暂时设置成PTHREAD_CANCEL_DEFERRED模式。为此,POSIX的Linux实现中还提供了一对不保证可移植的pthread_cleanup_push_defer_np() /  pthread_cleanup_pop_defer_np()扩展函数,功能与以下代码段相当:
{ 
int oldtype;
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &oldtype);
pthread_cleanup_push(routine, arg);
...
pthread_cleanup_pop(execute);
pthread_setcanceltype(oldtype, NULL);
}

第五篇线程杂项函数

获得本线程ID
#include <pthread.h>

pthread_t pthread_self(void);
本函数返回本线程的标识符。在LinuxThreads中,每个线程都用一个pthread_descr结构来描述,其中包含了线程状态、线程ID等所有需要的数据结构,此函数的实现就是在线程栈帧中找到本线程的pthread_descr结构,然后返回其中的p_tid项。pthread_t类型在LinuxThreads中定义为无符号长整型。

判断两个线程是否为同一线程

#include <pthread.h>

int pthread_equal(pthread_t t1, pthread_t t2);
判断两个线程描述符是否指向同一线程。在LinuxThreads中,线程ID相同的线程必然是同一个线程,因此,这个函数的实现仅仅判断thread1和thread2是否相等。

仅执行一次的操作
#include <pthread.h>

int pthread_once(pthread_once_t *once_control,void (*init_routine)(void));
pthread_once_t once_control = PTHREAD_ONCE_INIT;
本函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证init_routine()函数在本进程执行序列中仅执行一次。
#include <stdio.h>
#include <pthread.h>
pthread_once_t once=PTHREAD_ONCE_INIT;
void once_run(void)
{
        printf("once_run in thread %d\n",pthread_self());
}
void * child1(void *arg)
{
        int tid=pthread_self();
        printf("thread %d enter\n",tid);
        pthread_once(&once,once_run);
        printf("thread %d returns\n",tid);
}
void * child2(void *arg)
{
        int tid=pthread_self();
        printf("thread %d enter\n",tid);
        pthread_once(&once,once_run);
        printf("thread %d returns\n",tid);
}
int main(void)
{
        int tid1,tid2;
        printf("hello\n");
        pthread_create(&tid1,NULL,child1,NULL);
        pthread_create(&tid2,NULL,child2,NULL);
        sleep(10);
        printf("main thread exit\n");
        return 0;
}

//once_run()函数仅执行一次,且究竟在哪个线程中执行是不定的,尽管pthread_once(&once,once_run)出现在两个线程中。LinuxThreads使用互斥锁和条件变量保证由pthread_once()指定的函数执行且仅执行一次,而once_control则表征是否执行过。如果once_control的初值不是PTHREAD_ONCE_INIT(LinuxThreads定义为0),pthread_once()的行为就会不正常。在LinuxThreads中,实际"一次性函数"的执行状态有三种:NEVER(0)、IN_PROGRESS(1)、DONE(2),如果once初值设为1,则由于所有pthread_once()都必须等待其中一个激发"已执行一次"信号,因此所有pthread_once()都会陷入永久的等待中;如果设为2,则表示该函数已执行过一次,从而所有pthread_once()都会立即返回0。

你可能感兴趣的:(linux基础编程:多线程基础总结:线程创建 终止 线程同步/互斥量/条件变量/信号灯)