您的位置:首页 > 游戏 > 手游 > 实力网站优化公司首选_网页升级紧急通知网页_惠州seo代理商_seo是什么意思的缩写

实力网站优化公司首选_网页升级紧急通知网页_惠州seo代理商_seo是什么意思的缩写

2024/12/21 21:44:34 来源:https://blog.csdn.net/wxk2227814847/article/details/144096369  浏览:    关键词:实力网站优化公司首选_网页升级紧急通知网页_惠州seo代理商_seo是什么意思的缩写
实力网站优化公司首选_网页升级紧急通知网页_惠州seo代理商_seo是什么意思的缩写

目录

0.前言

1.相关概念

2.互斥量(mutex)

2.1 代码引入

2.2为什么需要互斥量

2.3互斥量的接口

2.3.1 初始化互斥量

2.3.2 销毁互斥量

2.3.3 互斥量加锁和解锁

2.4改写代码

 3.互斥量的封装

4.小结


(图像由AI生成) 

0.前言

在多线程编程中,线程之间的并发操作可能会导致共享资源的竞争问题,如数据不一致、状态紊乱等。为了保证程序的正确性和稳定性,必须引入线程同步机制,其中互斥量(mutex)是解决线程互斥的核心工具。本篇博客承接前文关于进程的讨论,深入介绍线程互斥的相关概念、实现方法以及代码实例,帮助理解如何在 Linux 环境下有效避免线程竞争问题。

1.相关概念

  • 临界资源:指多个线程需要共享访问的资源,例如全局变量、文件或数据库连接等。如果多个线程同时操作临界资源,可能会导致数据不一致或冲突。
  • 临界区:指访问临界资源的代码片段。为防止多个线程同时进入临界区,需要对其进行保护,确保同一时刻只有一个线程可以执行临界区代码。
  • 互斥:一种线程同步机制,用于确保多个线程对临界资源的访问是互斥的,即同一时间仅允许一个线程访问共享资源。互斥量(mutex)是实现互斥的常用工具。
  • 原子性:指某个操作不可被中断,要么完全执行完毕,要么完全不执行。在多线程环境下,原子性是实现线程安全的基本要求之一。

2.互斥量(mutex)

互斥量是一种线程同步机制,用于解决多线程并发访问共享资源时的冲突问题。在多线程编程中,互斥量通过对临界区的加锁和解锁,确保同一时刻只有一个线程可以访问共享资源,从而避免数据竞争。

2.1 代码引入

在大多数情况下,线程使用的数据是局部变量,变量的地址空间位于线程栈空间内,仅属于单个线程,其他线程无法访问。但在某些场景下,线程之间需要共享数据,这些变量称为共享变量,通过它们可以完成线程间的交互。

然而,当多个线程并发操作共享变量时,会导致数据不一致等问题。例如,一个典型的问题是多个线程争夺共享资源时的竞争。以下以“抢票”为例,展示未加锁的多线程争夺资源代码:

未加锁的多线程代码示例:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h> // usleep 函数int ticket = 100; // 共享资源void* sell_tickets(void* arg) {char* id = (char*)arg; // 将 void* 转为 char*while (1) {if (ticket > 0) { // 检查是否还有票usleep(1000); // 模拟售票的延迟printf("%s sells ticket: %d\n", id, ticket);ticket--; // 执行 -- 操作,存在数据竞争} else {break; // 没有票时退出}}return NULL;
}int main(void) {pthread_t t1, t2, t3, t4;// 创建四个线程,并显式转换字符串为 void*pthread_create(&t1, NULL, sell_tickets, (void*)"thread 1");pthread_create(&t2, NULL, sell_tickets, (void*)"thread 2");pthread_create(&t3, NULL, sell_tickets, (void*)"thread 3");pthread_create(&t4, NULL, sell_tickets, (void*)"thread 4");// 等待线程结束pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);return 0;
}

程序输出(部分):

thread 2 sells ticket: 100
thread 1 sells ticket: 100
thread 3 sells ticket: 100
thread 4 sells ticket: 100
...
thread 2 sells ticket: 3
thread 3 sells ticket: 3
thread 4 sells ticket: 1
thread 2 sells ticket: 0
thread 1 sells ticket: -1
thread 3 sells ticket: -2

2.2为什么需要互斥量

