多线程编程要使用的函数

创建线程

int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_function)(void *),void *restrict arg)
//成功返回0,其他值则出错
// 第一个参数为指向线程标识符的指针.第二个参数用来设置线程的属性,第三个参数是线程运行函数的起始位置,第四个参数是运行函数的参数

extern int pthread_join __p(pthread_t __th,void ** __thread_return);
//第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值.
//这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。如果执行成功,将返回0,如果失败则返回一个错误号。

C99新增restrict用于限定指针;该关键字用于告诉编译器,所有修改该指针所指向的内容的操作全部都是基于该指针的,即不存在其它进行修改操作的途径;这样可以帮助编译器进行更好的代码优化.生成更有效率的汇编代码.在gcc中使用C99标准要加上 -std=C99;
例如:void *memcpy( void * restrict dest ,const void * restrict src,sizi_t n) 这是一个很有用的内存复制函数,由于两个参数都加了restrict限定,所以两块区域不能重叠,即 dest指针所指的区域,不能让别的指针来修改,即src的指针不能修改. 相对应的别一个函数 memmove(void *dest,const void * src,size_t)则可以重叠。

  • 与fork()调用创建一个进程的方法不同,pthread_create()创建的线程并不具备和主线程(即调用pthread_create()的线程)同样的执行序列,而是使其运行start_routine(arg)函数。thread返回创建的线程ID,而 attr是创建线程时设置的线程属性。pthread_create()的返回值表示线程创建是否成功。尽管arg是void *类型的变量,但它同样可以作为任意类型的参数传给start_routine()函数;同时,start_routine()可以返回一个void *类型的返回值,而这个返回值也可以是其他类型,并由pthread_join()获取。
  • 为了设置attr属性,POSIX定义了一系列属性设置函数,包括pthread_attr_init()、pthread_attr_destroy()和与各个属性相关的pthread_attr_get---/pthread_attr_set---函数。

线程取消

通常线程会在主体函数退出的时候自动终止,但是也可以因为收到另外一个线程发来的终止(取消)请求而强制终止.线程取消的方法是向目标线程发送cancel信号,但是如何处理cancel信号则是由目标线程自己决定的,可以忽略,立即终止,或者是继续运行到cancelation-point(取消点),由不同的cancelation状态决定.线程收到CANCEL信号的缺省状态是运行到取消点(pthread_create()创建线程的缺省状态).
取消点包括以下一些函数:pthread_join(),pthread_testcancel(),pthread_condition_wait(),pthread_cond_timewait(),sem_wait(),sigwait()等等函数,以及read(),write()等会引起阻塞的系统调用都是取消点.而其他pthread函数都不会引起Cancelation动作。但是pthread_cancel的手 册页声称,由于LinuxThread库与C库结合得不好,因而目前C库函数都不是Cancelation-point;但CANCEL信号会使线程从阻 塞的系统调用中退出,并置EINTR错误码,因此可以在需要作为Cancelation-point的系统调用前后调用 pthread_testcancel(),从而达到POSIX标准所要求的目标,即如下代码段:

pthread_testcancel();
retcode = read(fd,buffer,length);
pthread_testcancel();
  • 若线程处于无限循环,且没有执行到取消点的必然路径,则线程无法由外部的其他线程取消请求而终止,因此在这样的循环体的必经路径上应该加入pthread_testcancel()调用.

  • 通过 int pthread_cancel(pthread_t tid);发送终止信号给thread线程,若成功则返回0,否则非0,成功发送并不意味着thread会终止.(那么检查pthread_cancel的返回状态不是没有意义么???)

  • 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_DEFFEREDPTHREAD_CANCEL_ASYCHRONOUS,仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出和 立即执行取消动作(退出);oldtype如果不为NULL则存入运来的取消动作类型值。

  • void pthread_testcancel(void)检查本线程是否处于Canceld状态,如果是,则进行取消动作,否则直接返回。


互斥锁

pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

  • pthread_mutex_t 是posix下抽象出的一个锁类型的结构:pthread_mutex_t.通过对该结构的访问.顾名思义,加锁以后,别人就无法打开,只有当锁没有关闭的时候才可以访问资源.使用互斥锁可以让线程按顺序执行.通常,互斥锁通过确保一次只有一个线程执行代码的临界段来同步多个线程.互斥锁还可以保护单线程代码.要更改缺省的互斥锁属性,可以对属性对象进行声明和初始化。通常,互斥锁属性会设置在应用程序开头的某个位置,以便可以快速查找和轻松修改。
  • pthread_mutex_init()函数是以动态方式创建互斥锁的,参数attr指定了新建互斥锁的属性。如果参数attr为NULL,则使用默认的互斥锁属性,默认属性为快速互斥锁 。互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。函数成功完成之后会返回0,其他的任何值表示出现错误.执行成功后,互斥锁被初始化为锁住的状态.

