Linux多线程编程完全指南(下):线程同步与互斥锁

引言

在上一篇文章中,我们学习了线程的创建、退出和等待机制。但我们留下了一个重要问题没有解决------线程同步

当多个线程同时访问共享变量时,由于线程调度的不确定性,可能会出现意想不到的错误结果。今天,我们将深入探讨这个问题的根源,并学习如何使用互斥锁(Mutex) 来解决线程同步问题。


第一部分:多线程共享变量的竞态条件

一、问题演示:全局变量累加

先看一个简单的例子:创建5个线程,每个线程对同一个全局变量进行1000次自增操作。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define THREAD_NUM 5
#define LOOP_COUNT 1000

int g_count = 1;  // 全局共享变量

void* thread_func(void* arg) {
    for (int i = 0; i < LOOP_COUNT; i++) {
        g_count++;
        // 每次自增后打印当前值
        printf("%d\n", g_count);
    }
    return NULL;
}

int main() {
    pthread_t tids[THREAD_NUM];
    
    for (int i = 0; i < THREAD_NUM; i++) {
        pthread_create(&tids[i], NULL, thread_func, NULL);
    }
    
    for (int i = 0; i < THREAD_NUM; i++) {
        pthread_join(tids[i], NULL);
    }
    
    printf("最终结果: %d\n", g_count);
    return 0;
}

预期结果:

  • 初始值:1

  • 每个线程增加1000次,5个线程共增加5000次

  • 最终值应为:1 + 5000 = 5001

实际运行结果:

  • 单核处理器:总是输出5001

  • 多核处理器:经常输出小于5001(如4999、4998等)

二、现象分析

运行结果观察:

第1次运行: 5001

第2次运行: 5001

第3次运行: 4999 ← 丢失了2次累加

第4次运行: 5001

第5次运行: 4998 ← 丢失了3次累加

关键发现:

  • 单处理器环境下,问题不会出现

  • 多处理器环境下,问题出现概率与处理器数量相关

  • 处理器越多,出现问题的概率越高


第二部分:问题的根本原因

一、a++ 不是原子操作

在C语言中,a++ 看起来是一条语句,但在硬件层面,它被分解为多个步骤:

二、竞态条件的产生过程

当两个线程并行执行时,可能出现以下情况:

三、竞态条件的本质

条件 说明
共享资源 多个线程可以访问的变量或资源
非原子操作 操作可以被中断,不是一步完成的
并行执行 多处理器同时执行,导致操作交错
结果不可预测 最终结果取决于线程调度的具体顺序

第三部分:解决方案------互斥锁(Mutex)

一、互斥锁的概念

互斥锁(Mutex,Mutual Exclusion)是一种同步机制,确保同一时刻只有一个线程可以进入临界区。

二、互斥锁的核心操作

函数 作用 说明
pthread_mutex_init 初始化互斥锁 使用前必须初始化
pthread_mutex_lock 加锁 如果锁已被占用,阻塞等待
pthread_mutex_unlock 解锁 释放锁,唤醒等待线程
pthread_mutex_destroy 销毁互斥锁 使用完毕后销毁

三、使用互斥锁解决竞态条件

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define THREAD_NUM 5
#define LOOP_COUNT 1000

int g_count = 1;
pthread_mutex_t mutex;  // 定义互斥锁

void* thread_func(void* arg) {
    for (int i = 0; i < LOOP_COUNT; i++) {
        pthread_mutex_lock(&mutex);   // 加锁
        g_count++;                     // 临界区
        pthread_mutex_unlock(&mutex); // 解锁
    }
    return NULL;
}

int main() {
    pthread_t tids[THREAD_NUM];
    
    // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);
    
    for (int i = 0; i < THREAD_NUM; i++) {
        pthread_create(&tids[i], NULL, thread_func, NULL);
    }
    
    for (int i = 0; i < THREAD_NUM; i++) {
        pthread_join(tids[i], NULL);
    }
    
    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);
    
    printf("最终结果: %d\n", g_count);  // 总是输出 5001
    return 0;
}

第四部分:互斥锁的更多细节

一、互斥锁与信号量的关系

特性 互斥锁 信号量(初值为1)
本质 二进制锁 计数器
操作 lock/unlock P/V
所有权 只有加锁的线程才能解锁 任何线程都可以执行V操作
适用场景 保护临界区 资源计数、同步

二、互斥锁的完整接口

cpp 复制代码
#include <pthread.h>

// 静态初始化(适用于全局变量)
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_trylock(pthread_mutex_t *mutex);

// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

// 销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

三、互斥锁的使用规范

cpp 复制代码
// 规范1:加锁后必须在所有退出路径上解锁
void good_function() {
    pthread_mutex_lock(&mutex);
    
    if (error_condition) {
        pthread_mutex_unlock(&mutex);
        return;
    }
    
    // 正常处理
    pthread_mutex_unlock(&mutex);
}

// 规范2:临界区代码应尽量简短
void good_critical_section() {
    pthread_mutex_lock(&mutex);
    // 只保护必要的最小代码段
    count++;
    pthread_mutex_unlock(&mutex);
    
    // 耗时操作放在临界区之外
    do_expensive_work();
}

