锁|多线程|解决并行计算所引发的数据不一致性问题|线程互斥|临界资源保护【超详细的解释和代码注释】


前言

那么这里博主先安利一下一些干货满满的专栏啦!

手撕数据结构https://blog.csdn.net/yu_cblog/category_11490888.html?spm=1001.2014.3001.5482https://blog.csdn.net/yu_cblog/category_11490888.html?spm=1001.2014.3001.5482这里包含了博主很多的数据结构学习上的总结,每一篇都是超级用心编写的,有兴趣的伙伴们都支持一下吧!
算法专栏https://blog.csdn.net/yu_cblog/category_11464817.htmlhttps://blog.csdn.net/yu_cblog/category_11464817.html这里是STL源码剖析专栏,这个专栏将会持续更新STL各种容器的模拟实现。

STL源码剖析https://blog.csdn.net/yu_cblog/category_11983210.html?spm=1001.2014.3001.5482https://blog.csdn.net/yu_cblog/category_11983210.html?spm=1001.2014.3001.5482


为什么需要互斥锁

为了解释清楚为什么需要锁,博主通过一段代码向大家展示。

这是一段多线程抢票逻辑的代码:

一共有tickets张票,我们创建三个线程进行抢票

按照道理:当票抢完之后,票数应该为0

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <vector>

// 如果多线程访问同一个全局变量,并对它进行数据计算,多线程会互相影响吗?
int tickets = 10000; // 这里的10000就是临界资源

void *getTickets(void *args)
{
    (void)args;
    while (true)
    {
        if (tickets > 0)
        {
            usleep(1000);
            printf("%p: %d\n", pthread_self(), tickets);
            tickets--;
        }
        else
        {
            // 没有票了
            break;
        }
    }
}
int main()
{
    pthread_t t1, t2, t3;
    // 多线程抢票的逻辑
    pthread_create(&t1, nullptr, getTickets, nullptr);//创建线程
    pthread_create(&t2, nullptr, getTickets, nullptr);
    pthread_create(&t3, nullptr, getTickets, nullptr);
    pthread_join(t1, nullptr);//线程等待
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);

    return 0;
}

现象

 最后抢到了-1,这是不合理的!

其实在代码中的一句话,其实对应汇编语言中是不止一句语句的。而线程有可能在任何时刻被调度器切换,此时,对于不加保护内存中的数据,可能就会出现数据不一致的问题。

在这里,博主给大家举一个例子,大家就明白了。

假设现在tickets是10000,现在线程A准备执行减减操作。

减减操作对应有三步:

  • 将tickets加载到cpu
  • cpu进行计算
  • 将tickets重新写回内存

假设线程A已经完成前两步了,此时准备进行第三步的时候,线程A被切走了,此时线程A就会把9999这个数存在寄存器中,等下一次A被切换回来,就继续执行第三部:把数据放回内存的操作。但是,因为tickets这部分内存因为没有被保护,在线程A被切回来之前,可能调度器已经把tickets减减到5000了,此时如果线程A重新被切回来,那么tickets又重新变回9999了。这个就是由于并行计算而导致的数据不一致问题!

所以我们要对tickets这部分内存进行保护!由于线程是否被切走,这是完全由调度器决定的,因此我们是没有办法控制的,我们唯一能做的就是,当线程被切走的时候,别的线程,也不能访问tickets,必须等A处理完,别的线程才能处理!

而在操作系统中,tickets这种能被多个执行流同时访问并修改的数据,叫做临界资源。

互斥锁

为什么加锁不加在while前面 解锁放在while结束之后?

如果是这样,整个抢票的逻辑就完全串行了 这和没有多线程有什么区别? 所以我们加锁的时候,一定要保证加锁的粒度,越小越好!

 

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <vector>
#include <time.h>
// 要对它进行加锁保护
// pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; // pthread_mutex_t 原生线程库提供的一个数据类型
int tickets = 10000;

#define THREAD_NUM 5 // 定义线程的个数
struct ThreadData
{
public:
    ThreadData(const std::string n, pthread_mutex_t *pm)
        : tname(n), pmtx(pm) {}

public:
    std::string tname;
    pthread_mutex_t *pmtx;
};

void *getTickets(void *args)
{
    ThreadData *td = (ThreadData *)args;
    while (true)
    {
        pthread_mutex_lock(td->pmtx);
        if (tickets > 0) // 判断的本质也是计算的一种
        {
            usleep(rand() % 1500 + 200); // 让休眠时间随机一点
            printf("%p: %d\n", pthread_self(), tickets);
            tickets--;
            pthread_mutex_unlock(td->pmtx);
        }
        else
        {
            // 没有票了
            pthread_mutex_unlock(td->pmtx);
            break;
        }
    }
    delete td;
}

int main()
{
    pthread_mutex_t mtx;
    pthread_mutex_init(&mtx, nullptr);
    srand((unsigned long)time(nullptr) ^ getpid() ^ 0x147);

    pthread_t t[THREAD_NUM];
    // 多线程抢票的逻辑
    for (int i = 0; i < 5; i++)
    {
        std::string name = "thread ";
        name += std::to_string(i + 1);
        ThreadData *td = new ThreadData(name, &mtx);
        pthread_create(t + i, nullptr, getTickets, (void *)td);
    }
    for (int i = 0; i < THREAD_NUM; i++)
    {
        pthread_join(t[i], nullptr);
    }
    pthread_mutex_destroy(&mtx);
    return 0;
}

 

加了锁之后,线程在临界区中,是否会切换?

        会切换,会有问题吗?不会!
虽然被切换,但是我们是持有锁被切换的!
其他执行流想要执行这部分代码,要申请锁,因此其他执行流申请锁会失败
加锁就是串行执行了吗?
是的!执行临界区代码一定是串行的
要访问临界资源,每一个线程都必须申请锁,前提是,每一个线程都必须先看到同一把锁 && 去访问它 那么,锁本身是不是一种共享临界资源? 谁来保证锁的安全呢? 所以为了保证锁的安全,申请和释放锁必须是原子的!!!
如何保证?锁究竟是什么?锁是如何实现的?

锁的原理

尾声

看到这里,大家对锁应该已经有了初步的了解了。但是,本期博客只是让大家看看,锁到底是一个什么东西而已。在现实应用中,锁不是这么用的,关于Linux的互斥锁,还有很多很复杂很深入的知识。而Linux多线程,最重要的我认为就是“生产者消费者模型”,这个实在是太重要了,在接下来的几篇博客中,博主都会围绕这一内容展开讲述!如果你觉的这些内容对你有帮助,不要忘记点赞收藏和转发哦!

posted @ 2023-03-24 15:22  背包Yu  阅读(13)  评论(0编辑  收藏  举报  来源