在多线程编程中,当多个线程并发访问共享资源时,如果没有同步机制进行保护,就会导致数据竞争和资源冲突等问题。以下是未加锁情况下上面的代码出现的问题:

  1. 票号重复销售:
    多个线程同时读取共享变量 ticket 的值,导致同一票号被多个线程同时销售。例如:

    thread 2 sells ticket: 100
    thread 1 sells ticket: 100
    thread 3 sells ticket: 100
    

    这是因为 ticket 的读取和更新是分步骤完成的,线程在切换时导致了数据的不一致。

  2. 超卖现象:
    由于多个线程同时修改 ticket 的值,可能导致最终结果错误,甚至出现负值。例如:

    thread 1 sells ticket: -1
    thread 3 sells ticket: -2
    

    这种现象表明线程在操作过程中缺乏有效的同步机制,无法确保共享变量的正确性。

  3. 数据竞争:
    ticket-- 是非原子操作,分为读取值、修改值和写回值三个步骤。在多线程环境下,这些步骤可能被其他线程的操作打断,导致多个线程同时更新变量的值,破坏数据一致性。

多线程编程中的共享资源竞争是导致数据不一致的主要原因。以抢票系统中的 --ticket 操作为例,尽管它看似简单,但实际上并非原子操作,而是由多条汇编指令组成的复杂过程。

--ticket 的汇编代码
通过 objdump 工具反汇编程序,我们可以看到 --ticket 的具体汇编指令:

152 40064b: 8b 05 e3 04 20 00    mov 0x2004e3(%rip),%eax   # 将共享变量加载到寄存器
153 400651: 83 e8 01             sub $0x1,%eax             # 更新寄存器中的值,执行 -1 操作
154 400654: 89 05 da 04 20 00    mov %eax,0x2004da(%rip)   # 将新值写回共享变量的内存地址

这三条指令的含义分别是:

  1. load: 将共享变量 ticket 的值从内存加载到寄存器。
  2. update: 在寄存器中执行 -1 操作,更新值。
  3. store: 将更新后的值写回共享变量的内存地址。

问题所在:
由于 --ticket 涉及三步操作,如果线程在任意步骤被中断,另一个线程可能会修改 ticket,导致数据竞争。例如:

  • 线程 A 从内存读取 ticket = 100,还未更新,线程 B 也读取了 ticket = 100
  • 两个线程都执行了 ticket-- 操作,结果是 ticket = 99,实际减少了一张票而非两张。

这种数据不一致问题会引发票号重复销售超卖现象,根本原因是 --ticket 不是原子操作。

如何解决这些问题?
为了解决共享资源的竞争问题,需要满足以下三点:

  1. 互斥行为: 当一个线程进入临界区执行代码时,其他线程必须被阻止进入临界区。
  2. 独占访问: 如果多个线程同时请求进入临界区,且临界区没有线程在执行,则仅允许一个线程进入。
  3. 非阻塞: 如果某线程不在临界区内执行,则不能阻止其他线程进入临界区。

这些条件的核心要求是一把锁,而 Linux 系统中提供的这把锁就是互斥量(mutex)

互斥量的作用:
互斥量通过加锁(pthread_mutex_lock)和解锁(pthread_mutex_unlock),实现对临界区的独占访问:

  • 加锁: 线程在访问共享资源前需要获得锁,如果其他线程已经持有锁,则当前线程会阻塞。
  • 解锁: 线程在完成共享资源操作后释放锁,其他阻塞线程才可以继续执行。

通过互斥量,--ticket 的多条汇编指令可以被视为一个原子操作,从而避免数据竞争,确保程序的正确性和线程安全。

2.3互斥量的接口

在多线程编程中,互斥量(mutex)提供了一种机制来确保共享资源的安全访问。以下介绍互斥量的核心操作接口。

2.3.1 初始化互斥量

互斥量在使用前需要进行初始化,主要有两种方法:

方法1:静态分配

通过宏 PTHREAD_MUTEX_INITIALIZER 初始化互斥量,适用于全局或静态互斥量:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

这种方式简单直接,适合在程序启动时确定的互斥量。

方法2:动态分配

通过函数 pthread_mutex_init 动态初始化互斥量,适用于动态创建的互斥量:

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); // 动态初始化
  • 参数说明:
    • mutex:指向需要初始化的互斥量。
    • attr:互斥量属性,一般传 NULL 表示使用默认属性。

示例代码:

pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL); // 动态初始化

2.3.2 销毁互斥量

互斥量使用完成后,需通过 pthread_mutex_destroy 释放资源:

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 注意事项:
    1. 使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量不需要显式销毁。
    2. **不要销毁一个已经加锁的互斥量,**否则可能导致程序崩溃或行为异常。
    3. **确保销毁后的互斥量不再被使用,**避免线程尝试加锁销毁的互斥量。

示例代码:

pthread_mutex_destroy(&mutex);

2.3.3 互斥量加锁和解锁

加锁

