Linux-互斥

这次,我们来讲解关于互斥、锁的知识点!

https://blog.youkuaiyun.com/go_bai/article/details/154755520?spm=1001.2014.3001.5501

之前,我们讲到过一点点关于锁的概念了。

生成汇编代码的指令(了解)

- 指令: objdump -d a.out > test.objdump 
​
1.  objdump -d :是Linux下的反汇编工具, -d 参数表示对程序中的代码段进行反汇编,生成汇编指令;
​
2.  a.out :是目标程序(C/C++编译后的默认输出文件);
​
3.  > test.objdump :将反汇编结果重定向输出到 test.objdump 文件中。

锁的概念:

我们得明白:

加锁的本质:是用时间来换取安全的!

加锁的表现:线程对于临界区代码串行执行。

加锁原则:尽量的要保证临界区代码,越少越好!

1. 锁的本质
锁本身是共享资源,因此申请锁(加锁)、释放锁(解锁)必须是原子性操作(保证操作不会被中断)。(ps:关于原子概念,我们之前讲到过了,看上面的链接)
2. 互斥锁的核心逻辑
- 作用:让所有线程按顺序获取共享资源(实现同步),解决资源互斥访问的问题。
- 场景:适合“纯互斥”场景(资源不能同时被多个线程访问),但锁分配不合理可能导致饥饿问题(并非所有互斥都会饥饿)。
3. 互斥锁的规则(“观察员”逻辑)
- 外部线程必须排队等待锁;
- 释放锁的线程不能立即重新申请,需排到队列尾部(避免长期占用)。
4. 临界区的特性
- 线程进入临界区(加锁后)可以被切换,但切换时会持有锁;
- 其他线程在当前线程释放锁前,无法进入临界区(临界区访问对其他线程是“原子性”的)。

我们现在来改进一下上一篇我们所出现的抢票问题:

#define NUM 5
int ticket = 1000;
//定义锁变量
pthread_mutex_t mutex;

class ThreadData
{
public:
    ThreadData(int num)
    {
        _threadname = "thread-" + std::to_string(num);
    }

    // private:
public:
    std::string _threadname;
};

void *mythread(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    const char *name = td->_threadname.c_str();
    while (true)
    {
        //加锁
        pthread_mutex_lock(&mutex);
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s,get a ticket:%d\n", name, ticket);
            ticket--;
            //释放锁
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return nullptr;
}

int main()
{
    //初始化
    pthread_mutex_init(&mutex, nullptr);

    std::vector<pthread_t> pids;
    std::vector<ThreadData *> thread_datas;
    for (int i = 0; i < NUM; i++)
    {
        pthread_t pid;
        ThreadData *td = new ThreadData(i);

        thread_datas.push_back(td);
        pthread_create(&pid, nullptr, mythread, thread_datas[i]);
        pids.push_back(pid);
    }
    for (int i = 0; i < NUM; i++)
    {
        pthread_join(pids[i], nullptr);
    }
    for (int i = 0; i < NUM; i++)
    {
        delete thread_datas[i];
    }
    //销毁锁
    pthread_mutex_destroy(&mutex);

    return 0;
}

当我们写出上面的代码,虽然解决了数据错乱的问题,但我们运行后发现,他会造成锁竞争,总是被一个线程持有不放!

我们加上sched_yield().表示放弃CPU.

简单了解完锁的应用后(只是见见锁,后面我们再具体讲讲锁的常见接口,以及C++里的锁接口,一并讲了)

锁的原理:

1. 原子性的定义
- 单条汇编语句是原子的(不会被中断);
- 像  tickets--  这类操作不是原子的,会被拆分为3条汇编指令。
2. 加锁( lock )的底层逻辑


操作流程:

加锁(lock)流程
1. 准备操作
线程执行  movb $0, %al  → 把0存入CPU的al寄存器(线程私有)
2. 原子交换
执行  xchgb %al, mutex  → 将al寄存器的0与内存中 mutex (锁变量)的值交换
(此步骤是原子的,不会被中断)
3. 结果判断
- 若al寄存器内容 > 0 → 加锁成功,线程继续执行
- 若al寄存器内容 = 0 → 加锁失败,线程挂起等待,之后跳转到 lock 步骤重试
解锁(unlock)流程
1. 释放锁标记
执行  movb $1, mutex  → 把1写回内存中的 mutex (表示锁已释放)
2. 唤醒等待线程
唤醒所有等待该 mutex 锁的线程
3. 完成解锁
返回0 → 解锁成功
3. 加锁的本质
把共享的锁变量(内存中的 mutex ),通过一条汇编指令交换到线程的**私有硬件上下文(寄存器)**中,从而保证“获取锁”操作的原子性。
- 寄存器是线程私有的,线程切换后回来会继续执行 xchgb 操作;
- 锁的核心是通过“原子交换”,让线程“持有”锁(锁变量进入线程私有上下文)。

可重入与线程安全:

在信号章节,我们介绍了一个可重入与不可重入的概念,并且举了一样高不可以重入的例子:

现在,我们再来认识一下:

重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题

常见的线程不安全的情况:

不保护共享变量的函数(我们上一篇的抢票机制)

函数状态随着被调用,状态发生变化的函数返回指向静态变量指针的函数

调用线程不安全函数的函数

常见线程安全的情况:

每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的类或者接口对于线程来说都是原子操作

多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况:

调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的

调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构可重入函数体内使用了静态的数据结构

常见可重入的情况:

不使用全局变量或静态变量

不使用用malloc或者new开辟出的空间

不调用不可重入函数

不返回静态或全局数据,所有数据都有函数的调用者提供

使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全的联系:

函数是可重入的,那就是线程安全的

函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

可重入与线程安全的区别:

可重入函数是线程安全函数的一种

线程安全不一定是可重入的,而可重入函数则一定是线程安全的。

如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的

死锁:

什么是死锁?

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。

现在,我们使用代码的形式:演示一下关于死锁的情况:

示例:

//定义两把互斥锁
pthread_mutex_t lock1=PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2=PTHREAD_MUTEX_INITIALIZER;
void*mythread1(void*args)
{
    pthread_mutex_lock(&lock1);
    printf("mythread1:拿到lock1,等待lock2\n");
    sleep(1); //故意延迟,让线程2有时间拿到lock2
    pthread_mutex_lock(&lock2); //线程1等待lock2(被线程2持有)
    printf("mythread1:拿到lock1");
    pthread_mutex_unlock(&lock2);
    pthread_mutex_unlock(&lock1);  
    return nullptr;
}

void*mythread2(void*args)
{
    pthread_mutex_lock(&lock2);
    printf("mythread2:拿到lock2,等待lock1\n");
    sleep(1); //故意延迟,让线程1有时间拿lock1
    pthread_mutex_lock(&lock1);
    printf("mythread1:拿到lock1");
  pthread_mutex_unlock(&lock1);
    pthread_mutex_unlock(&lock2);

    return nullptr;
}

int main()
{
    pthread_t tid1,tid2;
    pthread_create(&tid1,nullptr,mythread1,0);
    pthread_create(&tid2,nullptr,mythread2,0);
    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);
    return 0;
}

