每个线程都包含执行环境所需的信息。包括有
线程ID,一组寄存器值,栈,调度优先级和策略,信号屏蔽字,errno变量,线程私有数据
一个进程的所有信息对它的线程都是共享的,包括有
可执行程序的代码,程序的全局内存,堆内存,栈,文件描述符
7.1 线程标志
每个线程都有一个ID,但是不同于进程ID,进程的在整个系统中都是唯一的,但是线程ID只有在它所属的进程上下文中才有意义。
出于移植性的考虑,需要使用一个函数对两个线程ID进行比较。
//判断两个线程id是否相同
int pthread_equal(pthread_t tid1,pthread_t tid2)
相等为真,否则假
pthread_t pthread_self(void);
获取线程自身的ID
7.2 线程创建
每个进程只有一个控制线程
创建线程的过程如下
//创建线程
int pthread_create(pthread_t *tidp,
const pthread_attr_t *attr,
void *(*start_rtn)(void*),void *arg);
//返回值:成功返回0,否则返回错误编号
tidp:
通过这个地址写入线程id
attr:
定制各种不同的线程属性(NULL不设置)
start_rtn:
新线程从这个函数启动,函数参数必须为void*,用后面的arg参数传入
返回值必须为void*,这里使用void*的原因是这样可以返回大量的数据信息(结构体等)
arg:
传入到前面的函数指针的参数
7.3 线程终止
如果进程中的任意线程调用了exit, Exit或者_exit,那么整个进程都会被终止。
单个线程可以通过以下三种方式退出:
(1)从启动函数中返回,返回值是线程的退出码
(2)可以被同一进程中的其他线程取消
(3)线程调用pthread_exitnt
void pthread_exit(void *rval_ptr)
进程中的其它线程可以通过pthread_join函数来访问到这个指针rval_ptr
注意这个指针要求在函数结束后这块地址依旧可以使用,所以尽量不要使用栈
int pthread_join(pthread_t thread,void** rval_ptr)
//成功则返回0,否则返回错误编号
调用的线程将一直阻塞直到thread指定的线程按上述的3种方式退出
rval_ptr:
若从线程中返回则包含返回值
若是pthread_exit则*rval_ptr=rval_ptr(exit中)
线程被取消则设置为PTHREAD_CANCELED
对返回值不感兴趣则设置为NULL
//和wait函数相同,这是个收尸的
线程可以通过调用pthread_cancel来取消同一进程中的其他线程
int pthread_cancel(pthread_t tid)
//返回值:成功0,否则错误编号
该函数会使被取消的线程表现的如同调用了参数PTHREAD_CANCELED的pthread_exit函数
线程可以选择忽略取消或者控制如何被取消,该函数仅仅提出请求!
线程中也有类似于进程中的atexit函数,大体的例如推入栈的方式相同
void pthread_cleanup_push( void (*rtn) (void*), void* arg )
rtn表示清理函数,arg表示传入此函数的参数
在一些情况下会调用这些清理函数
(1)调用pthread_exit
(2)响应取消请求时
(3)使用非0参数调用pop时
//注意,return 并不会调用清理函数
void pthread_cleanup_pop(int execute)
出栈一个清理函数,如果execute参数为真则运行这个清理函数,否则只出栈不运行
默认情况下,线程的终止状态会一直保存知道调用join才会回收它的资源。
但是,如果线程已经被分离,其终止的时候资源就会被立即回收,分离后不可以使用join等待它的终止状态
int pthread_detach(pthread_t tid)
//分离一个线程
7.4 线程同步
除非是原子操作,否则同步问题始终是存在的,一般加减之列的操作(++)都不是原子操作
7.4.1 互斥量
类似于锁,会有线程挂在这个互斥量的队列上
通过休眠的方式使得线程阻塞,再通过信号的方式唤醒线程
//初始化方式
pthread_mutex_t t=PTHREAD_MUTEX_INITIALIZER; //静态分配互斥量
pthread_mutex_t *t=malloc(sizeof(pthread_mutex_t)); //动态分配策略
//动态分配的需要调用函数来清除
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//成功则返回0,否则返回错位
int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t *attr);
默认属性初始化锁只需要设置attr为NULL
锁的操作如下
//成功返回0,否则返回错误
int pthread_mutex_lock(pthread_mutex_t *mutex);
若互斥量已经被锁则阻塞直到被解锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
试着锁,不会阻塞,成功返回0,否则有锁返回EBUSY
int pthread_mutex_lock(pthread_mutex_t *mutex);
解锁
如果同一个线程试图对同一个互斥量加锁两次,它自身就会陷入死锁状态。可以使用trylock函数来尽量避免死锁。
7.4.2 读写锁
读写锁有三种状态:
(1)读模式下加锁状态
(2)写模式下加锁状态
(3)不加锁状态
写+锁状态时,试图对这个锁+锁的都会阻塞。
读+锁状态时,试图以读模式对它加锁的线程都会得到访问权,但是任何试图以写模式对它加锁的都会阻塞,直到释放锁。(此时会阻塞随后的读模式锁请求,防止请求写的线程饥饿)。
读写锁在使用之前必须初始化,在释放它们底层的内存之前必须摧毁。
int pthread_rwlock_init(pthread_rwlock_t *rwlock,
const pthread_rwlockattr_t *attr);
如果希望有默认属性,则设置attr为NULL
pthread_rwlock_t *A = PTHREAD_RWLOCK_INITIALIZER; //静态初始化
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
做清理工作
对读写锁的加锁解锁方式如下
//成功返回0,否则返回错误编号
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
读模式加锁,若同一个线程加锁两次会陷入死锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
写模式加锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
不管读写都可以通过此函数解锁
读写锁的条件版本
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//可以获取锁时,返回0,否则返回错误EBUSY
7.4.3 条件变量
条件变量是一个同步机制,条件变量与互斥量一起使用的时候,允许线程以无竞争的方式等待特定的条件发送。
条件变量的初始化过程为
pthread_cond_t *cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *attr);
attr设置为NULL则默认初始化
int pthread_cond_destroy(pthread_cond_t *cond)
销毁
使用下面函数等待条件成真,注意一定时间后未成真不会一直阻塞,会生成一个返回错误码的变量。
int pthread_cond_wait(pthread_cond_t *cond,
pthread_mutex_t *mutex);
//成功则返回0,否则返回错误编号,注意不会一直阻塞
//互斥量会对条件进行保护,因为函数中会对条件检查
//互斥量必须加锁才能使用此函数,在此函数等待过程中别的线程默认此互斥量是可以获得的
/* LOCK(mutex);
条件检查,修改;
放入等待条件队列;
UNLOCK(mutex);
sleep(time); //等待唤醒
LOCK(mutex);
*/
int pthread_cond_timedwait(pthread_cond_t *cond,
pthread_mutex_t *mutex,
const struct timespec *tsptr);
可以设定等待时间,tsptr是绝对时间,比如下午2点,可以配合gettimeofday+time设定为相对时间
使用下面函数给线程发送信号
int pthread_cond_signal(pthread_cond_t *cond);
至少唤醒一个等待该条件的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
唤醒所有等待该条件的线程
7.4.4 自旋锁
自旋锁的使用方式和互斥量类似,不同点在于获取锁之前一直是忙等待的状态,并不会休眠。
适用于底层原语(切换代价小),或者非抢占式内核中,或者用户空间中锁被持有的时间短而并不想被抢占的一些程序。
初始化和反初始化
int pthread_spin_init(pthread_spinlock_t *lock,int pshared);
pshared:
PROCESS_SHARED 可以被任何线程(只要有权限访问这块内存)访问
PTHREAD_PROCESS_PRIVATE 只能同一个进程下
int pthread_spin_destroy(pthread_spinlock_t *lock);
操作方式如下
int pthread_spin_lock(pthread_spinlock_t *lock);
加锁,同一线程重复加锁可能永久自旋
int pthread_spin_trylock(pthread_spinlock_t *lock);
已经被锁则返回EBUSY
int pthread_spin_unlock(pthread_spinlock_t *lock);
7.4.5 屏障
屏障是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都到达某一结点,然后从该结点继续执行。
int pthread_barrier_init(pthread_barrier_t *barrier,
const pthread_barrierattr_t *attr,
unsigned int count);
count:
到达屏障所需要的线程数目。
attr:
属性,默认NULL
int pthread_barrier_destroy(pthread_barrier_t *barrier);
int pthread_barrier_wait(pthread_barrier_t *barrier);
调用表示已经完成任务,在线程屏障未满足条件的时候,会进入休眠状态
若满足了屏障计数条件,所有的线程都会被唤醒
一旦达到屏障计数条件,而且线程处于非阻塞状态,屏障就可以被重用,但是count值并不会被改变。