这两章讲诉线程和线程控制的相关api。
为什么要有线程呢,因为线程更快,并发操作操作资源更简单。
注:线程系函数多半并不设置errno,而是直接返回错误码。
线程独立的资源:线程ID,一组寄存器、栈、调度优先级、策略、信号屏蔽字、errno和private data;
线程ID:pthread_t,实现可以用一个结构来表示,因此不能直接操作,需要用
#include <pthread.h>
int pthread_equal(pthread_t tid1,pthread_t tid2);
来比较是否相等(非0)。
线程可以通过
pthread_t pthread_self(void);获得自身的tid;
创建线程:
int pthread_create(pthread_t *restrict tidp, const pthread_t attr_t *restrict attr,
void *(*start_rtn)(void *),void *restrict arg);
attr是所创建线程的属性,可通过
int pthread_attr_init(pthread_attr_t attr);来初始化,然后通过
int pthread_attr_get/setdetachstate来获取/设置以分离状态启动线程。[分离状态:不再等待线程的终结状态,操作系统会自动回收资源(类似于孤儿进程)]
通过int pthread_attr_get/setstack来获取/设置栈属性,包括栈大小和栈的最低地址(不一定是起始地址,取决于增长方向),如果仅修改大小,使用pthread_attr_get/setstacksize;
使用int pthread_attr_get/setguardsize来获取/设置警戒缓冲区大小(避免栈溢出的扩展内存大小)
以上都是通过改变pthread_attr_t结构来实现的,还有几个属性是通过函数直接设置的,包括:
可取消选项:int pthread_setcancelstate(int state,int *oldstate),注意即使设置为PTHREAD_CANCEL_ENABLE,线程也不一定会立刻取消,必须通过pthread_setcanceltype(int type, int *oldtype)来设置其取消类型为PTHREAD_CANCEL_AYSNCHRONOUS(异步取消),才会立刻取消;否者默认为延迟取消,必须在合适的取消点才会取消线程函数,可以通过pthread_testcancel来自定义取消点。
并发度:int pthread_get/setconcurrency,如果操作系统控制并发度,get返回0;set参数为0取消设置,其他值用来设置自己希望的并发度。
注意:线程创建api,书中Linux2.4是使用clone system call实现的,这个实现方式被称为Linux Threads,已经过时,在2.6+ kernel中,实现方式改为NPTL,该实现是符合Posix.1兼容性需求的。
线程终止:
除了正常返回外,可以通过
void pthread_exit(void *rval_ptr);
退出线程函数。参数可以被
int pthread_join(pthread_t thread, void **rval_ptr);
获得。后者会阻塞线程直到等待的线程退出(rval指向返回数据)或被取消(*rval==PTHREAD_CANCELED),其行为类似waitpid。
如果线程已经被置于分离状态,调用失败,返回EINVAL。
取消线程可以通过
int pthread_cancel(pthread_t tid);来完成,效果同调用了参数为PTHREAD_CANCELED的pthread_exit函数。
可以注册线程清理程序(类似进程的atexit),对应函数为
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);
以非0参数调用后者,可以主动触发前者注册的函数;线程非正常结束时,会自动弹出(类似栈行为)注册的函数,并以参数调用。
由于两者可以被实现为宏,所以最好配对使用(在同一作用域)。
线程同步:
Unix线程同步的工具与Windows大体一致(比win32少一些,也不区分用户模式和内核模式),主要包括互斥量(mutex),读写锁(RW lock)和条件变量,至于信号量、消息队列和管道等属于进程间通信方式,将在chapter15学习。
可以通过
int phread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int phread_mutex_destroy(pthread_mutex_t *restrict mutex);
来创建和销毁互斥量。
attr是互斥量的属性,主要包括进程共享(int pthread_mutexattr_get/setshared)和类型(int pthread_mutexattr_get/settype)。前者可以将互斥量设置为进程间共享,从而使互斥量用于进程间通信;后者可将互斥量设置为可递归的|包含错误检查的|正常的这三种模式,可递归允许多次获得锁,对应着多次的unlock,在某些时候比较有用。
可以使用
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
来加锁/尝试加锁/解锁;trylock如果失败,返回EBUSY
互斥量经常和引用计数技术同时使用。
如果互斥量是非递归的,已经获得锁的情况下尝试再次获得锁,会导致死锁。
将锁嵌入结构,是多线程环境下常用的一种解决方案。
读写锁:
即共享/独占锁。
使用
int pthread_rwlock_init(phread_rwlock_t *restrict rwlock, const phread_rwlockattr_t *attr);
int pthread_rwlock_destroy(phread_rwlock_t *restrict rwlock);
来初始化/销毁一个读写锁。
使用
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
来加读/写锁或解锁。
也有对应的tryrdlock/trywrlock版本,返回值类似mutex;
属性:只支持进程共享属性,函数也是类似的get/setshared格式。
条件变量:
条件变量与互斥量一起使用时,允许多线程以无竞争状态等待某一条件的发生。
以
int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
来初始化/销毁条件变量。
以
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timewait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex,
const struct timespec *restrict timeout);
来等待/超时等待条件变量的发生。
调用者将锁住的条件变量传递给wait函数,后者将其放在等待条件的线程列表中,并对其解锁(原子操作),返回后,会自动再次加锁。
超市等待的时间是绝对值(即:明确的几点几分),这点要注意。
通知条件准备完成,可以用:
int pthread_cond_signal(pthread_cond_t *comd);
int pthread_cond_broadcast(pthread_cond_t *comd);
内核会以不确定的方式发送给各个等待条件的线程(如果条件的属性被设置为进程共享,那么也会通知其他进程的等待线程)。
重入:
多个线程可以同时安全地调用的函数,就是所谓的线程安全函数。
非线程安全的函数,系统可能有些线程安全版本(_r)。
线程安全和信号处理函数可重入不完全一致,后者的要求更严格一些(异步信号安全)。
标准流FILE结构,可以使用对应的操作函数来保证线程安全,注意由于字符处理函数的多线程版效率太低,因此额外提供了解锁版。
线程私有数据:
即线程本地化数据,通过创建"键"来关联线程私有数据:
int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));
第二个参数是析构函数,其参数是需要析构的数据的地址(系统自动添加);
删除使用:
int pthread_key_delete(pthread_key_t *keyp);
注意调用此函数不会自动激活析构。
在多个线程中共需的数据,可以使用
pthread_once_t initflag=PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag, void (*initfnt)(void));
来初始化该数据,initflag必须是非local变量;initfnt是初始化的函数;
使用key时常用的上pthread_once函数。
检查或关联私有数据需要使用函数:
void *pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void *value);
线程和信号:
在线程中处理信号,不能使用sigprocmask,而要用:
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
来设置屏蔽字,然后使用
int sigwait(const signet_t *restrict set, int *restrict signop);
来等待信号(signop作为返回值,表明发送信号数量)。为避免错误,记得在调用sigwait之前阻塞需要等待的信号。
常用的处理方案:在某个线程中专门等待某个信号,在其他线程中屏蔽该信号,这样就有了一个异步的专用的信号处理线程;
如果在主线程中使用sigaction/signal函数注册了信号处理函数,在某线程中又调用了sigwait等待该信号,内核以不确定的方式将信号发往其中某一个函数。
线程和fork:
fork后,只存在调用fork的线程的副本,其他线程的相关锁必须手动清除。但是如果fork后立刻调用exec系,就不必管它。
处理函数注册:
int pthread_atfork( void (*prepare)(void),void (*parent)(void),void (*child)(void));
prepare在fork前调用,目的是获得父进程的所有锁;
parent在父进程fork返回前调用,目的是解锁prepare中的所有锁;
child在子进程fork返回前调用,目的是解锁子进程副本中prepare的所有锁。
线程和I/O:
记得pread和pwrite即可。保证偏移和写入的原子性。