Linux线程同步/互斥过程详解

2025-08-02 20:50

本文主要是介绍Linux线程同步/互斥过程详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《Linux线程同步/互斥过程详解》文章讲解多线程并发访问导致竞态条件,需通过互斥锁、原子操作和条件变量实现线程安全与同步,分析死锁条件及避免方法,并介绍RAII封装技术提升资源管理效率...

01. 资源共享问题

1.1 多线程并发访问

例: 初始状态counter=0,线程 1 和 2 各自都执行counter++操作

Linux线程同步/互斥过程详解

要想对counter++做修改,在底层被编译成三条机器指令:

  1. 从内存加载counter的值到寄存器(LOAD)
  2. 寄存器中的值加1(ADD)
  3. 将寄存器的值写回内存(STORE)

假设counter初始值为0,在两个线程同时执行的时候,可能出现下面这种情况。以至于多线程场景中对全局变量并发访问不是 100%可靠的。

线程 1 执行

  • 从内存读取counter=0到寄存器。
  • 寄存器中counter+1=1未写回内存,就切换到另外一个线程。

线程 2 执行

  • 从内存读取counter=0(因线程 1 未更新内存)。
  • 寄存器中counter+1=1写回内存,此时counter=1

线程 1 恢复执行

  • 将寄存器中已计算的1写回内存,覆盖线程 2 的更新

最终结果counter=1(预期应为 2)。

1.2 临界区与临界资源

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

1.3 锁的引入

对于临界资源访问时的安全问题,也可以通过加锁来保证,实现多线程间的互斥访问,互斥锁就是解决多线程并发访问方法之一。

我们可以在线程1进入临界区之前加锁,出临界区之后解锁, 这样可以确保并发访问临界资源时的线性进行,若线程1在对共享资源进行操作时被切换成线程2,线程2也只能阻塞等待解锁。

Linux线程同步/互斥过程详解

注:

  • 加锁、解锁是比较耗费系统资源的,会在一定程序上降低程序的运行速度
  • 加锁后的代码是串行化执行的,势必会影响多线程场景中的运行速度
  • 所以为了尽可能的降低影响,加锁粒度要尽可能的细

02. 多线程案例

2.1 为什么线程需要互斥?

当多个线程同时访问共享资源时,可能导致竞态条件,造成数据不一致或程序异常。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。而多个线程并发的操作共享变量,会带来一些问题。线程互斥机制确保在任何时刻只有一个线程能访问共享资源。

#include <stdio.h>
#include <pthread.h>
int counter = 0;
void* increment(void* arg) {
    for (int i = 0; i < 100000; i++) {
        counter++; // 非原子操作}
    return NULL;}
int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, increment, NULL);
    pthread_create(&t2, NULL, increment, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    // 理论结果为:200000
    printf("Final counter: %d\n", counter); // 实际输出通常小于200000
    return 0;
}

Linux线程同步/互斥过程详解

在上面代码里面,我们知道counter是临界资源,而increment函数是访问临界资源的代码,亦称为临界区。

理想状态下是希望两个线程分别对counter100000次。但是由由于非原子操作内存可见性问题,当两个线程同时执行这些指令,可能会出现指令交错,导致最终结果通常会小于预期200000

要解决以上问题,需要做到三点:

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

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

2.2 线程或进程切换时机?

  1. 时间片耗尽时
  2. 有更高优先级的进程要调度时
  3. 通过sleep,从内核返回用户时,会进行时间片是否到达的检测,进而导致切换

Linux线程同步/互斥过程详解

如果锁对象是全局的或静态的,可以用宏:PTHREAD_MUTEX_INITIALIZER初始化,并且不用我们主动destroy;如果锁对象是局部的,需要用pthread_mutex_init初始化,用pthread_mutex_destroy释放。

  1. 所有对资源的保护,都是对临界区代码的访问,因为资源都是通过代码访问的。
  2. 要保证加锁的细粒度。
  3. 加锁就是找到临界区,对临界区进行加锁。

