【Linux】Linux线程的同步与互斥(1)

简介: 【Linux】Linux线程的同步与互斥

一、Linux线程的互斥

1、互斥的相关背景

我们先来看一段多线程抢票的代码,票数有10000张,共有4个线程

#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 票数
int tickets = 10000;
void* threadRoutine(void* args)
{
    char* s = static_cast<char*>(args);
    while (true)
    {
        if (tickets > 0)
        {
            usleep(2000); //抢票花费的时间
            cout << s << " get a ticket, surplus number is :" << --tickets << endl;
        }
        else
        {
            break;
        }
    }
    cout << "The tickets are sold out" << endl;
    return s;
}
int main()
{
    pthread_t tname[4];
    int n = sizeof(tname) / sizeof(tname[0]);
    // 创建线程抢票
    for (int i = 0; i < n; i++)
    {
        char* str = new char[64];
        snprintf(str, sizeof(str), "线程-%d", i);
        pthread_create(tname + i, nullptr, threadRoutine, str);
        usleep(2000);
    }
    // 回收线程以及内存
    void* ret = nullptr;
    for (int i = 0; i < n; i++)
    {
        int error = pthread_join(tname[i], &ret);
        if (error == 0)
        {
            delete[] (char*)ret;
        }
        else
        {
            cerr << strerror(error) << endl;
        }
    }
    return 0;
}

运行结果:

我们看到抢票时把票数抢到了负数,这是为什么呢,我们一起来分析一下:

要解决上述抢票系统的问题,需要做到三点:

  1. 代码必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  2. 如果多个线程同时要求执行临界区的代码,并且此时临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁,Linux上提供的这把锁叫互斥量。

2、互斥量的接口


①初始化互斥量

方法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即可 。

返回值说明:

  • 互斥量初始化成功返回0,失败返回错误码。
  • pthread_mutex_t是一种类型,可以用来定义一把互斥锁。
  • 静态分配的的互斥锁,不需要销毁,但是必须定义在全局。

②销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数说明:

  • mutex:需要销毁的互斥量的地址。

返回值说明:

  • 互斥量销毁成功返回0,失败返回错误码。

销毁互斥量需要注意:

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

③互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数说明:

  • mutex:需要加锁的互斥量的地址。

返回值说明:

  • 互斥量加锁成功返回0,失败返回错误码。

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

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

有了这些知识,我们就可以解决上面的问题了,我们在上述的抢票系统中引入互斥量,每一个线程要进入临界区之前都必须先申请锁,只有申请到锁的线程才可以进入临界区对临界资源进行访问,并且当线程出临界区的时候需要释放锁,这样才能让其余要进入临界区的线程可能申请到锁。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int tickets = 10000;
// 定义一把锁 
pthread_mutex_t mutex;
void* threadRoutine(void* args)
{
    char* s = static_cast<char*>(args);
    while (true)
    {
      // 加锁
        pthread_mutex_lock(&mutex);
        if (tickets > 0)
        {
            usleep(1000); //抢票花费的时间
            cout << s << " get a ticket, surplus number is :" << --tickets << endl;
            // 解锁
            pthread_mutex_unlock(&mutex);
        }
        else
        {
          // 解锁
            pthread_mutex_unlock(&mutex);
            break;
        }
        // 抢票以后的后续的处理
        usleep(1000);
    }
    cout << "The tickets are sold out" << endl;
    return s;
}
int main()
{
    // 对锁进行初始化
    pthread_mutex_init(&mutex, nullptr);
    pthread_t tname[4];
    int n = sizeof(tname) / sizeof(tname[0]);
    for (int i = 0; i < n; i++)
    {
        char* str = new char[64];
        snprintf(str, 64, "线程-%d", i);
        pthread_create(tname + i, nullptr, threadRoutine, str);
        usleep(1000);
    }
    // 回收线程以及内存
    void* ret = nullptr;
    for (int i = 0; i < n; i++)
    {
        int error = pthread_join(tname[i], &ret);
        if (error == 0)
        {
            delete[] (char*)ret;
        }
        else
        {
            cerr << strerror(error) << endl;
        }
    }
    // 销毁锁
    pthread_mutex_destroy(&mutex);
    return 0;
}

运行结果正常:

  • 此外加锁本身都是有损于性能的事,它让多执行流由并行执行变为了串行执行,这是不可避免的。
  • 我们应该在合适的位置进行加锁和解锁,这样能尽可能减少加锁带来的性能开销成本。
  • 进行临界资源的保护,是所有执行流都应该遵守的标准,这是在编码时需要注意的。

3、互斥量实现原理探究

  • 单纯的i++或者++i都不是原子的,有可能会有数据一致性问题。
    例如:取出ticket- -部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>

- - ,++操作并不是原子操作,而是对应三条汇编指令:

  1. load:将共享变量ticket从内存加载到寄存器中
  2. update: 更新寄存器里面的值,执行-1/+1操作
  3. store:将新值,从寄存器写回共享变量ticket的内存地址
  • 为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

现在我们把lockunlock的伪代码改一下。

我们可以认为mutex的初始值为1,al是计算机中的一个寄存器,当线程申请锁时,需要执行以下步骤:

  1. 先将al寄存器中的值清0。该动作可以被多个线程同时执行,因为每个线程都有自己的一组寄存器(用来存储线程上下文信息),执行该动作本质上是将线程自己的al寄存器清0。
  2. 然后交换al寄存器和mutex中的值。xchgb(exchange)是体系结构提供的交换指令,该指令可以完成寄存器和内存单元之间数据的交换。
  3. 最后判断al寄存器中的值是否大于0。若大于0则申请锁成功,此时就可以进入临界区访问对应的临界资源;否则申请锁失败需要被挂起等待,直到锁被释放后再次竞争申请锁。

