unix线程

前言

典型的UNIX进程可以看作是只有一个控制进程: 一个进程在某个时刻只能做一件事。有了多个控制线程之后,在程序设计时就可以把进程设计成在某一时刻能够做不止一件事,每个线程处理各自独立的任务。每个线程都包含有表示执行环境所必须的信息,其中包括进程中标志线程的 线程ID一组寄存器值调度优先级和策略信号屏蔽字errno变量以及线程私有数据。一个进程的所有信息对该进程所有线程都是共享的,包括可执行程序的代码、程序的全局内存和堆内存、栈以及文件描述符。

线程标志

每个进程都有一个类型是 pid_t 的进程id,线程也有一个类型为 pthread_t 的线程id。与进程不同的是,进程id在全局里唯一,线程id只有在它所属的进程上下文中才有意义。如果需要提高移植性可以将 pthread_t类型封装成结构体,但在调试打印时可能会有多余的麻烦。

注: 获取线程id的方式不止 pthread_self 这一种,像 gettid 也可以获取线程,那么他们的区别是什么呢?

  1. pthread_self 属于 POSIX 实现,而 gettid 属于系统调用。
  2. pthread_self 返回的是当前线程中该线程的唯一标识,对于当前进程是唯一的。所以在不同进程中调用可能会返回相同的线程id。
  3. gettid 返回的是系统内的各个线程的标识符,因为线程实际上就是轻量级进程,所以在单进程单进程中调用该函数实际上返回的是当前进程id。
#include <pthread.h>

// 判断两个线程是否相等
// 返回值: 若相等,返回非0数值;否则,返回0
int pthread_equal(pthread_t tid1, pthread_t tid2);

// 获取当前线程的线程ID
// 返回值: 调用线程的线程ID
pthread_t pthread_self(void);

线程创建

创建一个线程很简单,只需要调用 pthread_create 函数即可,但要注意线程创建时并不能保证那个线程会先运行,所以不能通过通过全局的方式获取当前线程id。新创建的线程可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集会被清除。

#include <pthread.h>

// 创建一个线程
// 返回值: 若成功,返回0;否则,返回错误编号
int pthread_create(pthread_t *restrict tidp,
				  const pthread_attr_t *restrict attr,
				  void* (*start_rtn)(void*), void *restrict arg);

注: 这里使用了一个c语言类型限定符: restrict
它的作用是告诉编译器,变量已经被指针所引用,不能通过除了该指针外所有其他直接或间接的方式修改该对象内容。

线程终止

线程终止不能单纯的调用 exit 族函数,调用之后会引起整个进程的终止,当然也不能是信号,如果默认动作是终止进程也会发生相似的情况。退出进程的方式有三种类型:

  • 线程自动退出程序段;
  • 当前线程被其他线程取消;
  • 当前线程使用 pthread_exit 函数;

当然线程的退出也是可以带参数的,可以通过 pthread_join 的第二个参数来获取,调用的线程表示等待目标线程完成后才向下开始执行。如果当前线程需要等待两个线程结束并使用返回参数的(顺序调用)时,设计者考虑到可能发生线程A退出触发函数时线程B早已经退出而获取不到资源,所以线程退出后并不会立即清除自己的相关信息,当然,也可以调用 pthread_detach 函数表示线程分离来立即清除相关信息。

#include <pthread.h>

// 当前线程退出
// 参数为线程退出回参,通过 pthread_join 进行获取并处理
void pthread_exit(void* rval_ptr);

// 请求取消同一进程中的其他线程
// 选词请求有讲究,线程可以选择忽略请求或者控制如何取消,该函数不阻塞等待目标线程退出
// 返回值: 若成功,返回0;否则,返回错误编号
int pthread_cancel(pthread_t tid);

// 合并线程,等待目标线程退出,同时参数二为目标线程退出时的回参
// 返回值: 若成功,返回0;否则,返回错误编号
int pthread_join(pthread_t thread, void** rval_prt);

// 分离线程,确保线程退出后立即释放自身信息
// 返回值: 若成功,返回0;否则,返回错误编号
int pthread_detach(pthread_t tid);

与进程类似,退出时能够注册一些清理函数,注册函数采用栈的方式进行存储,即先入后出的方式进行执行。当线程执行以下动作时, 都会使用 pthread_cleanup_pop 进行清理

  • 调用 pthread_exit 时;
  • 响应取消请求时;
  • 用非零 execute 参数调用 pthread_cleanup_pop 时;
    如果 execute 参数设置为0,清理函数将不会被自动调用,即如果设置为0,线程正常退出后就不会调用。不管发生上述那种情况,pthread_cleanup_pop 都将删除上次 pthread_cleanup_push 调用建立的清理处理程序。