死锁的四个必要条件:

互斥条件:一个资源每次只能被一个执行流使用(前提)

请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放(原则1)

不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺(原则2)

循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系(重要条件)

如何避免死锁?

我们上面说,死锁有四个必要条件,那么,本质上我们只要打破四个中其中一种就可以避免死锁了!

破坏死锁的四个必要条件

加锁顺序一致

避免锁未释放的场景

资源一次性分配

理念上是破坏其中一种条件就可以,但是我们是比较难以打破的。

实际上,我们通常会选择其他的方法来减少死锁!

其中,同步就是一种。

什么是同步?

同步问题是保证数据安全的情况下,让我们的线程访问资源具有一定的顺序性!

解决死锁问题引出

条件变量:

https://blog.youkuaiyun.com/go_bai/article/details/155112522?spm=1001.2014.3001.5501

这篇文章中有讲解到关于条件变量的接口,可以移至那里去了解了解

当一个线程互斥(每一次只有一个执行流访问资源)地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步

竞态条件:因为时序问题(执行顺序,时机不确定),而导致程序异常,我们称之为竞态条件

本质上,竞态条件:是线程对共享资源的非原子访问,被线程切换打乱了执行顺序!

eg:我们上一篇写的抢票机制!一张票被两个人抢走了,票数变成负数。

pthread_cond_t cond;
pthread_mutex_t mtx;

void*mythread1(void*args)
{
    while(true)
    {
        pthread_cond_wait(&cond,&mtx);
        std::cout<<"I am running"<<std::endl;
    }
    return nullptr;
}

void*mythread2(void*args)
{
    while(true)
    {
        pthread_cond_signal(&cond);
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid1,tid2;

    pthread_cond_init(&cond,nullptr);
    pthread_mutex_init(&mtx,nullptr);

    pthread_create(&tid1,nullptr,mythread1,nullptr);
    pthread_create(&tid2,nullptr,mythread2,nullptr);

    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);

    pthread_mutex_destroy(&mtx);
    pthread_cond_destroy(&cond);
    return 0;
}

一个线程等待,一个线程唤醒!

我们讨论一下,为什么条件等待要上锁?

我们来思考一下:

条件等待是线程之间同步的手段,试想一下,如果只有你一个线程,在条件不满足,一直等下去都不会满足,所以,它必须要有另外一个线程通过某种操作,改变共享变量,使得原先不满足的条件变得满足,并且友好地通知等待在条件变量上的线程。

同时,条件并不会无缘无故的突然变得满足的,这其中必定会涉及到共享数据的变化,那么,就一定需要互斥锁(一个时刻中,只有一个执行流能访问资源)来保证数据的安全性!

条件变量的设计:

1.先上锁,发现条件不满足,解锁,然后等待在条件变量,这样可行吗?

pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
} p
thread_mutex_unlock(&mutex);

答案:不可行:因为,当你条件不满足的时候,进入循环,然后解锁,又可能在条件等待之前,其他线程拿到了互斥量,改变条件满足,发送了信号,那么,接下来的条件变量将永远不会满足,就造成一直阻塞在pthread_cond_wait()那里。

因此,我们的解锁与条件等待必须是原子的!

int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进入该函数后,会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样

等待条件:

pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);

给条件发送唤醒信号

pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

好了,关于本次的分享就到此结束了,我们下次再见,希望大家一起进步!

最后,到了本次鸡汤环节:

希望梦想不是遥不可及的,在我愿意为之奋斗的前提下!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值