十一章 线程
- 多线程可以实现在某一个时刻能够做不止一件事,每个线程处理各自独立的任务
- 好处如下
- 通过为每种事件类型分配不单独的处理线程,可以简化处理异步事件代码
- 多线程可以共享存储地址空间和文件描述符
- 有些问题可以分解从而提高整个程序的吞吐量
- 交互的程序可以使用多线程来改善响应时间。
- 多线程在执行串行化任务时会阻塞
- 每个线程包含有表示执行环节所必须的信息,其中包括
- 进程中标识线程的线程ID
- 一组寄存器值,栈,调度优先级和策略
- 信号屏蔽字
- errno变量
- 以及线程私有数据。
- 进程的所有信息对改进程的所有线程都是共享的,包括可执行程序的代码,程序的全局内存和堆内存,栈,文件描述符。
11.3 进程标识
- 每个线程有一个线程id,线程id可能不是唯一的,只有在它所属的进程上下文中才有意义
- 线程id使用一个pthead_t数据类型表示。
#对两个线程进行比较
#include
int pthread_equal(pthread_t tid1, pthread_t tid2) ;
# 相等返回非0,否则返回0
#取得线程自身id
#include
pthread_t pthread_self(void) ;
# 返回调用线程的id
11.4 线程创建
- unxi程序开始运行的时候,是单进程单线程运行的
- 线程创建
#include
int pthread_create(pthread_t *restrict tidp,
const ptherad_attr_t *restrict attr, void *(*start_rtn)(void*), void *restrict arg) ;
#成功返回0,失败返回错误编号
#当pthread_create 成功返回时,新创建的线程id就会被设置为tidp指向的内存单元
#attr用于定制不同的线程属性,默认为NULL
#新创建的线程从start_stn函数的地址开始运行,该函数只有一个无参数的arg,如果需要向start_rtn传递的参数有一个以上,那么需要把这些参数放到一个结构中,然后把结构的地址作为arg参数传入
- 新创建的线程不能保障哪个线程会先运行:是新创建的线程还是调用线程。
- 新创建的线程可以访问进程的地址空间,并且继续调用线程的浮点环节和信号屏蔽字,但是该线程的挂起信号集会被清除。
- pthread调用失败返回错误码,每个线程有独立的错误码。
#include "apue.h"
#include
pthread_t ntid ;
void printids(const char *s) {
int pid ;
pthread_t tid ;
pid = getpid() ;
tid = pthread_self() ;
printf("%s pid %lu tid %lu (0x%lx) \n ", s, (unsigned long)pid, (unsigned long)tid, (unsigned long)tid) ;
}
void * thr_fn(void *arg) {
printids("new threads:") ;
return ((void *) 0) ;
}
int main(){
int err ;
err = pthread_create(&ntid, NULL, thr_fn, NULL) ;
if (err!=0)
printf("can not create thred");
printids("main thread") ;
sleep(1) ;
exit(0) ;
}
11.5 线程终止
- 如果进程中的任意线程调用了exit,_Exit或者_exit,那么整个进程就会终止
- 单个线程可以通过三种方式退出
- 线程可以简单的从启动例程中返回,返回值是线程的退出码
- 线程可以被同一进程中的其他线程取消
- 线程调用pthread_exit
#include
void pthread_exit(pthread_t thread, void *rval_ptr) ;
//rval_ptr是一个无类型指针,与传给启动例程的单个参数类似。
# 进程中的其他线程也可以通过调用pthread_join参数访问到这个指针
#下面的函数自动把线程置于分离状态,已经处于分离状态的线程再次调用会返回EINVAL。
#调用pthread_join可以等待指定的线程终止,但并不获取线程终止状态。
int pthread_join(pthread_t thread, void **rval_ptr) ;
//成功返回0,否则返回错误编号
#include "apue.h"
#include
#include
/*
使用pthread_join获得某个线程的退出状态
*/
pthread_t ntid ;
void * thr_fn1(void *arg) {
printf("thread 1 returning \n") ;
return ((void *) 1) ;
}
void * thr_fn2(void *arg) {
printf("thread 2 exiting \n") ;
pthread_exit((void*)2);
}
int main(void){
using namespace std ;
int err ;
pthread_t tid1, tid2 ;
void *tret ;
err = pthread_create(&tid1, NULL, thr_fn1, NULL) ;
if (err!=0)
printf("can not create thred \n");
err = pthread_create(&tid2, NULL, thr_fn2, NULL) ;
if (err!=0)
printf("can not create thred \n");
err = pthread_join(tid1, &tret) ;
if (err!=0)
printf("can not join with thread 1 \n");
printf("thread 1 exit code %ld \n", (long)tret);
err = pthread_join(tid2, &tret) ;
if (err!=0)
printf("can not join with thread 2 \n");
printf("thread 2 exit code %ld \n", (long)tret);
exit(0) ;
}
- 线程可以通过调用pthread_cancel函数请求取消同一进程中的其他线程
#include
int pthread_cancel(pthread_t tid) ;
//成功返回0,否则返回错误编号
//相当于调用pthread_exit()
#include "apue.h"
#include
/*
使用pthread_join接受线程的返回状态
*/
struct foo{
int a, b, c, d ;
} ;
void printfoo(const char *s, const struct foo *fp) {
printf("%s ", s) ;
printf(" structure at 0x%lx \n", (unsigned long) fp) ;
printf(" foo.a = %d \n ", fp->a) ;
printf(" foo.b = %d \n ", fp->b) ;
printf(" foo.c = %d \n ", fp->c) ;
printf(" foo.d = %d \n ", fp->d) ;
}
void * thr_fn1(void * arg){
struct foo foo = {1,2,3,4};
printfoo("thread1 :\n", &foo) ;
pthread_exit((void *)&foo) ;
}
void * thr_fn2(void * arg){
printf("thread2 :ID is %lu \n", (unsigned long)pthread_self());
pthread_exit((void *)0) ;
}
int main(void){
using namespace std ;
int err ;
pthread_t tid1 , tid2 ;
struct foo *fp ;
err = pthread_create(&tid1, NULL, thr_fn1, NULL) ;
if (err!=0)
printf("can not create thread 1");
err = pthread_join(tid1,(void **)&fp) ;
if(err!=0)
printf("can not join with thread 1") ;
sleep(1) ;
printf("parent starting second thread \n") ;
err = pthread_create(&tid2, NULL, thr_fn2, NULL) ;
if (err!=0)
printf("can not join with thread 2") ;
sleep(1) ;
printfoo("parent:\n" , fp);
exit(0) ;
}
- 线程可以安排它退出时需要调用的函数,这与进程在退出时可以用atexit函数安排退出时类似的。这样的函数叫线程清理处理函数
- 一个函数可以建立多个清理处理程序,处理程序记录在栈中,也就是说,执行顺序和注册顺序相反
#include
void pthread_cleanup_push(void (*rtn)(void *), void *arg) ;
void pthread_cleanup_pop(int execute) ;
- 当线程执行以下动作的时候,清理函数是由pthread_cleanup_push函数调度的,调用时只有一个参数arg
- 调用pthread_exit
- 响应取消请求时候
- 用非零execute参数调用pthread_cleanup_pop
- execete 参数设置为0,则清理函数不被调用
- 如果线程是从它的启动进程中返回而终止的话,它的清理处理程序就不会被调用。
- 在默认情况下,线程的终止状态会保存知道对该线程调用pthread_join,如果线程已经被分离,线程的底层存储资源可以在该线程终止的时候被立即收回。在线程被分离后,我们不能用pthread_join来等待其终止状态,否则会产生为定义的行为
- 可以调用下面函数对线程进行分离
#include
int pthread_detach(pthread_t tid) ;
11.6 线程同步
- 多个线程共享内存时,需要确保每个线程看到一致的数据视图。
- 对于多个线程都会修改的变量,我们需要对这些线程进行同步,避免访问到无效的值
- 当多个线程需要同步的时候,会需要使用线程锁进行同步
- 计算机的体系结构和程序使用变量的方式都会引起竞争和不一致。
11.6.1互斥量(mutex)
- mutex本质就是一把锁,在访问共享资源的时前对互斥量加锁,访问完成后解锁
- 对互斥量加锁之后,其他试图访问互斥量的线程都会被阻塞。直到当前线程释放该锁。
- 如果释放锁的时候有一个以上的线程被阻塞,那所有线程都会变成可运行状态,第一个变成可运行状态的线程可以对互斥量加锁,其他线程看到互斥量依然是锁的,只好回去继续等待。
- 互斥变量使用pthread_mutex_t数据类型表示,使用之前必须先初始化,可以把它设置为常量PTHREAD_MUTEX_INITIALIZER(只适用于静态分配的互斥量)也可以通过调用pthread_mutex_init函数进行初始化,如果动态分配互斥量(例如通过调用malloc函数),在释放内存前需要调用pthread_mutex_destroy
#include
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr) ;
int pthread_mutex_destroy(pthread_mutext_t *mutex) ;
- 要用默认属性初始化互斥量,只要把attr设置为NULL即可
- 对互斥量加锁,使用下面的方法
#include
int pthread_mutex_lock(pthread_mutex_t *mutex) ;
int pthread_mutex_trylock(pthread_mutex_t *mutex) ;
int pthread_mutex_tunlock(pthread_mutex_t *mutex) ;
- 调用线程将阻塞直到互斥量被解锁
- 如果线程不希望被阻塞,可以使用pthread_mutex_trylock尝试加锁。如果调用pthread_mutex_trylock时互斥量处于未锁住,则加锁,如果锁住,则返回EBUSY。
11.6.2 避免死锁
- 如果线程是对同一个互斥量加锁两次,那么它自身就会陷入死锁状态。
- 如果两个线程都在相互请求另一个线程拥有的资源时候,也会产生死锁
- 如何避免死锁
- 控制互斥量加锁顺序(死锁只会发生在一个线程试图锁住另一个线程以相反的顺序锁住的互斥量)
- 只适用相同的顺序加锁,则可以避免死锁
11.6.3 函数pthread_mutex_timelock
- 当线程试图获取一个已经加锁的互斥量时,pthread_mutex_timedlock互斥量原语允许绑定线程阻塞指定超时时间,达到超时时间时,不会对互斥量加锁,而是返回错误吗ETIMEOUT
11.6.4 读写锁
- 读写锁就是读锁和写锁
- 读写锁有三种状态
- 写加锁状态,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞
- 读加锁状态,所有试图对读模式对它进行加锁的线程都可以得到访问权,但是任何希望写模式对此锁加锁的线程都会阻塞,直到所有线程释放它的读锁
11.6.6 条件变量
11.6.8 屏障
- 屏障是用户协调多个线程并行工作的同步机制,屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行。
- pthread_join()就是一种屏障,允许一个线程等待,直到另一个线程退出。
12章:线程控制
12.3线程属性
- pthread接口允许我们通过设置每个对象关联的不同属性来细调线程和同步对象的行为。通常,管理这些属性遵循相同的模式:
- 每个对象与它自己类型的属性对象进行关联。
- 有一个初始化函数,把属性设置为默认值
- 有一个销毁属性对象的函数,该函数负责初始化韩培属性相关的资源
- 每个属性都有一个设置属性值的函数。