当一个线程申请锁成功以后,其他线程再进行申请时,由于mutex里面是0,al里面再进行交换拿到的依然是0,继续向后执行时会被挂起。

当线程释放锁时,需要执行以下步骤:

  1. 将内存中的mutex置回1。使得下一个申请锁的线程在执行交换指令后能够得到1,形象地说就是“将锁放回去”。
  2. 唤醒等待mutex的线程。唤醒因为申请锁失败而被挂起的线程,让它们继续竞争申请锁。

注意点

  1. 在线程释放锁时没有将当前线程al寄存器中的值清0,这不会造成影响,因为每次线程在申请锁时都会先将自己al寄存器中的值清0,再执行交换指令。
  2. 在申请锁时本质上就是哪一个线程先执行了交换指令,那么该线程就申请锁成功,因为此时该线程的al寄存器中的值就是1了。而交换指令就只是一条汇编指令,一个线程要么执行了交换指令,要么没有执行交换指令,所以申请锁的过程是原子的。
  3. CPU内的寄存器不是被所有的线程共享的,每个线程都有自己的一组寄存器,但内存中的数据是各个线程共享的。申请锁实际就是,把内存中的mutex通过交换指令,原子性的交换到自己的al寄存器中。

问题1:临界区内的线程可能进行线程切换吗?

临界区内的线程完全有可能进行线程切换,但即便该线程被切走,其他线程也无法进入临界区进行资源访问,因为此时该线程是拿着锁被切走的,锁没有被释放也就意味着其他线程无法申请到锁,也就无法进入临界区进行资源访问了。

其他想进入该临界区进行资源访问的线程,必须等该线程执行完临界区的代码并释放锁之后,才能申请锁,申请到锁之后才能进入临界区。

问题2:锁是否需要被保护?

我们说被多个执行流共享的资源叫做临界资源,访问临界资源的代码叫做临界区。所有的线程在进入临界区之前都必须竞争式的申请锁,因此锁也是被多个执行流共享的资源,也就是说锁本身就是临界资源。

既然锁是临界资源,那么锁就必须被保护起来,但锁本身就是用来保护临界资源的,那锁又由谁来保护的呢?

锁实际上是自己保护自己的,因为申请锁的过程是原子的,那么锁就是安全的。

相关文章
|
2月前
|
编解码 数据安全/隐私保护 计算机视觉
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
如何使用OpenCV进行同步和异步操作来打开海康摄像头,并提供了相关的代码示例。
95 1
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
|
1月前
|
Java 调度
Java 线程同步的四种方式,最全详解,建议收藏!
本文详细解析了Java线程同步的四种方式:synchronized关键字、ReentrantLock、原子变量和ThreadLocal,通过实例代码和对比分析,帮助你深入理解线程同步机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
Java 线程同步的四种方式,最全详解,建议收藏!
|
2月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
38 1
|
2月前
|
资源调度 Linux 调度
Linux C/C++之线程基础
这篇文章详细介绍了Linux下C/C++线程的基本概念、创建和管理线程的方法,以及线程同步的各种机制,并通过实例代码展示了线程同步技术的应用。
31 0
Linux C/C++之线程基础
|
3月前
|
Linux Docker 容器
9. 同步执行Linux多条命令
9. 同步执行Linux多条命令
|
2月前
|
安全 调度 C#
STA模型、同步上下文和多线程、异步调度
【10月更文挑战第19天】本文介绍了 STA 模型、同步上下文和多线程、异步调度的概念及其优缺点。STA 模型适用于单线程环境,确保资源访问的顺序性;同步上下文和多线程提高了程序的并发性和响应性,但增加了复杂性;异步调度提升了程序的响应性和资源利用率,但也带来了编程复杂性和错误处理的挑战。选择合适的模型需根据具体应用场景和需求进行权衡。
|
2月前
多线程通信和同步的方式有哪些?
【10月更文挑战第6天】
104 0
|
2月前
|
安全 Linux
Linux线程(十一)线程互斥锁-条件变量详解
Linux线程(十一)线程互斥锁-条件变量详解
|
4月前
|
开发者 C# UED
WPF与多媒体:解锁音频视频播放新姿势——从界面设计到代码实践,全方位教你如何在WPF应用中集成流畅的多媒体功能
【8月更文挑战第31天】本文以随笔形式介绍了如何在WPF应用中集成音频和视频播放功能。通过使用MediaElement控件,开发者能轻松创建多媒体应用程序。文章详细展示了从创建WPF项目到设计UI及实现媒体控制逻辑的过程,并提供了完整的示例代码。此外,还介绍了如何添加进度条等额外功能以增强用户体验。希望本文能为WPF开发者提供实用的技术指导与灵感。
161 0
|
4月前
|
开发者 C# 存储
WPF开发者必读:资源字典应用秘籍,轻松实现样式与模板共享,让你的WPF应用更上一层楼!
【8月更文挑战第31天】在WPF开发中,资源字典是一种强大的工具,用于共享样式、模板、图像等资源,提高了应用的可维护性和可扩展性。本文介绍了资源字典的基础知识、创建方法及最佳实践,并通过示例展示了如何在项目中有效利用资源字典,实现资源的重用和动态绑定。
97 0