线程操作
创建线程
描述: 创建一个新线程
原型:
#include
int pthread_create(pthread_t thread, const pthread_attr_t attr, void (start_routine) (void ), void arg);
返回值:
成功返回0
失败返回错误号
参数:
thread:传出参数,保存线程ID
attr:通常传NULL,表示使用线程的默认属性.
start_routine:函数指针,指向线程主函数(线程体).该函数运行结束,则线程结束.
arg:线程主函数执行期间所使用的参数
由于pthread_create返回的错误码不保存在errno中,因此不能直接使用perror()打印错误信息,可以先用strerror()把错误码转换成错误信息再打印.
如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止.
Compile and link with -pthread.
线程退出
在线程中禁止调用exit函数, 否则导致整个进程退出, 取而代之的是调用pthread_exit函数, 此函数只会使调用线程退出, 不影响其他线程. 主线程调用pthread_exit函数也是如此.
描述:线程退出
原型:
#include
void pthread_exit(void *retval);
返回值:
参数:
retval: 传出参数. 线程退出状态, 通常传NULL
pthread_exit或者return返回的指针所指向的内存单元必须是全局或malloc分配,不能是在栈上分配.因为当其他线程得到这个返回指针时,栈空间已被回收.
C语言中
1.任何一个线程调用了exit()都会导致进程退出. [ 子线程调用代码 ] [ 主线程调用代码 ]
2.子线程调用return 或 pthread_exit并不会导致进程退出, 且子线程正常退出. [ 代码 ]
3.主线程调用return会导致进程退出. [ 代码 ]
4.主线程调用pthread_exit并不会导致进程退出, 但主线程已退出且处于僵尸状态. 主线程会等待所有子线程执行完毕后才结束进程. [ 代码 ]
1.Java中的main线程是JVM进程的第二个线程 .
2.即便Java中的main线程退出了,而与之一一对应的底层线程还没有退出. 底层线程阻塞在 futex(0x7ffca800c078, FUTEX_WAIT_PRIVATE, 0, NULL 它会等待所有子线程执行完毕后才退出.
3.main线程退出 -> 第一个线程退出 -> JVM进程结束
线程回收
描述:阻塞等待线程退出,获取线程退出状态. 类似进程中的waitpid()函数.
原型:
#include
int pthread_join(pthread_t thread, void **retval);
返回值:
成功返回0
失败返回错误号
参数:
thread: 待回收线程
retval: 传出参数. 存储线程退出状态,整个指针和pthread_exit函数的参数是同一块内存地址.
线程分离
线程分离状态: 指定该状态,线程主动与主控线程断开关系. 线程退出后,其退出状态不由其他线程获取,而由自己主动释放. 网络,多线程服务器常用.
描述:实现线程分离
原型:
#include
int pthread_detach(pthread_t thread);
返回值:
成功返回0
失败返回错误号
参数:
thread: 待分离线程
一般情况下,线程退出后,其退出状态一直保留到其他线程调用pthread_join获取它的状态为止.但是线程也可以设置detach状态,这样线程一旦退出时系统会自动回收资源,而不保留退出状态.
不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误,即已经对一个线程调用了pthread_detach函数就不能再调用pthread_join函数.
如果在创建线程时没有设置线程的属性为PTHREAD_CREATE_DETACHED,则线程默认是可结合的. 可结合的线程在线程退出后不会立即释放资源,必须调用pthread_join等待线程退出,并释放所占用的资源. 分离的线程在线程退出时系统会自动回收资源.
在线程中调用pthread_detach(pthread_self())
在主线程中调用pthread_detach(thread)
取消线程
描述:取消线程. 类似进程中的kill()函数.
原型:
#include
int pthread_cancel(pthread_t thread);
返回值:
成功返回0
失败返回错误号
参数:
thread:待取消线程
线程取消并不是实时的,而有一定的延时.需要等待线程到达某个取消点(检查点).
取消点:
线程检查是否被取消,并按请求进行动作的一个位置. 通常是一些系统调用(如create,open,pause,close,read,write…),可以粗略认为一个系统调用(进入内核)就是一个取消点.
通过man 7 pthreads查看具备取消点的系统调用列表.
通过调用pthread_testcancel()函数设置一个取消点.
原型:
#include
void pthread_testcancel(void);
比较线程
描述:比较两个线程ID是否相等
原型:
#include
int pthread_equal(pthread_t t1, pthread_t t2);
返回值:
如果相等返回非0值
如果不等返回0值
进程函数和线程函数比较
进程 | 线程 |
---|---|
fork | pthread_create |
exit | pthread_exit |
wait,waitpid | pthread_join |
kill | pthread_cancel |
getpid | pthread_self |
include
// 获取内核级别的线程ID
pid_t tid = syscall(SYS_gettid);
主线程内存分配
1.进入main方法之前, 主线程已经分配132KB的堆空间内存
2.当主线程调用malloc(size),且现有内存空间不足时
2.1 if size < 128KB, 内部调用brk在堆内存空间分配内存
2.2 if size > 128KB, 内部调用mmap在内存映射空间分配内存. 当调用free函数时, 内部直接调用munmap函数将内存归还给操作系统
由M_MMAP_THRESHOLD = 128KB选项调节 . brk分配的内存需要等到高地址内存释放以后才能释放低地址内存 . 当高地址空闲内存超过128KB(由M_TRIM_THRESHOLD选项调节)时, 执行内存紧缩(trim)操作 . mmap分配的内存可以单独释放 .
子线程内存分配
1.调用malloc(size)时, 内部调用mmap在内存映射空间分配内存
2.子线程的栈空间和堆空间都在内存映射空间 验证
栈空间默认8MB
[注] clone & exit
clone(child_stack=0x7f8f5ae0ffb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f8f5ae109d0, tls=0x7f8f5ae10700, child_tidptr=0x7f8f5ae109d0) = 1936
exit(0)
exit_group(0)
[附] 进程内存布局案例
infuq-zone\重要-操作系统\images\虚拟内存空间-C程序.png
线程属性
线程同步(C库函数)
C库函数的互斥锁,读写锁,条件变量和信号量的底层均使用内核Linux futex机制实现 .
获取锁的一般流程
1.使用CAS操作变量值(0->1) [用户态]
2.如果操作成功则直接返回,如果操作失败则自旋一定次数. [用户态]
3.如果自旋一定次数依然失败,则调用futex_wait进入内核态,将线程挂起并入队,等待释放锁的线程调用futex_wake. [内核态]
前两步操作在用户态,只有第三步操作在内核态 . 用户态也可以有CAS操作,并不是只有内核态才有CAS操作 .
互斥锁
pthread_mutex_t mutex 只有0和1两种取值
描述: 初始化一个互斥锁
原型:
#include
int pthread_mutex_init(pthread_mutex_t restrict mutex, const pthread_mutexattr_t restrict attr);
返回值:
参数:
mutex: 传出参数. 调用时传&mutex.
attr: 传入参数. 互斥锁属性,通常传NULL,选用默认属性(线程间共享)
静态初始化: 如果互斥锁mutex是静态分配(全局定义,或static关键字修饰),可以直接使用宏进行初始化.
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态初始化: 局部变量采用动态初始化
pthread_mutex_init(&mutex, NULL)
描述: 销毁一个互斥锁
原型:
#include
int pthread_mutex_destroy(pthread_mutex_t *mutex);
描述: 加锁
原型:
#include
int pthread_mutex_lock(pthread_mutex_t mutex); 阻塞
int pthread_mutex_trylock(pthread_mutex_t mutex); 非阻塞
pthread_mutex_lock内部实现
{
lll_lock ((mutex)->data.lock, PTHREAD_MUTEX_PSHARED (mutex)) -> lll_lock_wait_private
}
void __lll_lock_wait_private (int futex)
{
if (futex == 2)
lll_futex_wait (futex, 2, LLL_PRIVATE);
while (atomic_exchange_acq (futex, 2) != 0)
lll_futex_wait (futex, 2, LLL_PRIVATE);
}
glibc-2.14
描述: 解锁
原型:
#include
int pthread_mutex_unlock(pthread_mutex_t *mutex);
说明:
解锁之后,会将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级,调度等.默认先阻塞先唤醒.
参考外链
1.https://blog.csdn.net/tlxamulet/article/details/79047717
自旋锁
pthread_spin_lock
pthread_spin_trylock
pthread_spin_unlock
读写锁
写独占, 读共享
某线程已加写锁,其他线程对该锁加锁(读锁或写锁)都会被阻塞.
某线程已加读锁,其他线程对该锁加读锁会成功.
某线程已加读锁,其他线程对该锁加写锁会被阻塞.
某线程已加读锁,其他线程既有对该锁加读锁,也有对该锁加写锁.则优先满足写模式锁,阻塞读模式锁请求.
pthread_rwlock_t rwlock
描述: 初始化一个读写锁
原型:
#include
int pthread_rwlock_init(pthread_rwlock_t restrict rwlock, const pthread_rwlockattr_t restrict attr);
参数:
rwlock:
attr: 传入参数. 读写锁属性,通常传NULL,选用默认属性.
描述: 销毁一个读写锁
原型:
#include
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
描述: 加读锁
原型:
#include
int pthread_rwlock_rdlock(pthread_rwlock_t rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t rwlock);
描述: 加写锁
原型:
#include
int pthread_rwlock_wrlock(pthread_rwlock_t rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t rwlock);
描述: 解锁
原型:
#include
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
条件变量
条件变量不是锁,但它也可以造成线程阻塞,等条件满足时解除阻塞.
通常与互斥锁配合使用.
pthread_cond_t cond
描述: 初始化一个条件变量
原型:
#include
int pthread_cond_init(pthread_cond_t restrict cond, const pthread_condattr_t restrict attr);
返回值:
成功返回0
失败返回错误号
参数:
cond:
attr: 传入参数. 条件变量属性,通常传NULL,选用默认属性.
描述: 销毁一个条件变量
原型:
#include
int pthread_cond_destroy(pthread_cond_t *cond);
返回值:
成功返回0
失败返回错误号
描述: 条件不满足时线程阻塞,并释放锁. 条件满足时,线程解除阻塞,需要重新获取锁.
原型:
#include
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);
返回值:
成功返回0
失败返回错误号
pthread_cond_wait内部实现
{
/ Wait until woken by signal or broadcast. /
lll_futex_wait (&cond->data.__futex, futex_val, pshared);
}
glibc-2.14
描述: 唤醒至少一个阻塞在该条件变量上的线程
原型:
#include
int pthread_cond_signal(pthread_cond_t *cond);
发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行. 如果没有处于阻塞等待状态的线程,则pthread_cond_signal也会成功返回.
返回值:
成功返回0
失败返回错误号
实现生产者-消费者方式
1.互斥量&条件变量
2.信号量
参考外链
1.Java实现生产者-消费者三种方式 [ 文档 ]
2.__pthread_cond_wait [ glibc-2.14 ]
3.https://blog.51cto.com/u_1038741/1939661
信号量
sem_t sem
描述: 初始化信号量
原型:
#include
int sem_init(sem_t *sem, int pshared, unsigned int value);
返回值:
成功返回0.
失败返回-1,并设置errno值.
参数:
sem:
pshared: 0表示线程同步, 1表示进程同步
value: 最多有几个线程操作共享数据
描述: 此函数相当于sem—, 当sem=0时,线程被阻塞
原型:
#include
int sem_wait(sem_t sem); 阻塞
int sem_trywait(sem_t sem); 非阻塞
int sem_timedwait(sem_t sem, const struct timespec abs_timeout); 阻塞一定时间
返回值:
成功返回0.
失败返回-1,并设置errno值.
原型: 此函数相当于sem++
描述:
#include
int sem_post(sem_t *sem);
返回值:
成功返回0.
失败返回-1,并设置errno值.
描述: 销毁信号量
原型:
#include
int sem_destroy(sem_t *sem);
返回值:
成功返回0.
失败返回-1,并设置errno值.
内核级同步机制
Futex机制
Fast Userspace Mutexes
内核会维护一个与用户空间锁变量uaddr配对的等待队列.
这种机制核心思想是通过将大多数情况下非同时竞争锁的操作放在用户态执行,而不是代价昂贵的内核系统调用方式执行,从而提高效率 . 出现竞争时才通过系统调用方式进入内核态 .
参考外链
1.https://www.jianshu.com/p/604d277cbc6f
互斥锁
互斥锁允许task睡眠 互斥锁基于自旋锁实现
linux-4.19.196\include\linux\mutex.h
<br />struct mutex {<br /> atomic_long_t owner; // 互斥锁的持有者<br /> spinlock_t wait_lock; /* 自旋锁 */<br />#ifdef CONFIG_MUTEX_SPIN_ON_OWNER<br /> struct optimistic_spin_queue osq; /* Spinner MCS lock */<br />#endif<br /> struct list_head wait_list; /* 等待队列 */<br />#ifdef CONFIG_DEBUG_MUTEXES<br /> void *magic;<br />#endif<br />#ifdef CONFIG_DEBUG_LOCK_ALLOC<br /> struct lockdep_map dep_map;<br />#endif<br />};<br />
信号量
信号量允许task睡眠 允许多个task进入临界区代码执行 信号量基于自旋锁实现
linux-4.19.196\include\linux\semaphore.h<br />struct semaphore {<br /> raw_spinlock_t lock; // 自旋锁<br /> unsigned int count;<br /> struct list_head wait_list; // 等待队列<br />};<br />
```
void down(struct semaphore *sem)
{
unsigned long flags;
// 获取自旋锁<br /> raw_spin_lock_irqsave(&sem->lock, flags);<br /> if (likely(sem->count > 0))<br /> // 信号量减一<br /> sem->count--;<br /> else<br /> // 加入等待队列并释放锁<br /> __down(sem);<br /> // 释放自旋锁<br /> raw_spin_unlock_irqrestore(&sem->lock, flags);<br />}<br />```
自旋锁
忙等机制 适用于临界资源被锁定时间很短的情况 自旋锁不允许task睡眠 一次只能有一个task获取锁并进入临界区
使用场景 : 操作系统内核的并发数据结构(短临界区)
原子操作 + 无限循环
v2.6.39.3
Intel x86体系加锁操作
Intel x86体系释放锁操作
原子操作 + 被挂起
v4.19.196
内部包含原子操作
infuq-zone\重要-多线程\images\自旋锁.png
原子变量
原子操作
[ 附 ] 原子操作 - 原子性
单核 | - | 关中断 | 说明 |
---|---|---|---|
多核 | Intel x86 | LOCK# | |
ARM | LDREX STREX |
多核平台,关中断操作只能关闭本核中断 . 所有的原子操作都需要用到和CPU体系结构相关的汇编,而且一定要CPU支持 .
只有ADD,ADC,AND,BTC,BTR,BTS,CMPXCHG,DEC,INC,NEG,NOT,OR,SUB,SBB,XOR,XADD,XCHG指令前面可以加LOCK指令,可以独占性地内存访问(排他),在这段期间指令执行不会被打断,实现原子操作. -> CPU提供的原子操作指令
CPU芯片上有一条引线#HLOCK pin . 如果汇编语言在一条指令前面加上前缀LOCK,经过汇编之后的机器代码就会使CPU在执行这条指令的时候把#HLOCK pin的电位拉低, 持续到这条指令结束时放开,从而把总线锁住. 这样同一总线上的其他CPU暂时不能通过总线访问内存,保证了这条指令在多处理器环境中的原子性.
LDREX,STREX和CLREX三条汇编指令可以实现独占性地内存访问(排他),实现原子操作.
执行LDREX的CPU会将访问的内存地址标记为独占,随后执行STREX时会检查这个标记,如果标记存在,说明没有CPU访问这个内存地址,则会将数据写到内存,取消独占标记,并返回0,表示更新成功,如果标记不存在,说明此内存地址的值被修改过,则不会将数据写入内存,此时返回1,表示更新失败,需要重新执行LDREX和STREX写入数据.
[ 附 ] 内存屏障 - 可见性
CPU缓存在提高性能的同时也导致缓存可见性问题,解决方案如下
1.总线锁 + CPU嗅探
嗅探带有LOCK汇编指令的操作
2.缓存一致性协议MESI, 以及优化MESI引入Store Buffer和Invalidate Queues .
在未引入Store Buffer和Invalidate Queues之前, 缓存之间的数据强一致, 而引入之后, 缓存之间的数据变成弱一致性.
https://www.scss.tcd.ie/Jeremy.Jones/VivioJS/caches/MESIHelp.htm
http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.07.23a.pdf
为了解决数据弱一致性, 采用内存屏障,具体如下
1.写屏障 Store Barrier
处理器#1在写入数据之后必须将Store Buffer中的数据冲刷到缓存, MESI可以保证对其他处理器可见. [ 数据冲刷到缓存 ]
写指令 写屏障 其他指令
2.读屏障 Load Barrier
处理器#2在读取数据之前必须将Invalidate Queue中对应地址的缓存行全部标记为失效, 保证该处理器接下来读取对应地址的数据可以得到最新值. [ 缓存行失效 ]
读屏障 读指令 其他指令
3.LOCK指令
参考外链
1.https://blog.csdn.net/dai_xiangjun/article/details/122208908
2.https://zhuanlan.zhihu.com/p/84500221
[ 附 ] 内存屏障 - 重排序
防止编译时指令重排
防止运行时指令重排
[ 附图 ] 内存屏障
\infuq-zone\重要-多线程\images\内存屏障.png