锁|多线程|解决并行计算所引发的数据不一致性问题|线程互斥|临界资源保护【超详细的解释和代码注释】
前言
那么这里博主先安利一下一些干货满满的专栏啦!
这里包含了博主很多的数据结构学习上的总结,每一篇都是超级用心编写的,有兴趣的伙伴们都支持一下吧!手撕数据结构https://blog.csdn.net/yu_cblog/category_11490888.html?spm=1001.2014.3001.5482
https://blog.csdn.net/yu_cblog/category_11490888.html?spm=1001.2014.3001.5482
这里是STL源码剖析专栏,这个专栏将会持续更新STL各种容器的模拟实现。算法专栏https://blog.csdn.net/yu_cblog/category_11464817.html
https://blog.csdn.net/yu_cblog/category_11464817.html
为什么需要互斥锁
为了解释清楚为什么需要锁,博主通过一段代码向大家展示。
这是一段多线程抢票逻辑的代码:
一共有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多线程,最重要的我认为就是“生产者消费者模型”,这个实在是太重要了,在接下来的几篇博客中,博主都会围绕这一内容展开讲述!如果你觉的这些内容对你有帮助,不要忘记点赞收藏和转发哦!