_Linux多线程-线程互斥篇
创始人
2024-05-11 02:39:27
0

文章目录

  • 1. 进程线程间的互斥相关背景概念
  • 2. 互斥量mutex
  • 3. 互斥量的接口
    • 初始化互斥量
    • 销毁互斥量
    • 互斥量加锁和解锁
  • 4. 互斥量---锁
    • 静态分配(初始化)
    • 动态分配(初始化)
  • 5. 互斥量实现原理探究
  • 6. 总结:

1. 进程线程间的互斥相关背景概念

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

2. 互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,会带来一些问题。

例子:操作共享变量会有问题的售票系统代码

  • 代码块
#include 
#include 
#include 
#include using namespace std;//买票逻辑
int tickets = 1000; // 在并发访问的时候,导致了我们数据不一致的问题!void *getTickets(void *args)
{(void*)args;// pthread_self() 获取该线程的idwhile(true){if(tickets>0){usleep(1000);printf("%p: %d\n", pthread_self(), tickets);tickets--;}else{break;}}
}int main()
{// 创建一个线程pthread_t tid1, tid2, tid3;pthread_create(&tid1, nullptr, getTickets, nullptr);pthread_create(&tid2, nullptr, getTickets, nullptr);pthread_create(&tid3, nullptr, getTickets, nullptr);pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);return 0;
}

我们理想下:最后票数为0。

  • 运行结果
    在这里插入图片描述
    结果竟然是-1。

  • 为什么可能无法获得争取结果?

    • if 语句判断条件为真以后,代码可以并发的切换到其他线程。
    • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
    • - -ticket 操作本身就不是一个原子操作。
        1. 读取数据到cpu内的寄存器中
      • 2.CPU内部进行计算**- -**
      • 3.将结果写回内存中
        • 在这其中很容易进行到一半就挂起;让其它线程来再执行。

在这里插入图片描述
—(图片摘自相关教材)

  • – 操作并不是原子操作,而是对应三条汇编指令:
    • load :将共享变量ticket从内存加载到寄存器中。
    • update : 更新寄存器里面的值,执行-1操作。
    • store :将新值,从寄存器写回共享变量ticket的内存地址。
  • 要解决以上问题,需要做到三点:
    • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
    • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
    • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
  • 要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
    在这里插入图片描述

3. 互斥量的接口

初始化互斥量

初始化互斥量有两种方法:

  • 方法1,静态分配:
 pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER
  • 方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

参数: mutex:要初始化的互斥量 attr:NULL

销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex);

销毁互斥量需要注意:

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁。
  • 不要销毁一个已经加锁的互斥量。
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

调用 pthread_ lock 时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

4. 互斥量—锁

改进上面的售票系统:

静态分配(初始化)

  • 代码块:
#include 
#include 
#include 
#include 
#include using namespace std;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态分配// 买票逻辑
int tickets = 1000; // 在并发访问的时候,导致了我们数据不一致的问题!void *getTickets(void *args)
{(void *)args;// pthread_self() 获取该线程的idwhile (true){int n = pthread_mutex_lock(&mutex); // 上锁assert(n == 0);if (tickets > 0){usleep(1000);printf("%p: %d\n", pthread_self(), tickets);tickets--;pthread_mutex_unlock(&mutex); // 解锁assert(n == 0);}else{pthread_mutex_unlock(&mutex); // 解锁assert(n == 0);break;}// 抢完票,其实还需要后续的动作usleep(rand() % 200);}
}int main()
{srand((unsigned long)time(nullptr) ^ getpid() ^ 2023);// 创建一个线程pthread_t tid1, tid2, tid3;pthread_create(&tid1, nullptr, getTickets, nullptr);pthread_create(&tid2, nullptr, getTickets, nullptr);pthread_create(&tid3, nullptr, getTickets, nullptr);pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);pthread_mutex_destroy(&mutex); // 销毁锁return 0;
}
  • 结果达到理想状态:

在这里插入图片描述

动态分配(初始化)

  • 代码块:
#include 
#include 
#include 
#include 
#include using namespace std;// pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态分配// 买票逻辑
int tickets = 1000; // 在并发访问的时候,导致了我们数据不一致的问题!#define THREAD_NUM 5class ThreadData
{
public:ThreadData(const string &n, pthread_mutex_t *pm) : tname(n), pmtx(pm){}string tname;pthread_mutex_t *pmtx;
};
void *getTickets(void *args)
{// pthread_self() 获取该线程的idThreadData *mutex = (ThreadData *)args;while (true){int n = pthread_mutex_lock(mutex->pmtx); // 上锁assert(n == 0);if (tickets > 0){usleep(1000);printf("%s: %d\n", mutex->tname.c_str(), tickets);tickets--;pthread_mutex_unlock(mutex->pmtx); // 解锁assert(n == 0);}else{pthread_mutex_unlock(mutex->pmtx); // 解锁assert(n == 0);break;}// 抢完票,其实还需要后续的动作usleep(rand() % 200);}delete mutex;return nullptr;
}int main()
{srand((unsigned long)time(nullptr) ^ getpid() ^ 2023);pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr);pthread_t t[THREAD_NUM];// 创建线程for (int i = 0; i < THREAD_NUM; ++i){string name = "thread ";name += to_string(i + 1);ThreadData *td = new ThreadData(name, &mutex);pthread_create(t + i, nullptr, getTickets, td);}// 等待线程for (int i = 0; i < THREAD_NUM; ++i){pthread_join(t[i], nullptr);}pthread_mutex_destroy(&mutex); // 销毁锁return 0;
}
  • 结果展示:
    在这里插入图片描述