那么相应的又有一些问题:

  • 锁也是全局的共享资源,谁保证锁的安全?加锁和解锁被设计为原子的。
  • 如果看待锁?加锁本质就是对资源的预定工作,整体使用资源,所以加锁前先要申请锁。
  • 如果申请锁的时候,锁已经被别的线程拿走了怎么办?其他线程阻塞等待。
  • 线程在访问临界区的时候,可不可以被切换?可以,我被切走,其他线程也不能进来,因为我走的时候是带着锁走的,保证了原子性。

03. 线程互斥

3.1 互斥锁操作

有以下特点

  • 最简单的同步原语
  • 只有"锁定"和"未锁定"两种状态
  • 同一时间只允许一个线程持有锁
// 初始化(静态)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 初始化(动态)
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
// 加锁/解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

3.2代码互斥问题优化

通过对上面代码进行改进,我们便可以得到正确的结果。编程

Linux线程同步/互斥过程详解

细节: 互斥会给其他线程带来影响

当某个线程持有[锁资源】 时,对于其他线程的有意义的状态:在这两种状态的划分下,确保了多线程并发访问时的 原子性

  • 锁被我申请了(其他线程无法获取)
  • 锁被我释放了(其他线程可以获取锁)

3.3 互斥锁原理

lock是原子的,其他线程无法进入。 为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令,该指令的作用是把寄存器和内存单元的数据交换(私有和共享),由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

3.4 多线程封装

着手编写一个小组件: Demo 版线程库目标:对 原生线程库 提供的接口进行封装,进一步提高对线程相关接口的熟练程度既然是封装,这里的类成员包括:

  • 线程 ID
  • 线程名 name
  • 线程状态 status
  • 线程回调函数 fun t
  • 传递给回调函数的参数 args

3.4.1 thread.hpp编写

#pragma once

#include <IOStream>
#include <string>
#include <pthread.h>
#include <cassert>

// 参数、返回值为 void 的函数类型
typedef void *(*func_t)(void *);
const int num = 1024;
class Thread
{
public:
    Thread(func_t func, void *args = nupythonllptr, int number = 0)
        : _func(func), _args(args)
    {
        // 根据编号写入名字
        char buf[128];
        snprintf(buf, sizeof buf, "thread-%d", num);
        _name = buf;
        int n = pthread_create(&_tid, nullptr, runHelper, this); // this->Thread*
        assert(n == 0);
        (void)n;
    }
    // 回调方法
    static void *runHelper(void *args)
    {
        Thread *_this = static_cast<Thread *>(args);
        return _this->callback();
    }
    // 获取 ID
    pthread_t getTID() const
    {
        return _tid;
    }
    // 获取线程名
    std::string getName() const
    {
        return _name;
    }
    // 启动线程
    void run()
    {
        int ret = pthread_create(&_tid, nullptr, runHelper, this );//this 是一个指向当前类类型的常量指针
        if (ret != 0)
        {
            std::cerr << "create thread fail!" << std::endl;
            exit(1); // 创建线程失败,直接退出
        }
    }
    // 线程等待
    void join()
    {
        int ret = pthread_join(_tid, nullptr);
        if (ret != 0)
        {
            std::cerr << "thread join fail!" << std::endl;
            exit(1); // 等待失败,直接退出
        }
    }
    void *callback()
    { // 亦指在外调用的线程处理函数,_args与是否返回值有关
        return _func(_args);
    }

private:
    pthread_t _tid;    // 线程 ID
    std::string _name; // 线程名
    func_t _func;      // 线程回调函数
    void *_args;       // 传递给回调函数的参数
};

测试代码:

#include "thread.hpp"
// 1:线程创建和运行
void *basic_task(void *arg){
    int *val = static_cast<int *>(arg);
    std::cout << "线程正在运行,初始值为: " << *val << std::endl;
    *val *= 2; // 修改传入的值
    return nullptr;}
// 2:带返回值
void *task_with_returnjavascript(void *arg){
    std::string *msg = new std::string("Hello!");
    return msg;}
int main(){{
        int value = 42;
        Thread t1(basic_task, &value);
        t1.join();
        std::cout << "修改后旳值为: " << value << std::endl; // 应该输出84}
    std::cout << "---------------: " << std::endl;{
        Thread t2(task_with_return);
        void *ret_val = nullptr;
        pthread_join(t2.getTID(), &ret_val); // 直接使用pthread_join获取返回值
        if (ret_val){
            std::string *msg = static_cast<std::string *>(ret_val);
            std::cout << *msg << std::endl; // 输出线程返回的消息
            delete msg;                     // 记得释放内存
        }}  return 0;}

结果如下:

Linux线程同步/互斥过程详解

3.5 互斥锁封装

我们对锁进行封装,实现一个简单易用的小组件。利用创建对象时调用构造函数,对象生命周期结束时调用析构函数的特点,融入加锁、解锁等操作。更加方便

#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
    Mutex(const Mutex &) = delete;
    const Mutex &operator=(const Mutex &) = delete;
    Mutex(){
        int n = pthread_mutex_init(&_lock, nullptr);
    }
    void Lock(){
        int n = pthread_mutex_lock(&_lock);
    }
    void Unlock(){
        int n = pthread_mutex_unlock(&_lock);
    }
    pthread_mutex_t *LockPtr() { return &_lock; }
    ~Mutex(){
        int n = pthread_mutex_destroy(&_lock);
    }

private:
    pthread_mutex_t _lock;
};
class LockGuard{
public:
    LockGuard(Mutex &mutex)
        : _mutex(mutex){
        _mutex.Lock();
    }
    ~LockGuard(){
        _mutex.Unlock();
    }
private:
    Mutex &_mutex; // 在该类下面定义了一个Mutex类型的引用成员变量,_mutex为变量名
};

3.5.1 RAII风格

像这种获取资源即初始化的风格称为RAII风格,非常巧妙的运用了类和对象的特性,实现半自动化操作。

04. 线程同步

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如:一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

同步概念与竞态条件:

  • 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
  • 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解

4.1 死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

4.1.1 死锁四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

4.1.2 避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

4.1.3 避免死锁算法

  • 死锁检测算法(了解)
  • 银行家算法(了解

4.2 条件变量

条件变量是线程同步的高级机制,用于解决"等待特定条件成立"的场景。它总是与互斥锁配合使用,实现高效的线程等待-通知机制。有以下特点

  • 总是与互斥锁配合使用
  • 解决"等待-通知"问题
  • 避免忙等待(busy-waiting)

操作代码:

// 初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

// 等待条件满足(自动释放关联互斥锁)
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

// 通知条件
int pthread_cond_signal(pthread_cond_t *cond);      // 唤醒一个线程
int pthread_cond_broadcast(pthread_cond_t *cond);   // 广播。。唤醒所有线程

可以把条件变量看作一个结构体,其中包含一个队列结构,用来存储正在排队等候的线程信息,当条件满足时,就会取 队头 线程进行操作,操作完成后重新进入队尾。后续基于此实现生产者-消费者模型。

Linux线程同步/互斥过程详解

简单使用示例:

#include <pthread.h>
#inclphpude <stdio.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int data_ready = 0;  // 共享条件

void* consumer(void* arg) {
    pthread_mutex_lock(&mutex);
    while (data_ready == 0) {
        printf("Consumer: Waiting...\n");
        pthread_cond_wait(&cond, &mutex); // 阻塞并释放锁
    }
    printf("Consumer: Processing data.\n");
    data_ready = 0;
    pthread_mutex_unlock(&mutex);
    return NULL;
}

void* producer(void* arg) {
    sleep(1); // 模拟数据准备时间
    pthread_mutex_lock(&mutex);
    printf("Producer: Data ready.\n");
    data_ready = 1;
    pthread_cond_signal(&cond); // 唤醒消费者
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, consumer, NULL);
    pthread_create(&tid2, NULL, producer, NULL);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持China编程(www.chinasem.cn)。

这篇关于Linux线程同步/互斥过程详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1155567

相关文章

批量导入txt数据到的redis过程

《批量导入txt数据到的redis过程》用户通过将Redis命令逐行写入txt文件,利用管道模式运行客户端,成功执行批量删除以Product*匹配的Key操作,提高了数据清理效率... 目录批量导入txt数据到Redisjs把redis命令按一条 一行写到txt中管道命令运行redis客户端成功了批量删除k

分布式锁在Spring Boot应用中的实现过程

《分布式锁在SpringBoot应用中的实现过程》文章介绍在SpringBoot中通过自定义Lock注解、LockAspect切面和RedisLockUtils工具类实现分布式锁,确保多实例并发操作... 目录Lock注解LockASPect切面RedisLockUtils工具类总结在现代微服务架构中,分布

Win10安装Maven与环境变量配置过程

《Win10安装Maven与环境变量配置过程》本文介绍Maven的安装与配置方法,涵盖下载、环境变量设置、本地仓库及镜像配置,指导如何在IDEA中正确配置Maven,适用于Java及其他语言项目的构建... 目录Maven 是什么?一、下载二、安装三、配置环境四、验证测试五、配置本地仓库六、配置国内镜像地址

Python使用Tenacity一行代码实现自动重试详解

《Python使用Tenacity一行代码实现自动重试详解》tenacity是一个专为Python设计的通用重试库,它的核心理念就是用简单、清晰的方式,为任何可能失败的操作添加重试能力,下面我们就来看... 目录一切始于一个简单的 API 调用Tenacity 入门:一行代码实现优雅重试精细控制:让重试按我

Python实现网格交易策略的过程

《Python实现网格交易策略的过程》本文讲解Python网格交易策略,利用ccxt获取加密货币数据及backtrader回测,通过设定网格节点,低买高卖获利,适合震荡行情,下面跟我一起看看我们的第一... 网格交易是一种经典的量化交易策略,其核心思想是在价格上下预设多个“网格”,当价格触发特定网格时执行买

Python标准库之数据压缩和存档的应用详解

《Python标准库之数据压缩和存档的应用详解》在数据处理与存储领域,压缩和存档是提升效率的关键技术,Python标准库提供了一套完整的工具链,下面小编就来和大家简单介绍一下吧... 目录一、核心模块架构与设计哲学二、关键模块深度解析1.tarfile:专业级归档工具2.zipfile:跨平台归档首选3.

Oracle数据库定时备份脚本方式(Linux)

《Oracle数据库定时备份脚本方式(Linux)》文章介绍Oracle数据库自动备份方案,包含主机备份传输与备机解压导入流程,强调需提前全量删除原库数据避免报错,并需配置无密传输、定时任务及验证脚本... 目录说明主机脚本备机上自动导库脚本整个自动备份oracle数据库的过程(建议全程用root用户)总结

Linux如何查看文件权限的命令

《Linux如何查看文件权限的命令》Linux中使用ls-R命令递归查看指定目录及子目录下所有文件和文件夹的权限信息,以列表形式展示权限位、所有者、组等详细内容... 目录linux China编程查看文件权限命令输出结果示例这里是查看tomcat文件夹总结Linux 查看文件权限命令ls -l 文件或文件夹

idea的终端(Terminal)cmd的命令换成linux的命令详解

《idea的终端(Terminal)cmd的命令换成linux的命令详解》本文介绍IDEA配置Git的步骤:安装Git、修改终端设置并重启IDEA,强调顺序,作为个人经验分享,希望提供参考并支持脚本之... 目录一编程、设置前二、前置条件三、android设置四、设置后总结一、php设置前二、前置条件

python设置环境变量路径实现过程

《python设置环境变量路径实现过程》本文介绍设置Python路径的多种方法:临时设置(Windows用`set`,Linux/macOS用`export`)、永久设置(系统属性或shell配置文件... 目录设置python路径的方法临时设置环境变量(适用于当前会话)永久设置环境变量(Windows系统