// 规范3:避免死锁------不要嵌套加锁
void avoid_deadlock() {
    // 如果需要多个锁,确保固定顺序
    pthread_mutex_lock(&mutex_a);
    pthread_mutex_lock(&mutex_b);
    // ...
    pthread_mutex_unlock(&mutex_b);
    pthread_mutex_unlock(&mutex_a);
}

第五部分:线程同步的应用场景

一、典型场景对比

场景 问题 解决方案
卖票系统 100张票卖出102张 互斥锁保护票数变量
账户转账 并发取款导致余额错误 互斥锁保护余额操作
日志系统 多条日志交错输出 互斥锁保护写入操作
生产者-消费者 缓冲区数据不一致 条件变量 + 互斥锁

二、卖票系统示例

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

int tickets = 100;  // 总票数
pthread_mutex_t mutex;

void* sell_ticket(void* arg) {
    int tid = *(int*)arg;
    
    while (1) {
        pthread_mutex_lock(&mutex);
        
        if (tickets > 0) {
            printf("线程%d售出第%d张票\n", tid, tickets);
            tickets--;
            pthread_mutex_unlock(&mutex);
            usleep(100);  // 模拟售票耗时
        } else {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    
    return NULL;
}

int main() {
    pthread_t tids[5];
    int ids[5];
    
    pthread_mutex_init(&mutex, NULL);
    
    for (int i = 0; i < 5; i++) {
        ids[i] = i + 1;
        pthread_create(&tids[i], NULL, sell_ticket, &ids[i]);
    }
    
    for (int i = 0; i < 5; i++) {
        pthread_join(tids[i], NULL);
    }
    
    pthread_mutex_destroy(&mutex);
    
    printf("剩余票数:%d\n", tickets);
    return 0;
}

第六部分:Ubuntu帮助手册安装

一、安装POSIX帮助手册

Ubuntu 20.04及之前版本需要手动安装线程相关帮助文档:

cpp 复制代码
# 安装帮助手册
sudo apt update
sudo apt install manpages-posix-dev

# 验证安装
man pthread_mutex_init
man pthread_mutex_lock
man pthread_mutex_unlock

二、帮助手册的使用

cpp 复制代码
# 查看函数原型和说明
man pthread_mutex_init

# 查看错误码
man pthread_mutex_lock

# 搜索相关函数
man -k pthread_mutex

第七部分:并发与并行的概念回顾

一、并发 vs 并行

概念 定义 硬件要求 执行特征
并发 多个任务交替执行 单处理器即可 某一时刻只有一个任务在执行
并行 多个任务同时执行 多处理器 某一时刻有多个任务在执行

二、对线程同步的影响

总结

一、核心概念对比

概念 说明
竞态条件 多线程同时访问共享资源导致的结果不确定性
原子操作 不可分割的操作(硬件层面)
临界区 需要互斥保护的代码段
互斥锁 保证同一时刻只有一个线程进入临界区

二、互斥锁核心函数

函数 作用 调用时机
pthread_mutex_init 初始化 创建线程之前
pthread_mutex_lock 加锁 进入临界区之前
pthread_mutex_unlock 解锁 退出临界区之后
pthread_mutex_destroy 销毁 所有线程结束后

三、使用规范总结

规范 说明
临界区最小化 只保护必要的代码
解锁路径完整 所有退出路径都要解锁
避免嵌套锁 防止死锁
初始化与销毁 配对使用

本篇文章介绍了多线程编程中的核心问题------竞态条件,以及使用互斥锁解决问题的标准方法。

核心要点回顾:

  1. a++ 不是原子操作,多线程并行执行时可能出错

  2. 互斥锁确保同一时刻只有一个线程进入临界区

  3. 加锁/解锁必须配对,避免死锁

  4. 临界区代码应尽量简短

学习建议:

  1. 编写多线程代码时,始终考虑共享资源的保护

  2. 使用互斥锁保护所有共享变量的访问

  3. 临界区越短越好

  4. 学会使用帮助手册查询函数接口

相关推荐
一个人旅程~1 小时前
Win旧版或win10部分版本如何解除260字符长路径名限制?
linux·windows·经验分享·电脑
iEdHu1 小时前
LinuxDO | L站 | Linux.do邀请码2026最新获取方式【邀请链接每日分享】
linux·经验分享·其他·社交电子
Lyyaoo.1 小时前
Session粘滞性问题->Redis实现session共享
数据库·redis·缓存
中国lanwp1 小时前
CentOS 7 搭建 NFS Server 服务端 + 客户端 完整一键配置
linux·运维·centos
charlie1145141912 小时前
嵌入式Linux驱动开发(8)——内存映射 I/O - 别拿物理地址当指针用
linux·开发语言·驱动开发·c·imx6ull
a2591748032-随心所记2 小时前
android拆解super.img内容
android·linux·运维·服务器
Mr_sst2 小时前
文件上传并发控制:为什么选Redisson可过期信号量?(避坑指南)
网络·数据库·redis·分布式·安全架构
倚楼盼风雨2 小时前
Redis 为什么快
数据库·redis·缓存
xiaoliuliu123452 小时前
redis-windows-7.2.3安装步骤详解(附Redis配置与Windows服务注册)
数据库·windows·redis