unix线程

1. 概念

一个unix进程可看成只有一个控制线程。多线程好处:

  • 可为每种事件分配单独的处理线程.可采用同步和异步两种编程模式,同步比异步更简单。
  • 不同于进程,多线程可自动访问相同的存储地址空间和文件描述符,而进程需要操作系统复杂机制才能实现内存和文件描述符的共享.
  • 有些问题可分解从而提高整个程序的吞吐量。多个线程可使相互独立的任务交叉进行。
  • 交互程序可通过多线程改善响应时间

每个线程都包含执行环境所必需的信息,包括线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量以及线程私有数据。一个进程的所有信息对该进程的所有线程都是**共享的,包括可执行程序的代码、程序的全局内存和堆内存、栈以及文件描述符**。

2. 线程标识

线程ID数据类型: pthread_t

#include <pthread.h>

int pthread_equal(pthread_t tid1, pthread_t tid2);
pthread_t pthread_self(void); //调用线程的线程ID

3.线程创建、终止

线程创建:

#include <pthread.h>

int pthread_create(pthread_t *restrict tidp,
                    const pthread_attr_t *restrict attr,
                    void *(*start_rtn)(void *), void *restrict arg);

attr: 线程属性
start_rtn: 待执行函数的函数地址
arg: 待执行函数start_rtn的参数,若需要传递一个以上参数,需把这些参数放到一个结构中,然后把结构的地址作为arg参数传入。

新创建的线程可访问进程的地址空间,并继承调用线程的浮点环境和信号屏蔽字,但该线程的挂起信号集会被清除

线程退出:
单个线程3种退出方式:

  • 从启动例程返回
  • 被同一进程中其他线程取消
  • 调用pthread_exit: void pthread_exit(void *rval_ptr)。
#include <pthread.h>

int pthread_join(pthread_t thread, void **rval_ptr);

阻塞调用线程直到线程调用pthread_exit、从启动例程返回或者被取消。
通过调用pthread_join自动把线程置于分离状态,从而回收资源。若线程已经处于分离状态,pthread_join调用会失败,返回EINVAL.

线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程。线程可以选择忽略取消或者控制如何被取消

#include <pthread.h>

int pthread_cancel(pthread_ t tid);

线程清理处理程序: 与进程退出时可用atexit函数安排退出类似。处理程序记录在栈中,也就是,它们的执行顺序与它们注册时相反。

#include <pthread.h>

void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);

何时调度清理函数rtn?

  • 调用pthread_exit时;
  • 响应取消时;
  • 用非零execute参数调用pthread_cleanup_pop时

note: 线程例程return时不会执行清理程序。

分离线程:默认情况下,线程的终止状态会一直保存直到对该线程调用pthread_join。若线程已分离,线程的底部存储资源可以在线程终止时立即被收回。

int pthread_detach(pthread_t tid);

4.线程同步

一致性问题

5.互斥量

1.互斥量

互斥量: 本质上是一把锁,在访问共享资源前对互斥量进行设置(加锁),完成后释放(解锁)互斥量

互斥量数据类型: pthread_mutex_t
初始化:

  • 常量PTHREAD_MUTEX_INITIALIZER (只适用于静态分配的互斥量)
  • pthread_mutex_init函数初始化

释放: pthread_mutex_destroy

对互斥量加锁/解锁:

#include <pthread.h>
#include <time.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict tstpr);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
                        所有函数: 若成功返回0,否则返回错误编号
  • pthread_mutex_lock: 若互斥量已上锁,调用线程会阻塞直到互斥量被解锁
  • pthread_mutex_trylock: 若互斥量已上锁,pthread_mutex_trylock调用失败,返回EBUSY
  • pthread_mutex_timedlock: 允许调用线程阻塞一定时间,超时不会对互斥量加锁,而是返回错误码ETIMEDOUT.

2.死锁

  • 如果线程对同一个互斥量加锁两次,那它自身就会陷入死锁状态
  • 其他,如两个线程都在相互请求另一个线程所拥有的资源

