Apue.2e Chapter1112 Thread

这两章讲诉线程和线程控制的相关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即可。保证偏移和写入的原子性。

原文地址:https://www.cnblogs.com/livewithnorest/p/2908897.html