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

相关文章

PHP轻松处理千万行数据的方法详解

《PHP轻松处理千万行数据的方法详解》说到处理大数据集,PHP通常不是第一个想到的语言,但如果你曾经需要处理数百万行数据而不让服务器崩溃或内存耗尽,你就会知道PHP用对了工具有多强大,下面小编就... 目录问题的本质php 中的数据流处理:为什么必不可少生成器:内存高效的迭代方式流量控制:避免系统过载一次性

MySQL的JDBC编程详解

《MySQL的JDBC编程详解》:本文主要介绍MySQL的JDBC编程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录前言一、前置知识1. 引入依赖2. 认识 url二、JDBC 操作流程1. JDBC 的写操作2. JDBC 的读操作总结前言本文介绍了mysq

Redis 的 SUBSCRIBE命令详解

《Redis的SUBSCRIBE命令详解》Redis的SUBSCRIBE命令用于订阅一个或多个频道,以便接收发送到这些频道的消息,本文给大家介绍Redis的SUBSCRIBE命令,感兴趣的朋友跟随... 目录基本语法工作原理示例消息格式相关命令python 示例Redis 的 SUBSCRIBE 命令用于订

防止Linux rm命令误操作的多场景防护方案与实践

《防止Linuxrm命令误操作的多场景防护方案与实践》在Linux系统中,rm命令是删除文件和目录的高效工具,但一旦误操作,如执行rm-rf/或rm-rf/*,极易导致系统数据灾难,本文针对不同场景... 目录引言理解 rm 命令及误操作风险rm 命令基础常见误操作案例防护方案使用 rm编程 别名及安全删除

Linux下MySQL数据库定时备份脚本与Crontab配置教学

《Linux下MySQL数据库定时备份脚本与Crontab配置教学》在生产环境中,数据库是核心资产之一,定期备份数据库可以有效防止意外数据丢失,本文将分享一份MySQL定时备份脚本,并讲解如何通过cr... 目录备份脚本详解脚本功能说明授权与可执行权限使用 Crontab 定时执行编辑 Crontab添加定

oracle 11g导入\导出(expdp impdp)之导入过程

《oracle11g导入导出(expdpimpdp)之导入过程》导出需使用SEC.DMP格式,无分号;建立expdir目录(E:/exp)并确保存在;导入在cmd下执行,需sys用户权限;若需修... 目录准备文件导入(impdp)1、建立directory2、导入语句 3、更改密码总结上一个环节,我们讲了

使用Python批量将.ncm格式的音频文件转换为.mp3格式的实战详解

《使用Python批量将.ncm格式的音频文件转换为.mp3格式的实战详解》本文详细介绍了如何使用Python通过ncmdump工具批量将.ncm音频转换为.mp3的步骤,包括安装、配置ffmpeg环... 目录1. 前言2. 安装 ncmdump3. 实现 .ncm 转 .mp34. 执行过程5. 执行结

Python中 try / except / else / finally 异常处理方法详解

《Python中try/except/else/finally异常处理方法详解》:本文主要介绍Python中try/except/else/finally异常处理方法的相关资料,涵... 目录1. 基本结构2. 各部分的作用tryexceptelsefinally3. 执行流程总结4. 常见用法(1)多个e

SpringBoot日志级别与日志分组详解

《SpringBoot日志级别与日志分组详解》文章介绍了日志级别(ALL至OFF)及其作用,说明SpringBoot默认日志级别为INFO,可通过application.properties调整全局或... 目录日志级别1、级别内容2、调整日志级别调整默认日志级别调整指定类的日志级别项目开发过程中,利用日志

Java中的抽象类与abstract 关键字使用详解

《Java中的抽象类与abstract关键字使用详解》:本文主要介绍Java中的抽象类与abstract关键字使用详解,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录一、抽象类的概念二、使用 abstract2.1 修饰类 => 抽象类2.2 修饰方法 => 抽象方法,没有