Unix线程

线程创建与关闭

  在Unix操作系统中,一个进程可以同时运行多个线程,线程由线程ID、寄存器值集合、栈、信号掩码(signal mask)、错误标示符(errno)、线程私有数据构成。同一进程中的多个线程共享文本片段、程序全局存储区、堆存储区、栈存储区、文件描述符。

  线程的创建可以使用pthread_create函数,其函数原型为

#include <pthread.h>

int pthread_create(pthread_t *tidp, const pthread_attr_t *attr, void *(*start_rtn) (void *),  void *arg);

  参数tidp用于存储线程ID,参数attr标明线程属性。当线程启动时,线程函数start_rtn被调用,其参数为art。如果需要向线程函数中传递多个参数,可以将参数存储于结构体中。新建线程将继承调用线程的信号掩码,并清空悬而未决的信号。与创建进程类似,我们无法确保子线程与主线程的调用顺序。

  线程属性由结构体pthread_attr_t定义,在函数pthread_create中使用。使用pthread_attr_t结构体变量前需要通过pthread_attr_init函数初始化,使用结束后需要通过pthread_attr_destory函数释放其所占用的资源。pthread_attr_t结构体对程序是不透明的,我们仅能够使用有限的函数来对线程属性进行操作,而不能通过直接修改结构体中相关域来修改线程属性。与线程属性相关的函数定义及用途如表1所示

函数原型 用途
int pthread_attr_getdetachstate( const pthread_attr_t *attr, int *detachstate); 获取线程分离状态,PTHREAD_CREATE_DETACHED 或 PTHREAD_CREATE_JOINABLE
int pthread_attr_setdetachstate( pthread_attr_t *attr, int detachstate); 设置线程分离状态,PTHREAD_CREATE_DETACHED 或 PTHREAD_CREATE_JOINABLE
int pthread_attr_getstack( const pthread_attr_t *attr, void **stackaddr, size_t *stacksize); 获取线程栈地址
int pthread_attr_setstack( pthread_attr_t *attr, void *stackaddr, size_t *stacksize); 设置线程栈地址
int pthread_attr_getstacksize( const pthread_attr_t *attr, size_t *stacksize); 获取线程栈大小
int pthread_attr_setstacksize( pthread_attr_t *attr, size_t stacksize); 设置线程栈大小
int pthread_attr_getguardsize( const pthread_attr_t *attr, size_t *guardsize); 获取栈溢出检查值大小,当值为0时,系统不检查栈溢出状态
int pthread_attr_setguardsize( pthread_attr_t *attr, size_t guardsize); 设置栈溢出检查值大小,当值为0时,系统不检查栈溢出状态
int pthread_getconcurrency(void); 获取线程最大并行数
int pthread_setconcurrency(int level); 设置线程最大并行数

表1.  线程属性相关函数

  如果需要退出线程,可以在线程处理函数中return,或者调用pthread_exit函数。如果一个线程对另一个线程执行pthread_cancel操作,且该线程没有忽略cancel状态,则该线程被取消,退出该线程。pthread_cleanup_push函数可以建立栈并将指定函数入栈,栈中的函数将在线程异常退出时被调用。相应的出栈函数为pthread_cleanup_pop。这里的异常退出指:

  • 调用pthread_exit函数
  • 回复cancel操作
  • 调用pthread_cleanup_pop函数,参数为非零值

  线程可以通过调用pthread_setcancelstate函数来指定接收到cancel命令时的行为,其值可以为PTHREAD_CANCEL_ENABLE(立即处理)或PTHREAD_CANCEL_DISABLE(不立即处理,该消息悬而未决)。调用pthread_testcancel函数时,如果当前状态有cancel消息悬而未决,则线程立即退出,否则不产生任何影响。

  如果一个线程需要等待另一个线程结束,可以使用pthread_join函数。其原型为:

int pthread_join(pthread_t thread, void **rval_ptr);

 

  被等待的线程线程结束时,线程返回值保存在参数rval_ptr指定的内存单元中。调用pthread_join函数时,线程将被分离(detached),线程结束时,线程终止信息(thread's termination status)将被立即收回。pthread_detach函数可以将指定线程分离。

  

线程同步

  线程的同步通过互斥加锁操作来实现。互斥操作又分为互斥锁(mutexes)、读写锁(Read-Write Locks)、条件锁(Condition Variables)。锁可以是递归锁,也可以是非递归锁,这一点由锁的属性决定。当一个线程多次试图对同一个非递归锁加锁时,会出现死锁现象。线程将无限期的阻塞下去。我们可以使用trylock系列函数尝试加锁,当加锁失败时,该函数并不会进入阻塞状态,能够避免死锁的出现。

  与锁有关的函数具有统一的命名规则,便于记忆与使用。相关函数原型为

#include <pthread.h>

//初始化系列
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

//销毁系列
int pthread_mutex_destory(pthread_mutex_t *mutex);
int pthread_rwlock_destory(pthread_rwlock_t *mutex);
int pthread_cond_destory(pthread_cond_t *mutex);

//加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_rwlock_rdlock(pthread_rwlock_t *mutex);
int pthread_rwlock_wrlock(pthread_rwlock_t *mutex);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_rwlock_unlock(pthread_rwlock_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

//非阻塞加锁,加锁成功时返回0,失败时返回非0值。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *mutex);
int pthread_rwlock_trywrlock(pthread_rwlock_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex);

 

  读写锁中的读锁可以被多次加锁,而不产生阻塞。写锁仅能被加锁一次,当写锁已经被加锁时,再次执行加锁操作将导致阻塞。使用条件锁之前需要先定义一个互斥锁,并对互斥锁加锁。条件锁被加锁时,锁定的互斥锁可以保证条件的改变一定发生在条件锁加锁后,以避免出现先改变条件,后加锁的情况。条件锁加锁成功后,互斥锁仍然为已锁定状态。pthread_cond_signal函数可以唤醒一个被指定条件锁阻塞的线程,pthread_cond_broadcast函数将唤醒全部被指定条件锁阻塞的线程。

  线程锁既可以用于同步线程,也可以用于同步进程。同步进程时,需要指定锁属性为PTHREAD_PROCESS_SHARED。相关函数原型为

//获取共享状态,线程共享或进程共享
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *pshared);
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_condattr_getpshared(const pthread_condattr_t *attr, int *pshared);

//设置共享状态
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);

 

  可以通过向pthread_mutexattr_settype函数传递PTHREAD_MUTEX_RECURSIVE参数来指定互斥锁为递归锁。递归锁可以在同一线程中被加锁多次,但相应的,递归锁必须被解锁同样次数。递归锁仅适用于一些特殊情况,如果有其他方案,应当尽量避免使用递归锁。函数原型为

int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

 

  如果一个函数可以在多个线程中被同时调用,而保持数据正确,则称该函数为线程安全的。一般而言,如果函数中需要向静态存储空间中保存数据,则该函数非线程安全函数。

  实际应用中,线程可能需要动态申请一段内存空间,将其中数据设置为线程私有,这时可以使用pthread_setspecific函数。在调用pthread_setspecific之前,需要先建立一个连接点,类型为pthread_key_t。堆中的私有数据空间与key相关联。相关函数原型为

int pthread_key_create(pthread_key_t *keyp, void (*destructor) (void*));
int pthread_key_delete(pthread_key_t *key);
int pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void *value);

 

当pthread_getspecific函数返回NULL时,堆中没有与key相关联的内存单元。

  如果某一函数需要在该进程的所有线程中仅被执行一次,可使用pthread_once函数。其原型为

pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag, void (*initfn) (void));

 

线程的使用

  每一个线程具有不同的信号掩码(signal mask)。进程中产生一个信号后,该信号将被发送至任意一个没有阻塞该信号的线程。当需要在线程中设置信号掩码时,可以使用pthread_sigmask函数,其原型与sigprocmask函数类似。

  当线程需要等待信号时,可以使用sigwait函数,其原型为

int sigwait(const sigset_t *set, int *signop);

 

参数set指定线程希望等待的信号,当接收到信号时,参数signop为接收到的信号ID。

  当一个线程需要向指定线程发送信号时,可以使用pthread_kill函数,其原型为

int pthread_kill(pthread_t thread, int signo);

 

  如果在一个线程中创建了一个新的进程,该子进程将复制父进程中的所有数据,子进程中,只包含一个线程,该线程由创建进程的线程复制而来。由于子进程包含父进程的所有数据,而子进程无法知道哪些锁已被锁定。因此,子进程需要释放已锁定的锁锁占用的系统资源。pthrea_atfork函数可以指定线程调用fork函数前后,父进程与子进程必须执行的操作。其函数原型为

int pthread_atfork( void (*prepare) (void), void (*parent) (void), void (*child) (void));

 

prepare函数将在fork执行前调用,fork执行后,父进程调用parent函数,子进程调用child函数。我们可以在这里为子进程清除父进程锁使用的锁。

 

posted @ 2012-09-10 02:39  o0慢节奏0o  阅读(1446)  评论(0编辑  收藏  举报