Linux多线程-2

“我是我境遇里的起步者。” -- 里尔克《布里格手记》

在上一节Linux多线程当中,我们讲述了Linux中线程的概念以及线程的控制内容。这一篇博客承接上次内容,我们来对线程安全进行阐述。

目录

1.线程安全

1.1互斥的实现

1.1.1互斥锁实现互斥

1.1.2互斥锁相关接口

1.1.3互斥锁的死锁情况

1.2同步的实现

1.2.1条件变量实现同步

1.2.2条件变量相关接口

1.2.3信号量实现同步

1.线程安全

在多线程程序当中,当我们涉及到共享资源的操作时,可能会导致数据二义性造成结果不可控现象。那么对于线程安全而言,便是当我们对共享资源进行操作的时候,不会造成数据的二义性。简而言之,线程安全指的是多线程中对共享资源的操作不会出现问题。

那么我们该通过何种方式来保证线程安全,即互斥和同步。

  • 互斥:同一时间执行流对资源的操作唯一,保证访问安全性;
  • 同步:通过条件控制,让多执行流对资源的获取更加合理。

1.1互斥的实现

互斥的实现有较多方式:互斥锁,读写锁,自旋锁……我们详细讲述互斥锁的内容:

1.1.1互斥锁实现互斥

讨论互斥锁的原理之前,首先我们来谈一谈互斥锁的本质:0/1计数器,即通过0和1来标记资源的可访问和不可访问状态。(0-不可访问,1-可访问)

当访问资源之前进行加锁操作(通过状态判断是否可访问,不可访问则阻塞);在访问资源之后进行解锁操作(将资源置为可访问状态,唤醒其他被阻塞的进程)。

对于多线程而言,当我们对资源进行互斥锁管理的时候,就必须保证多个线程访问的是同一个互斥锁(加锁的是同一个共享资源),否则多个线程对于资源的访问时没有收到限制的。

互斥锁的操作本身必须是安全的:互斥锁本身计数器操作是原子操作。那么对于互斥锁本身是如何实现安全的,我们做以下讨论:

在具体的实现当中,一个变量的数据加载到内存之中不是瞬间的,而是需要一个过程。所以万一出现在某一线程对共享资源的访问中,还未曾将计数器的1修改为0时,线程切换,第二个线程要对共享资源进行访问,它会发现共享资源还未被置为不可访问状态,于是它也可以对共享资源进行访问。

就类似于:当一号线程进门之后正准备访问资源和关门,但是线程切换,二号线程发现共享资源的门是开着的,于是它也就进门对共享资源进行操作。

所以为了避免上述问题的出现,我们在线程访问cpu中加入寄存器,并通过exchange操作来交换寄存器和内存中的数据,即:

  1. 先将指定寄存器中的值修改为0;
  2. 将寄存器中与内存的数据进行交换;
  3. 判断相关条件是否能够加锁或者去锁。

此时,当一个线程对共享资源进行访问还没来得及加锁,线程切换后其他线程看到的依旧是0,即其他线程无法进行访问。

1.1.2互斥锁相关接口

int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexatter_t *attr);

pthread_mutex_init接口用于实现互斥锁的初始化,其中mutex是互斥锁变量的地址,attr:互斥锁变量属性,通常置为NULL。

int pthread_mutex_lock(pthread_mutex_t *mutex);

 pthread_mutex_lock是阻塞加锁接口,如果加不上锁,则一直等待,直到加锁成功;

int pthread_mutex_trylock(pthread_mutex_t *mutex);

pthread_mutex_trylock是非阻塞加锁接口,如果加锁不成功,则立即报错返回。

int pthread_mutex_unlock(pthread_mutex_t *mutex);

 pthread_mutex_unlock用于访问资源完毕时解锁。

int pthread_mutex_destroy(pthread_mutex_t *mutex);

pthread_mutex_destroy用于释放资源时销毁锁。 

了解完接口之后,我们照例来进行代码演示:

 我们对上述存在多线程的代码编译并执行我们会发现:

怎么会存在第100位数据被重复获取,这就联系到了我们今天的内容,即线程安全。上述代码展示出来的结果暴露了一个问题,即共享资源被多线程访问的不安全问题,那么结合互斥锁的内容,我们对其进行改进和优化:将判断共享资源剩余和获取共享资源保护起来,中间不可被打断。

加入互斥锁后,需要注意在任何线程可能退出的位置都需要解锁。并且在一个线程获取完资源之后,我们加入ulseep保证时间片的分配,否则结合加锁解锁操作,始终会是一个线程对资源进行访问。最后编译执行得到的结果如下:

很好的展现了互斥锁在线程安全中的作用。 

1.1.3互斥锁的死锁情况

死锁是多个线程对锁资源的争抢不当导致程序流卡死,程序无法继续向前推进的情况。

死锁产生有两种简单的情况,即:加锁之后未解锁导致其他线程无法访问资源导致程序卡死,或是多锁使用时,加锁顺序和解锁顺序不一致。

但实际使用过程中死锁的产生原有远不止上述简单的两种,更可能是以下死锁产生的必要条件:

  1. 互斥条件 -- 同一时间一个锁只能被一个线程获取,多个线程无法访问同一个锁;
  2. 不可剥夺条件 -- 一个线程加的锁,只能由该线程解锁,其他线程无法释放;
  3. 请求与保持条件 -- 线程加了A锁之后又去请求B锁,如果B锁无法请求也不释放A锁;
  4. 环路等待条件 -- 线程1加了A锁请求B锁,线程2加了B锁请求A锁。

死锁的预防:破坏死锁产生的必要条件

  1. 多个线程加锁和解锁的顺序保持一致,尽可能避免环路条件的产生;
  2. 采用非阻塞加锁,如果无法成功加锁,则将已经加锁成功的释放。 -- 破坏请求与保持条件

死锁的避免:当死锁已经产生的解决方案

  1. 银行家算法;
  2. 死锁检测算法;
  3. ……

1.2同步的实现

同步是为了让资源的分配更加合理,但是资源分配的合理不会保证资源分配的安全,这是我们需要注意的点,具体的同步实现方式有:条件变量、信号量……

1.2.1条件变量实现同步

条件变量主要是提供一个pcb等待队列,以及阻塞和唤醒阻塞线程的接口。其实现原理如下:

  1. 线程1对资源获取条件进行判断,若线程不满足获取条件,则调用阻塞接口阻塞线程;
  2. 线程2促使资源获取条件满足之后,通过唤醒接口唤醒被阻塞的线程。

值得注意的是,一个线程是否满足获取资源条件或是可以唤醒被阻塞线程的条件,需要程序员我们自己来判断。而当在多个线程操作中,条件操作本质上是一个临界资源的操作,那么便需要加锁来进行保护。

所以总而言之,条件变量是和互斥锁搭配进行使用的。

1.2.2条件变量相关接口

定义条件变量的变量类型:pthread_cond_t 

int pthread_cond_init(pthread_cond_t *cond, pthread_condattr *attr);

pthread_cond_init是初始化接口,其中cond是条件变量的地址,attr是条件变量的属性,一般置为NULL。

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

pthread_cond_wait是阻塞等待接口,如果等不到资源则一直等待。

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, struct timespec *t);

pthread_cond_timedwait是有时长的阻塞等待接口,如果在t时间内未等到资源,则结束。

返回值为:ETIMEDOUT则表示时间结束时仍未获取到资源。

int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_signal是唤醒至少一个阻塞队列中的线程。

int pthread_cond_broadcast(pthread_cond_t *cond);

pthread_cond_broadcast是唤醒等待队列中的所有线程。

int pthread_cond_destroy(pthread_cond_t *cond);

pthread_cond_destroy用于释放资源。

在大致了解完相关接口之后,我们照例来通过代码对内容进行实践。

对cond.c编译并执行可到结果如下:

我们可以很直观的看出,资源的获取和生产是交替打印的,这便很好的体现了条件变量的同步,即线程交替运行。

1.2.3信号量实现同步

由于本篇博客内容较长,所以对于这部分内容,我们在下一篇多线程博客中再进行讨论。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值