os-线程总结
Contents
概念
现代系统允许一个进程里同时运行多个线程,线程由内核自动调度。每个线程都有它自己的线程上下文,包括一个唯一的整数线程ID、栈、栈指针、程序计数器(PC)、通用目的寄存器和条件码。
共享本进程虚拟地址空间的所有内容,包括它的代码、数据、堆、共享库和打开的文件。
Posix线程(Pthreads)是在C程序中处理线程的标准接口。
线程上下文模型
线程库
linux下提供了多种方式来处理线程同步,最常用的互斥锁、条件变量、信号量和读写锁。
pthread线程库
线程基本函数,linux下man pthread.h
请查看。
头文件pthread.h
, gcc链接是参数: -lpthread
。
1.创建线程:int pthrad_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func)(void *), void *arg);
tid
: 输出参数,保存返回的线程ID(与linux系统中的线程ID不一样,这个ID应该理解为一个地址),用无符号长整型表示;attr
: 输入参数,线程的相关属性,如线程优先级、初始栈大小、是否为守护进程等,一般置为NULL,表示使用默认属性;func
: 输入参数,一个函数指针(void*job(void *arg);
),线程执行的函数;arg
: 输入参数,函数的参数,如果有多个参数须将其封装为一个结构体;
返回值:成功返回0,失败返回errno值(正数);
2.退出线程:void pthread_exit(void *status);
status
: 输入状态,退出状态
3.等待线程退出:int pthread_join(pthread_t tid, void **status);
tid
: 输入参数,指定等待的线程ID;status
: 输出参数,一个二级指针,保存退出值,可为NULL; 返回值:成功返回0,失败返回errno值;- 功能:会阻塞直到线程tid终止,将线程例程返回的通用
(void*)
指针赋值为thread_return指向的位置,然后回收已终止线程占用的所有资源。
4.获取当前线程ID :pthread_t pthread_self(void);
5.分离线程 :int pthread_detach(pthread_t tid);
tid
:输入参数,指定的线程ID;- 返回值:成功返回0,失败返回errno值;
结合线程&分离线程
在任何时间节点上,线程是可结合的或者是分离的。
- 结合线程:能够被其他线程回收和杀死,在他被回收之前内存资源不释放的;
- 分离线程:不能被其他线程回收或杀死,他的内存资源在它终止时由系统自动释放;
通信(同步)方式
互斥锁
互斥锁是同一时刻只允许一个线程执行一个关键部分代码。
1.静态初始化:static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
2.动态初始化:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
:
mutex
:输出参数,互斥变量;- 返回值:成功返回0,失败返回errno值;
attr
:输入参数,锁属性,NULL值为默认属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前有四个值可供选择:PTHREAD_MUTEX_TIMED_NP
: 这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。PTHREAD_MUTEX_RECURSIVE_NP
: 嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。PTHREAD_MUTEX_ERRORCHECK_NP
: 检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。PTHREAD_MUTEX_ADAPTIVE_NP
: 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
3.阻塞加锁:int pthread_mutex_lock(pthread_mutex *mutex);
4.非阻塞加锁:int pthread_mutex_trylock( pthread_mutex_t *mutex);
不同于阻塞加锁,已经被占用时返回EBUSY而不是挂起等待。
5.解锁(要求,锁是lock状态,并且由加锁线程解锁):int pthread_mutex_unlock(pthread_mutex *mutex);
6.销毁锁(要求,锁是unlock状态,否则返回EBUSY):int pthread_mutex_destroy(pthread_mutex *mutex);
条件变量
条件变量是利用线程间共享全局变量进行的一种机制。条件变量上基本操作有:触发条件(当条件变量为true时);等待条件,挂起线程直到其他线程出发条件。
1.静态初始化、动态初始化(和互斥锁相似):static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
:静态初始化int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
:动态初始化
2.无条件等待:int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
3.计时等待:int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
无条件等待、或计时等待都必须和互斥锁配合以防止多个线程同时请求:
- mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP);
- 在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。
- 在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。
4.激发条件,激活一个等待该条件的线程(存在多个等待线程时按入队顺序激活其中一个):int pthread_cond_signal(pthread_cond_t *cond);
5.激活所有等待线程:int pthread_cond_broadcast(pthread_cond_t *cond);
6.销毁条件变量:int pthread_cond_destroy(pthread_cond_t *cond);
- 只有在没有线程在该条件变量上等待的时候才能销毁这个条件变量,否则返回EBUSY
说明:
- pthread_cond_wait 自动解锁互斥量(如同执行了pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用CPU时间,直到条件变量被触发(变量为ture)。在调用 pthread_cond_wait之前,应用程序必须加锁互斥量。pthread_cond_wait函数返回前,自动重新对互斥量加锁(如同执行了pthread_lock_mutex)。
- 互斥量的解锁和在条件变量上挂起都是自动进行的。因此,在条件变量被触发前,如果所有的线程都要对互斥量加锁,这种机制可保证在线程加锁互斥量和进入等待条件变量期间,条件变量不被触发。条件变量要和互斥量相联结,以避免出现条件竞争——个线程预备等待一个条件变量,当它在真正进入等待之前,另一个线程恰好触发了该条件(条件满足信号有可能在测试条件和调用pthread_cond_wait函数(block)之间被发出,从而造成无限制的等待)
- 条件变量函数不是异步信号安全的,不应当在信号处理程序中进行调用。特别要注意,如果在信号处理程序中调用 pthread_cond_signal 或 pthread_cond_boardcast 函数,可能导致调用线程死锁
为什么pthread_cond_wait需要的互斥锁不在函数内部定义,而要使用户定义的呢?
- pthread_cond_wait 和 pthread_cond_timewait 函数为什么需要互斥锁?因为:条件变量是线程同步的一种方法,这两个函数又是等待信号的函数,函数内部一定有须要同步保护的数据。
- 使用用户定义的互斥锁而不在函数内部定义的原因是:无法确定会有多少用户使用条件变量,所以每个互斥锁都须要动态定义,而且管理大量互斥锁的开销太大,使用用户定义的即灵活又方便,符合UNIX哲学的编程风格(随便推荐阅读《UNIX编程哲学》这本好书!)。
- 大神推测pthread_cond_wait函数的内部结构时
1 | int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) |
作为有态度的技术人揣测咱们也不能轻易的相信,探究pthread_cond_wait函数的内部结构过程:
1.从http://ftp.gnu.org/gnu/glibc/
下载最新的glibc-2.29.tar.gz(本人下载)
,在nptl目录下的pthread_cond_wait.c
文件中
2.函数实现,整体过程跟大神讲的差不多,不同点在于加锁和解锁在循环外面
1 | static __always_inline int |
信号量
如同进程一样,线程也可以通过信号量来实现通信。
头文件semaphore.h
, 记得man
查看详情。
1.初始化int sem_init (sem_t *sem , int pshared, unsigned int value);
- sem: 指定要初始化的信号量;
- pshared: 信号量 sem 的共享选项,linux只支持0,表示它是当前进程的局部信号量;
- value: 信号量 sem 的初始值。
2.信号量值加1int sem_post(sem_t *sem);
3.信号量值减1int sem_wait(sem_t *sem);
4.销毁信号量int sem_destroy(sem_t *sem);
读写锁
读锁:一个读锁占用的临界区后来的读锁也可以进入,但是如果是写锁,则阻塞。在写锁阻塞后,后面再来读锁也会阻塞。这样避免读锁长期占用资源,防止写锁饥俄。
写锁:写锁占用了临界区,其他的线程(读写锁)都会阻塞。
1.初始化
1 | // 动态初始化 |
2.读写锁
1 | // 加锁 |
3.销毁锁int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
并发小示例
要求:用两个线程分别输出数组的奇数下标和偶数下标的内容;
实现(忽略捕获异常):
1 |
|
编译:gcc -o pthread_array pthread-array.c -lpthread
编译时warn可忽略。。。
输出:
1 | ./pthread_array |
一个进程23672,三个线程(算main线程),即一个程序至少有一个进程,一个进程至少有一个线程 。