unix编程以及xv6系统浅谈(七)线程

509 阅读7分钟

每个线程都包含执行环境所需的信息。包括有

​ 线程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值并不会被改变。