使用 pthread_mutex_lock 对互斥量加锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 行为:
    1. 如果互斥量处于未锁状态,调用线程会成功加锁并继续执行。
    2. 如果互斥量已被其他线程锁定,调用线程会阻塞,等待互斥量解锁。

解锁

使用 pthread_mutex_unlock 对互斥量解锁:

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 解锁后,其他等待的线程将有机会获得锁。

返回值:

  • 成功返回 0
  • 失败返回错误号(例如尝试解锁未加锁的互斥量)。

示例代码:

pthread_mutex_lock(&mutex);
// 临界区代码
pthread_mutex_unlock(&mutex);

2.4改写代码

在 2.1 的示例代码中,由于 ticket-- 操作不是原子操作,导致出现数据竞争和不一致的问题。通过引入互斥量(mutex),可以确保对共享资源 ticket 的访问具有互斥性,从而解决上述问题。

以下是改写后的代码,使用互斥量实现线程安全:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h> // usleep 函数int ticket = 100; // 共享资源
pthread_mutex_t mutex; // 定义互斥量void* sell_tickets(void* arg) {char* id = (char*)arg; // 将 void* 转为 char*while (1) {pthread_mutex_lock(&mutex); // 加锁,保护共享资源if (ticket > 0) { // 检查是否还有票usleep(1000); // 模拟售票的延迟printf("%s sells ticket: %d\n", id, ticket);ticket--; // 执行 -- 操作,已被互斥量保护} else {pthread_mutex_unlock(&mutex); // 解锁,退出循环前释放锁break;}pthread_mutex_unlock(&mutex); // 解锁,允许其他线程访问共享资源}return NULL;
}int main(void) {pthread_mutex_init(&mutex, NULL); // 初始化互斥量pthread_t t1, t2, t3, t4;// 创建四个线程,并显式转换字符串为 void*pthread_create(&t1, NULL, sell_tickets, (void*)"thread 1");pthread_create(&t2, NULL, sell_tickets, (void*)"thread 2");pthread_create(&t3, NULL, sell_tickets, (void*)"thread 3");pthread_create(&t4, NULL, sell_tickets, (void*)"thread 4");// 等待线程结束pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);pthread_mutex_destroy(&mutex); // 销毁互斥量return 0;
}

 3.互斥量的封装

在实际开发中,直接操作互斥量可能会导致代码冗长且容易出错。通过对互斥量的封装,可以简化使用流程并提高代码的可维护性。以下通过 Lock.hpp 文件展示如何封装互斥量,并采用 RAII 风格实现自动化管理。

#pragma once
#include <pthread.h>namespace LockModule {// 对互斥量进行封装
class Mutex {
public:// 禁止拷贝构造和赋值Mutex(const Mutex &) = delete;const Mutex &operator=(const Mutex &) = delete;// 构造函数,初始化互斥量Mutex() {int n = pthread_mutex_init(&_mutex, nullptr);(void)n; // 忽略返回值,实际开发中可以添加错误检查}// 加锁void Lock() {int n = pthread_mutex_lock(&_mutex);(void)n;}// 解锁void Unlock() {int n = pthread_mutex_unlock(&_mutex);(void)n;}// 获取互斥量的原始指针pthread_mutex_t *GetMutexOriginal() {return &_mutex;}// 析构函数,销毁互斥量~Mutex() {int n = pthread_mutex_destroy(&_mutex);(void)n;}private:pthread_mutex_t _mutex; // 封装的互斥量
};// RAII 风格的锁管理器
class LockGuard {
public:// 构造函数,自动加锁LockGuard(Mutex &mutex) : _mutex(mutex) {_mutex.Lock();}// 析构函数,自动解锁~LockGuard() {_mutex.Unlock();}private:Mutex &_mutex; // 引用封装的互斥量
};}

封装的核心思想

  1. Mutex 类:

    • 封装了 pthread_mutex_t 的操作,包括初始化、加锁、解锁和销毁。
    • 禁止拷贝构造和赋值,避免多次操作同一个互斥量。
    • 提供获取原始互斥量指针的方法,以便在某些特殊场景中直接操作底层互斥量。
  2. LockGuard 类:

    • 采用 RAII(Resource Acquisition Is Initialization)风格,通过构造函数加锁,析构函数解锁,实现自动化管理。
    • 避免手动解锁可能导致的遗漏问题。

4.小结

线程间的共享资源竞争是多线程编程中的核心问题,互斥量(mutex)提供了一种高效的解决方案。通过本篇博客,我们从互斥量的基础概念入手,详细介绍了其初始化、加锁解锁操作,以及如何通过封装实现更安全和高效的资源管理。通过互斥量,我们可以确保临界区操作的线程安全性,避免数据竞争和资源冲突,为构建健壮的多线程应用奠定基础。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com