互斥锁的属性:

  1. PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
  2. PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
  3. PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
  4. PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
锁的一些操作
  1. int pthread_mutxe_lock(pthread_mutex_t *mutex)
  2. int phread_mutex_unlock(pthread_mutex_t *mutex)
  3. int pthread_mutex_trylock(pthread_mutex_t *mutex)

pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。

死锁

死锁主要发生在有多个依赖锁存在时, 会在一个线程试图以与另一个线程相反顺序锁住互斥量时发生. 如何避免死锁是使用互斥量应该格外注意的东西。总体来讲, 有几个不成文的基本原则:

  1. 对共享资源操作前一定要获得锁。
  2. 完成操作以后一定要释放锁。
  3. 尽量短时间地占用锁。
  4. 如果有多锁, 如获得顺序是ABC连环扣, 释放顺序也应该是ABC。
  5. 线程错误返回时应该释放它所获得的锁。

条件变量

#include <pthread.h>
pthread_cond_init(pthread_cond_t *cv, const pthread_condattr_t *attr);
//成功返回0,其他的值表示错误

不能由多个线程同时初始化一个条件变量。当需要重新初始化或释放一个条件变量时,应用程序必须保证这个条件变量未被使用。

int pthread_cond_wait(pthread_cond_t *cv, pthread_mutex_t *mutex);
//返回0表示成功,其他值表示失败

该函数将解锁mutex参数指向的互斥锁,并使当前线程阻塞在cv参数指向的条件变量上。被阻塞的线程可以被pthread_cond_signal函数,pthread_cond_broadcast函数唤醒,也可能在被信号中断后被唤醒。pthread_cond_wait函数的返回并不意味着条件的值一定发生了变化,必须重新检查条件的值。pthread_cond_wait函数返回时,相应的互斥锁将被当前线程锁定,即使是函数出错返回。一般一个条件表达式都是在一个互斥锁的保护下被检查。当条件表达式未被满足时,线程将仍然阻塞在这个条件变量上。当另一个线程改变了条件的值并向条件变量发出信号时,等待在这个条件变量上的一个线程或所有线程被唤醒,接着都试图再次占有相应的互斥锁。


pthread_cleanup_push()和pthread_cleanup_pop()的目的和作用

例如一个线程thread1:

pthread_mutex_lock(&mutex);
//一些会阻塞程序运行的调用,比如套接字的accept,等待客户连接
sock = accept(......);            //这里是随便找的一个可以阻塞的接口
pthread_mutex_unlock(&mutex);

上例中若线程1执行了accept(),线程会阻塞(也就是等在那里,有客户端连接的时候才返回,或则出现其他故障),线程等待中......
这时候线程2发现线程1等了很久,不赖烦了,他想关掉线程1,于是调用pthread_cancel()或者类似函数,请求线程1立即退出。这时候线程1仍然在accept等待中,当它收到线程2的cancel信号后,就会从accept中退出,然后终止线程,注意这个时候线程1还没有执行:pthread_mutex_unlock(&mutex);也就是说锁资源没有释放,这回造成其他线程的死锁问题。
所以必须在线程接收到cancel后用一种方法来保证异常退出(也就是线程没达到终点)时可以做清理工作(主要是解锁方面),pthread_cleanup_pushpthread_cleanup_pop就是用来做这样的工作的。

pthread_cleanup_push(some_clean_func,...)
pthread_mutex_lock(&mutex);
//一些会阻塞程序运行的调用,比如套接字的accept,等待客户连接
sock = accept(......);            //这里是随便找的一个可以阻塞的接口
pthread_mutex_unlock(&mutex);
pthread_cleanup_pop(0);
return NULL;

上面的代码,如果accept被cancel后线程退出,会自动调用some_clean_func函数,在这个函数中你可以释放锁资源。如果accept没有被cancel,那么线程继续执行,当pthread_mutex_unlock(&mutex);表示线程自己正确的释放资源了,而执行pthread_cleanup_pop(0);也就是取消掉前面的some_clean_func函数。接着return线程就正确的结束了。
push进去的函数可能在以下三个时机执行:

  1. 显示的调用pthread_exit();
  2. 在cancel点线程被cancel。
  3. pthread_cleanup_pop()的参数不为0时。

推荐阅读更多精彩内容