线程
对操作系统来说,线程是最小的执行单元,进程是最小的资源管理单元。
1. 线程概念
- LWP: light-weight Process,本质上仍然是进程(Linux环境下)
- 线程与进程
- 进程:不同的进程拥有独立的地址空间
- 线程:也有PCB控制块,但是没有独立的地址空间,共享。
- Linux下,线程是最小的执行单位,而进程是最小的资源分配单位。
- 查看进程下的线程:
ps -Lf 进程id
LWP
是线程号,用于cpu分配时间的,而不是线程id号。
- 栈帧空间:存放局部变量和临时值
2. 优缺点
- 线程的共享资源:
- 文件描述符
- 每种信号的处理方式
- 当前工作目录:由工作目录进程决定
- 共享用户id和组id
- 内存地址空间:(
./text/.data/.bass/heap/动态库
,即除了栈)。即也共享全局变量。
- 非共享
- 线程id
- 处理器现场和栈指针
- 独立的栈空间
errno
变量- 信号屏蔽字
- 优先级调度
- 优点
- 提高程序的并发性
- 开销小
- 数据通信、共享数据方便
- 缺点
- 库函数,不稳定
- 调试、编写困难、gdb调试支持
- 对信号支持不好
线程同步
下面的函数都是成功返回0,失败返回错误号。
1. mutext
- 初始化
#include <pthread.h> int pthread_mutex_destroy(pthread_mutex_t *mutex); int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 互斥量初始化后是1。
lock/unlock
#include <pthread.h> int pthread_mutex_lock(pthread_mutex_t *mutex); // 堵塞 int pthread_mutex_trylock(pthread_mutex_t *mutex); // 不堵塞 int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 注意这是锁是为了保护共享数据区域,应该使得锁的粒度越小越好。
比如:
while(1) { pthread_mutex_lock(&mutex); printf("h "); // 1 sleep(rand() % 3); printf("w.\n"); // 2 pthread_mutex_unlock(&mutex); sleep(rand() % 3); }
- 上面的锁是为了保护
stdout
。unlock应该放在第二个print的位置,而不是放在最后一个sleep的后面。 - 死锁
- 同一个线程试图对同一个互斥量加锁两次,即两次调用
pthread_mutex_lock
,而中间没有调用pthread_mutex_unlock
。 - 线程1拥有A锁,请求B锁,线程2拥有B锁,请求A锁
pthread_mutex_trylock
:当拿不到锁时,就会放弃所有的锁,就不会有死锁。
2. 读写锁
- 写独占,读共享,写锁优先级更高。相比较互斥量锁,效率更高,因为读时可以共享。
- 函数
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
3. 条件变量
- 条件变量不是锁,但它可以造成线程堵塞。通常和
mutex
配合使用,给多线程提供一个会和的场所。 - 函数
#include <pthread.h> int pthread_cond_destroy(pthread_cond_t *cond); int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime); int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); int pthread_cond_broadcast(pthread_cond_t *cond); int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_wait
:堵塞等待一个条件变量。
- 堵塞等待条件变量
cond
满足 - 释放已掌握的互斥锁,相当于
pthread_mutex_unlock(&mutex)
。这两步是一个原子操作。 - 当被唤醒,
pthread_cond_wait
函数返回时,解除堵塞并且重新申请获取互斥锁。
pthread_cond_broadcast/pthread_cond_signal
:唤醒堵塞中的pthread_cond_wait
。
pthread_cond_broadcast
:唤醒所有堵塞的条件量pthread_cond_signal
: 至少唤醒一个条件量
pthread_cond_timedwait
:
- 绝对时间的获取:
time_t cur = time(NULL); // 获取当前的时间 timespec t; t.tv_sec = cur + secons; // 在当前时间的基础上向后偏移才能获取绝对时间
- 生产者与消费者设计模式
- 条件量的优点:
相较于mutex可以减少竞争。
如果直接使用mutex,除了生产者和消费者之间要竞争互斥量之外,消费者之间也需要竞争互斥量,但是如果链表中没有数据,消费者之间的竞争是没有意义的。通过条件变量的机制,只有在生产者完成生产之后,消费者之间才会开始竞争,提高了效率。
线程函数
只要使用了线程函数,编译时需要链接到-pthread
。
1. pthread_self
#include <pthread.h> pthread_t pthread_self(void);
返回线程id。
2. pthread_Create
#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
创用于建线程,成功时候返回0,失败返回错误编号。
- 参数
thread
: 线程idattr
: 线程属性start_routine
: 线程函数,返回类型是void*
,函数参数类型是void*
arg
: 线程函数的参数
- 注意这个参数传递是以值传递,如果是以指针传递会有危险,因为以指针传递,有可能原本主线程的指针对象可能已经发生了改变。
3. pthread_exit
#include <pthread.h> void pthread_exit(void *retval);
退出的是单个线程,退出值存放在retval
。
exit
:是直接将整个进程退出。无论exit
在同一进程中的哪个位置只要调用了exit
整个进程都会退出。pthread_exit
:只是将调用线程退出。只要在线程中调用了pthread_exit
,即使是深层嵌套,那么该线程就会退出。return
:返回到函数调用者。只在mian
中return == exit
。- 任务:通过
pthread_exit
完成主线程等到所有的子线程完成。
int main(int argc, char const *argv[]) { ''' pthread_exit(NULL); return 0; }
- 在主线程中调用
pthread_exit
,使得主线程退出,就不会执行后面的return
,也就不会退出整个进程。因此仅当最后一个线程执行pthread_exit
之后,整个进程才会退出。
4. pthread_join
#include <pthread.h> int pthread_join(pthread_t thread, void **retval);
阻塞线程等待指定线程thread
退出,获取退出状态,线程退出状态存储在retval
。如果线程已经终止,函数立即返回。
- 线程退出状态相关
- 如果线程被取消了,即调用
pthread_cancel
,则退出状态是PTHREAD_CANCELED
。 pthread_exit(void* )
的退出状态定义为类型void*
,而这里的是void**
。即,原本pthread_exit
是将值转换成void*
,pthread_join
是为获取退出状态,需要用void**
,即更深一级指针。- 在涉及到
malloc/free
时,也可以先开始在主线程中malloc
,将所得地址指针再传入子线程,等一切结束在主线程中free
。
5. pthread_detach
#include <pthread.h> int pthread_detach(pthread_t thread);
pthread_detach
使得指定线程和主线程分离,并且不需要别的线程来对这个线程进程资源回收。当这个detached线程结束后,资源自动回收到系统。
6.pthread_cancel
#include <pthread.h> int pthread_cancel(pthread_t thread);
请求杀死指定线程。
目标线程的是否取消以及何时取消取决于目标线程的两个因素:cancelability state and type
cancelability state
: 决定是否能取消。 这由函数pthread_setcancelstate(3)
决定,默认情况是是能状态。type
:决定何时取消,默认type==defer
,状态可以由函数pthread_setcanceltype(3)
改变为asynchronous
,即任意时候。
- 当
type==defer
,线程只能在取消点到来时候取消。
具体的取消点函数可以man 7 pthreads
查看,一般都是系统调用函数。 - 当
type==dasynchronous
,线程能立即取消。
7. 信号量
- 函数
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value); int sem_destroy(sem_t *sem); int sem_wait(sem_t *sem); int sem_trywait(sem_t *sem); int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
- 编译时需要链接到:
-pthread
。 - 成功返回0,失败返回-1,设置
errno
。 sem_t
数据类型 : 本质上是结构体,但是在使用时可以将其看作是个整数,忽略实现细节。sem_t sem
: 规定信号量大小不能小于0.- 既可以用于进程同步,也可以用于线程同步。
- 信号基本操作
sem_wait
:信号量大于0,则信号量–,类似于pthread_mutex_lock
。信号量等于0时,造成线程阻塞。sem_post
:将信号量++,同时唤醒堵塞在信号量上的线程。类似于pthread_mutex_unlock
。- 信号量的初值,决定了占用信号量的线程个数。