Linux:线程互斥

进程线程互斥相关概念

临界资源:多线程执行流共享的资源叫做临界资源
临界区:每个线程内部,访问临界资源的代码,叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
原子性:不会被任何调度机制打断的操作,该操作只有两种状态:要么完成,要么未完成
例如实现售票系统:

#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include <stdlib.h>
int tickets=200;
void *GetTicket(void *arg)
{
    while(1)
    {
        if(tickets>0)//说明有票
        {
            usleep(100000);//以微秒来睡眠
            printf(" get a ticket no is : %d\n",tickets--);
        }
        else
        {
            printf("%s............quit\n",(char*)arg);
            break;
        }
    }
    pthread_exit((void*)0);
}
int main()
{
    pthread_t tid1,tid2,tid3,tid4;
    pthread_create(&tid1,NULL,GetTicket,(void*)"thread 1");
    pthread_create(&tid2,NULL,GetTicket,(void*)"thread 2");
    pthread_create(&tid3,NULL,GetTicket,(void*)"thread 3");
    pthread_create(&tid4,NULL,GetTicket,(void*)"thread 4");

    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_join(tid3,NULL);
    pthread_join(tid4,NULL);
}

它的执行结果是:

 get a ticket no is : 200
 get a ticket no is : 199
 get a ticket no is : 198
 get a ticket no is : 197
 get a ticket no is : 196
 get a ticket no is : 195
 get a ticket no is : 194
 get a ticket no is : 193
 ...
 get a ticket no is : 4
 get a ticket no is : 3
 get a ticket no is : 2
 get a ticket no is : 1
thread 3............quit
 get a ticket no is : 0
thread 4............quit
 get a ticket no is : -1
thread 2............quit
 get a ticket no is : -2
thread 1............quit
段错误(吐核)

一共有200张票,但是卖出去的票比200多,可能有多个线程同时访问tickets这个临界资源,if语句判断条件为真后,代码可以并发的切换到其他线程,usleep的漫长过程中,可能有很多个线程进入该代码,并且ticket–这个操作本身也不是一个原子操作,而是对应3条汇编指令:
(1)将共享变量tickets从内存加载到CPU内部的寄存器中;(CPU内部有若干类寄存器,通用的有状态寄存器,指令寄存器、程序计数器
(2)更新CPU内部的寄存器中的值,执行-1操作;
(3)将新值从CPU寄存器写回共享变量的内存地址中。
因此,要解决上述问题,要做到
(1)代码必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区
(2)如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区
(3)如果线程不在临界区执行,那么该线程不能阻止其他线程进入临界区
上述这三点就是一把锁,Linux提供的这把锁叫做互斥量

互斥量

在大部分情况下,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这样变量归属于单个线程,其他线程无法获得这种变量;但是有时候,很多变量要在线程间共享,这样的变量称为共享变量(上述售票系统中的tickets就是共享变量),可以通过数据的共享完成线程之间的交互,这同时也会带来上述的问题。
互斥量就是一把锁,如图:
在这里插入图片描述
给临界区加上锁,保证了线程对临界区的原子性。

  • 互斥量的接口
    1、初始化互斥量
    初始化互斥量有两种方法:
    (1)静态分配
    例如:
 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

使用PTHREAD_MUTEX_INITALIZER这个宏来进行初始化
(2)动态分配

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);

其中参数mutex表示要初始化的互斥量,attr表示属性,一般是NULL;
2、销毁互斥量
函数原型是:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

注意:
(1)使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁;
(2)不要销毁一个已经加锁的互斥量
(3)已经销毁的互斥量,确保后面不会再有线程尝试加锁
3、互斥量加锁和解锁
函数原型是:

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数mutex表示互斥量。
在调用pthread_mutex_lock时,会遇到以下情况:
(1)互斥量未锁,该函数将互斥量锁定,同时返回成功;
(2)发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_mutex_lock调用陷入阻塞(执行流挂起),等待互斥量解锁。
这样我们就可以给刚刚的售票系统加锁,例如:

#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include <stdlib.h>
int tickets=200;
pthread_mutex_t lock;//创建锁
void *GetTicket(void *arg)
{
    while(1)
    {
        pthread_mutex_lock(&lock);
        if(tickets>0)//说明有票
        {
            usleep(100000);//以微秒来睡眠
            printf(" get a ticket no is : %d\n",tickets--);
            pthread_mutex_unlock(&lock);//解锁
        }
        else
        {
            printf("%s............quit\n",(char*)arg);
            pthread_mutex_unlock(&lock);
            break;
        }
    }
    pthread_exit((void*)0);
}
int main()
{
    pthread_mutex_init(&lock,NULL);//初始化互斥量
    pthread_t tid1,tid2,tid3,tid4;
    pthread_create(&tid1,NULL,GetTicket,(void*)"thread 1");
    pthread_create(&tid1,NULL,GetTicket,(void*)"thread 2");
    pthread_create(&tid1,NULL,GetTicket,(void*)"thread 3");
    pthread_create(&tid1,NULL,GetTicket,(void*)"thread 4");

    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_join(tid3,NULL);
    pthread_join(tid4,NULL);
    pthread_mutex_destroy(&lock);//销毁互斥量
}

最终就不会有多余的票,而且运行速度变慢,因为实现了加锁和解锁,加锁会占用系统资源程序运行时性能变低,lock的加锁和解锁一定要做到原子性,因为它也是临界资源(每个线程在访问临界区之前都要先访问锁,因此是临界资源)。

  • 互斥量实现原理
    单纯的i++或者++i不是原子的,可能会出现数据一致性问题,为了实现互斥锁的操作,大多数体系结构都提供了swap或者exchange指令,该指令作用是将CPU寄存器和内存单元的数据相交换,因为只有一条指令,保证了原子性;
    假设Mutex变量的值为1表示互斥锁空闲,这时某个进程调用lock可以获得锁,而Mutex的值为0表示互斥锁已经被某个线程获得,其他线程再调用lock只能挂起等待,我们来看一段lock和unlock的伪代码:
lock:
    if(mutex>0)
    {
        mutex=0;
        return 0;
    }
    else
    //挂起等待
    goto lock;
unlock:
    mutex=1;
    //唤醒等待Mutex的线程
    return 0;

因为unlock过程一定是原子的,所以注意lock过程,unlock操作中唤醒等待线程的步骤可以有不同的实现,可以只唤醒一个等待线程,也可以唤醒所有等待该Mutex的线程,然后让被唤醒的这些线程去竞争获得Mutex,竞争失败的线程继续等待。
但是上述伪代码中lock函数中对Mutex的读取、判断和修改是非原子操作,如果两个线程同时调用lock,这时Mutex是1,而两个线程都判断mutex>0成立,然后其中一个线程置mutex=0,另一个线程并不知道这一情况,也置mutex=0,于是两个线程都以为自己获得了锁,因为线程在任何时候都可能触发导致切换,而在每个线程运行时,mutex都会首先被读到CPU里的寄存器中,这将导致mutex的副本过多,数据无法保持一致性,因此这种伪代码的实现是错误的。
因此来看另一种实现方法
在这里插入图片描述
首先利用汇编指令令xchgb将已经置为0的寄存器a1与互斥量mutex值进行交换,只对寄存器操作,不会产生物理内存中mutex的副本,这样即使线程在执行xchgb指令和条件判断时被切换出去,也是没有任何意义的,对程序结果没有影响,unlock中释放锁操作同样只用一条指令实现,保证了它的原子性,这就是lock和unlock的实现原理。
注意:每个Mutex都有一个等待队列,一个线程要在Mutex上挂起等待,首先要把自己(PCB)加入等待队列中,然后置线程状态为睡眠状态,然后调用调度器函数切换到别的线程,一个线程要唤醒等待队列中的其它线程,只需要从等待队列中取出一项,把它的状态从睡眠改为就绪,加入就绪队列,那么下次调度器函数执行时就有可能切换到被唤醒的线程。
源代码(github):
https://github.com/wangbiy/Linux2/commit/eb12b0ba74896d0593f374c3a3ea2287f894bf8e

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值