#include <pthread.h>

// 为线程退出注册一个清理函数
void pthread_cleanup_push(void (*rtn)(void*), void *arg);

// 线程退出时推出栈顶清理函数并执行
void pthread_cleanup_pop(int execute);

线程与进程相似函数列举

进程原语线程原语描述
forkpthread_create创建新的控制流
exitpthread_exit从现有的控制流中退出
waitpidpthread_join从控制流中得到退出状态
atexitpthread_cancel_push注册在退出控制时调用的函数
getpidpthread_self获取控制流的ID
abortpthread_cancel请求控制流的非正常退出

线程同步

当多个线程共享相同的内存时,需要确保每个线程看到一致的数据视图。pthread库提供了不同类型的线程同步接口用来保护数据。

  • 互斥量(pthread_mutex_t)
  • 读写锁(pthread_rwlock_t)
  • 条件变量(pthread_cond_t)
  • 自旋锁(pthread_spinlock_t)
  • 屏障(pthread_barrier_t)

互斥量

互斥量从本质上来说是一把锁,在访问共享资源前对互斥量进行设置(加锁),在访问完成后释放(解锁)互斥量。对互斥量进行加锁之后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放互斥锁。当然使用互斥量时要注意死锁问题,加锁后必须要进行解锁,如果对相同的锁进行两次 lock 操作就会发生死锁。在多把锁时要注意按顺序进行加解锁,不然也会发生死锁。对于这种问题可以考虑使用 muduo 中提到的方法 永远先锁住地址最小的互斥量

#include <pthread.h>

// 静态创建mutex,通常用于局部锁初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 初始化一个互斥量(使用malloc动态创建)
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
					  const pthread_mutexattr_t *restrict attr);

// 销毁一个互斥量(动态创建后需调用)
int pthread_mutex_destroy(pthread_mutex_t* mutex);

// 互斥量直接加锁(没有获取到锁时阻塞)
int pthread_mutex_lock(pthread_mutex_t* mutex);

// 互斥量尝试加锁(没有获取到锁时返回EBUSY,获取到锁时加锁)
int pthread_mutex_trylock(pthread_mutex_t* mutex);

// 互斥量在指定时间内尝试加锁(没有获取到锁时返回ETIMEDOUT,获取到锁时加锁)
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
						   const struct timespec *restrict tsptr);

// 互斥量解锁
int pthread_mutex_unlock(pthread_mutex_t* mutex); 

// 以上所有函数返回值: 若成功,返回0;否则,返回错误编号

读写锁

与互斥量类似,不过读写锁允许更高的并行性。互斥量只有加锁与解锁两种状态,在读写锁中有读模式加锁、写模式加锁和不加锁三种状态。在读数据的时候读模式加锁,如果有线程此时想要修改数据就会阻塞,反之亦然。但为了避免长期阻塞写锁,通常写锁阻塞时会进行抢占,对读锁进行阻塞,以避免读模式锁长期占用导致数据无法及时修改。

注: 读写锁加锁时拥有两种状态,但却共用同一种解锁函数,那它是如何知道当前释放的是那种锁呢?
原理很简单,实际上读写锁就是排他锁,即两种锁不会同时出现,如果先获取到的是读锁,那么后续使用读锁就能成功加锁,但如果你使用写锁进行加锁就会阻塞并改变当前标志位阻塞后面的读锁确保写锁及时获取到资源。如果先获取到的是写锁,所有读锁都会阻塞,直到写锁进行释放。而 unlock 函数可能就只需要更改引用计数即可所以可以共用。

#include <pthread.h>

// 静态创建读写锁
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

// 动态创建读写锁(使用malloc进行创建),需调用destroy进行释放
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
					   const pthread_rwlockattr_t *restrict attr);

// 销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);

// 为读写锁添加读锁(没有获取到时进行阻塞)
// 注意由于各种实现下可能会对共享模式下可获取读写锁的次数进行限制
// 所以需要检查该函数的返回值
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);

// 为读写锁添加写锁(没有获取到时进行阻塞)
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);

// 尝试为读写锁添加读锁(没有获取到时返回EBUSY,获取到时添加读锁)
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);