避免死锁: 控制互斥量加锁顺序来避免死锁。有时候对互斥量排序很难,这时可以先释放占有的锁,等一段时间再试,此时可以使用pthread_mutex_trylock来避免死锁。若不能获取锁,可以先释放已占有的锁,做好清理,等过一段时间再重新试。

锁的粒度: 粒度太粗,就会出现很多线程等待相同的锁,这可能不能改善并发性; 粒度太细,过多的锁开销会使系统性能受到影响,且代码变复杂。应在二者之间寻找平衡

3.读写锁

与互斥量类似,不过有更高的并行性。

读写锁3种状态:

  • 读模式下加锁
  • 写模式下加锁
  • 不加锁

一次只有一个线程可以占有写模式的读写锁,但多个线程可同时占有读模式的读写锁

读写锁初始化:

  • PTHREAD_RWLOCK_INITIALIZER常量
  • int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

销毁: int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

加锁/解锁:

#include <pthread.h>

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);  //读加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);  //写加锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);  //读加锁, 成功返回0,失败返回错误EBUSY
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);  //写加锁, 成功返回0,失败返回错误EBUSY
int pthread_rwlock_timedrdlock(pthread_rwlock_t *rwlock);  //带有超时的读加锁, 失败返回ETIMEDOUT错误
int pthread_rwlock_timedwrlock(pthread_rwlock_t *rwlock);  //带有超时的写加锁, 失败返回ETIMEDOUT错误
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);  //解锁

4.条件变量

条件变量是线程另一种同步机制,给多个线程提供了一个汇合场所,条件变量和互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生.

条件变量本身由互斥量保护,线程在改变条件变量之前必须首先锁住互斥量

条件变量数据类型: pthread_cont_t
初始化:

  • PTHREAD_COND_INITIALIZER
  • pthread_cond_init函数

销毁: pthread_cond_destroy函数

#include <pthread.h>

int pthread_cond_init(pthread_cond_t *restrict cond, 
                      const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *restrict cond, 
                      pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, 
                          pthread_mutex_t *restrict mutex,
                          const struct timespec *restrict tsptr);

pthread_cond_wait: 等待条件变量变为真

传递给pthread_cond_wait的互斥量对条件进行保护。调用者把锁住的互斥量传给函数,函数然后原子性地把调用线程放到等待条件的线程列表上,并对互斥量解锁。pthread_cond_wait返回时,互斥量再次被锁住

pthread_cond_wait或pthread_cond_timedwait调用成功返回时,线程需要重新计算条件,因为另一个线程可能已经在运行并改变了条件。

线程唤醒(给线程或条件发信号):

  • pthread_cond_signal函数: 至少能唤醒一个等待该条件的线程
  • pthread_cond_broadcast函数: 唤醒等待该条件的所有线程
#include <pthread.h>

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

unix线程
图11-15中在while循环中检查条件,可以避免问题; 线程醒来,发现队列仍为空,然后返回继续等待。若代码不能容忍这种竞争,就需要在给线程发信号的时候占有互斥量。

小结: 条件变量是在多线程程序中实现“等待–>唤醒”逻辑的常用方法。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作: 一个线程等待“条件变量”的条件成立而挂起;另一个线程使条件成立。条件变量的使用总是和一个互斥锁结合在一起。线程在改变条件状态前必须首先锁住互斥量,函数pthread_cond_wait把自己放到等待条件的线程列表上,然后对互斥量解锁。这两个操作是原子操作。在函数返回时,互斥量再次被锁住。

5. 自旋锁

自旋锁不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)状态。

#include <pthread.h>

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

6.屏障

屏障是协调多个线程并行工作的同步机制,它们允许任意数量的线程等待,直到所有的线程完成处理工作,而线程不需要退出,所有线程到到达屏障后可以接着工作。

#include <pthread.h>

int pthread_barrier_init(pthread_barrier_t *restrict barrier,
                        const pthread_barrierattr_t *restrict attr,
                        unsigned int count); //count指定在允许所有线程继续允许之前,必须到达屏障的线程数目
int pthread_barrier_destroy(pthread_barrier_t *barrier);

int pthread_barrier_wait(pthread_barrier_t *barrier);