5. 互斥量实现原理探究

  • 经过上面的例子我们已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题。
  • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下。
    在这里插入图片描述
    交换的现象: 内存<->%al 做交换
    交换的本质: 共享<->私有(线程自己的上下文)
    即保证了锁里面‘1’ 的唯一性。
    在这里插入图片描述

6. 总结:

  • 如果多线程访问同一个全局变量,并对它进行数据计算,多线程会互相影响;避免方法:

    • 加锁保护:加锁的时候,一定要保证加锁的粒度,越小越好!!
  • 加锁就是串行执行。

    • 执行临界区代码一定是串行的!
  • 加锁了之后,线程在临界区中,会切换,没有时序问题。

    • 原子性的体现
    • 虽然被切换了,但是你是持有锁被切换的, 所以其他抢票线程要执行临界区代码,也必须先申请锁,锁它是无法申请成功的,所以,也不会让其他线程进入临界区,就保证了临界区中数据一致性!
  • 我是一个线程,我不申请锁,就是单纯的访问临界资源! 这是一种错误的编码方式。

  • 在没有持有锁的线程看来,对它最有意义的情况只有两种:

      1. 线程1没有持有锁(什么都没做)
      1. 线程1释放锁(做完),此时我可以申请锁!
  • 要访问临界资源,每一个线程都必须现申请锁,每一个线程都必须先看到同一把锁并且可以访问它,即锁本身就是一种共享资源。那么谁来保证锁的安全呢??为了保证锁的安全,申请和释放锁,必须是原子滴

  • 锁自身保证的(上面的伪代码)—设计者这样设计滴

  • 在执行流视角,是如何看待CPU上面的寄存器的?

    • CPU内部的寄存器本质,叫做当前执行流的上下文! !寄存器们,空间是被所有的执行流共享的,但是寄存器的内容,是被每一个执行流私有的! 上下文!

相关内容

热门资讯

常用商务英语口语   商务英语是以适应职场生活的语言要求为目的,内容涉及到商务活动的方方面面。下面是小编收集的常用商务...
六年级上册英语第一单元练习题   一、根据要求写单词。  1.dry(反义词)__________________  2.writ...
复活节英文怎么说 复活节英文怎么说?复活节的英语翻译是什么?复活节:Easter;"Easter,anniversar...
2008年北京奥运会主题曲 2008年北京奥运会(第29届夏季奥林匹克运动会),2008年8月8日到2008年8月24日在中华人...
英语道歉信 英语道歉信15篇  在日常生活中,道歉信的使用频率越来越高,通过道歉信,我们可以更好地解释事情发生的...
六年级英语专题训练(连词成句... 六年级英语专题训练(连词成句30题)  1. have,playhouse,many,I,toy,i...
上班迟到情况说明英语   每个人都或多或少的迟到过那么几次,因为各种原因,可能生病,可能因为交通堵车,可能是因为天气冷,有...
小学英语教学论文 小学英语教学论文范文  引导语:英语教育一直都是每个家长所器重的,那么有关小学英语教学论文要怎么写呢...
英语口语学习必看的方法技巧 英语口语学习必看的方法技巧如何才能说流利的英语? 说外语时,我们主要应做到四件事:理解、回答、提问、...
四级英语作文选:Birth ... 四级英语作文范文选:Birth controlSince the Chinese Governmen...
金融专业英语面试自我介绍 金融专业英语面试自我介绍3篇  金融专业的学生面试时,面试官要求用英语做自我介绍该怎么说。下面是小编...
我的李老师走了四年级英语日记... 我的李老师走了四年级英语日记带翻译  我上了五个学期的小学却换了六任老师,李老师是带我们班最长的语文...
小学三年级英语日记带翻译捡玉... 小学三年级英语日记带翻译捡玉米  今天,我和妈妈去外婆家,外婆家有刚剥的`玉米棒上带有玉米籽,好大的...
七年级英语优秀教学设计 七年级英语优秀教学设计  作为一位兢兢业业的人民教师,常常要写一份优秀的教学设计,教学设计是把教学原...
我的英语老师作文 我的英语老师作文(通用21篇)  在日常生活或是工作学习中,大家都有写作文的经历,对作文很是熟悉吧,...
英语老师教学经验总结 英语老师教学经验总结(通用19篇)  总结是指社会团体、企业单位和个人对某一阶段的学习、工作或其完成...
初一英语暑假作业答案 初一英语暑假作业答案  英语练习一(基础训练)第一题1.D2.H3.E4.F5.I6.A7.J8.C...
大学生的英语演讲稿 大学生的英语演讲稿范文(精选10篇)  使用正确的写作思路书写演讲稿会更加事半功倍。在现实社会中,越...
VOA美国之音英语学习网址 VOA美国之音英语学习推荐网址 美国之音网站已经成为语言学习最重要的资源站点,在互联网上还有若干网站...
商务英语期末试卷 Part I Term Translation (20%)Section A: Translate ...