// 尝试为读写锁添加写锁(没有获取到时返回EBUSY,获取到时添加写锁)
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);

// 在指定时间内获取读锁(没有获取到时返回ETIMEDOUT,获取到时添加读锁)
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
							  const struct timespec *restrict tsptr);

// 在指定时间内获取写锁(没有获取到时返回ETIMEDOUT,获取到时添加写锁)
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
							  const struct timespec *restrict tsptr);

// 读写锁解锁(读锁和写锁共用这个API)
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);

// 以上所有函数返回值: 若成功,返回0;否则,返回错误编号

条件变量

条件变量是线程可用的另一种同步机制,给多个线程提供了一个会合的场所,条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。如果条件变量满足时,就通过传递信号的方式通知阻塞线程开始执行,但线程需要重新计算条件,因为另一个线程可能正在运行并改变了条件。

#include <pthread.h>

// 静态方式初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

// 动态方式初始化条件变量(使用malloc初始化)
int pthread_cond_init(pthread_cond_t *restrict cond,
					 const pthread_condattr_t *restrict attr);

// 使用动态方式初始化条件变量时需要调用该函数释放
int pthread_cond_destroy(pthread_cond_t* cond);

// 等待条件发生变化时触发
int pthread_cond_wait(pthread_cond_t *restrict cond,
					 pthread_mutex_t *restrict mutex);

// 等待一定时间条件发生变化(等待超时返回ETIMEDOUT)
// 注意这个函数的时间不是等待多久,而是直到下一次时间(当前时间+等待时间)
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
						  pthread_mutex_t *restrict mutex,
						  const struct timespec *restrict tsptr);

// 至少唤醒一个等待该条件变量的线程(虚假唤醒)
int pthread_cond_signal(pthread_cond_t* cond);

// 唤醒所有等待该条件变量的线程
int pthread_cond_broadcast(pthread_cond_t* cond);

// 以上所有函数返回值: 若成功,返回0;否则,返回错误编号

自旋锁

自旋锁与互斥量类似,但它不是通过休眠使进程阻塞(实际上现在的互斥量在等待的时候也会通过一定时间的自旋),而是在获取锁之前一直处于忙等待状态。自旋锁主要应用于锁被持有的时间短,而且线程并不希望在重新调度上花费太多的成本。对于自旋锁可以想象成嵌入式系统中的应用,通过中断的抢占机制进行其他程序的处理。当然如果使用了自旋锁就不要调用会进入休眠状态的函数,如果调用了这些函数会浪费CPU资源,延长自旋锁需要等待的时间。

#include <pthread.h>

// 初始化自旋锁(只有线程进程共享同步属性是自旋锁所拥有的)
// 第二个参数设置进程共享属性
// PTHREAD_PROCESS_SHARED: 自旋锁能被可以访问锁底层内存的线程获取
// PTHREAD_PROCESS_PRIVAET: 自旋锁只能被初始化该锁的进程内部的线程所访问
int pthread_spin_init(pthread_spinlock_t* lock, int pshared);

// 销毁自旋锁
int pthread_spin_destory(pthread_spinlock_t* lock);

// 自旋锁加锁(未获取成功则自旋等待,直到成功获取锁)
int pthread_spin_lock(pthread_spinlock_t* lock);

// 尝试为自旋锁加锁(未获取成功则返回EBUSY,否则返回0)
int pthread_spin_trylock(pthread_spinlock_t* lock);

// 自旋锁解锁
int pthread_spin_unlock(pthread_spinlock_t* lock);

// 以上所有函数返回值: 若成功,返回0;否则,返回错误编号

屏障

屏障是用户协调多个线程并行工作的同步机制。屏障允许线程等待,直到所有的合作线程都达到某一点,然后从该点继续执行。实际上之前的 pthread_join 也是一种屏障,它要求当前线程等待目标线程执行完成之后再继续执行。

#include <pthread.h>

// 初始化屏障
// 第三个参数设定屏障数值,即等待完成的线程个数
// 如果想要重置屏障数值,必须要先释放当前屏障并重新初始化屏障才有效
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
						const pthread_barrierattr_t *restrict attr,
						unsigned int count);

// 销毁屏障
int pthread_barrier_destory(pthread_barrier_t* barrier);

// 表明当前线程已完成任务,返回值非0时需要等待其他线程完成
int pthread_barrier_wait(pthread_barrier_t* barrier);

// 以上所有函数返回值: 若成功,返回0;否则,返回错误编号
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@G.y

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值