Linux线程的同步与互斥

线程互斥

• 共享资源

• 临界资源:多线程执⾏流被保护的共享的资源就叫做临界资源

• 临界区:每个线程内部,访问临界资源的 代码,就叫做临界区

• 互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起 保护作⽤

• 原⼦性(后⾯讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成, 要么未完成

线程共享空间

复制代码
┌─────────────────────────────┐
│      内核空间(所有进程共享) │
├─────────────────────────────┤
│                             │
│        共享库 .so/.dll       │  ✅ 线程共享(只读代码+全局数据)
│                             │
├─────────────────────────────┤
│        堆 Heap ↑            │  ✅ 线程共享(malloc/new)
│        向上增长              │
├─────────────────────────────┤
│                             │
│        数据段 .data          │  ✅ 线程共享:初始化全局变量、static变量
│        BSS段 .bss            │  ✅ 线程共享:未初始化全局、static变量
│                             │
├─────────────────────────────┤
│        代码段 .text          │  ✅ 线程共享:只读程序指令
├─────────────────────────────┤
│ 线程1栈 Stack ↓ (私有)      │  ❌ 线程私有:局部变量、函数参数
│ 线程2栈 Stack ↓ (私有)      │  ❌ 线程私有
│ 线程3栈 Stack ↓ (私有)      │  ❌ 线程私有
│  ...                         │
├─────────────────────────────┤
│ TLS 线程局部存储             │  ❌ 线程私有(__thread / thread_local)
└─────────────────────────────┘

|--------------|--------|------|---|---|---|
| 区域 | 是否线程共享 | 存放内容 | | | |
| .text 代码段 | ✅ 共享 | 程序二进制指令,只读 ||| |
| .data / .bss | ✅ 共享 | 全局变量、static 静态变量 ||| |
| Heap 堆 | ✅ 共享 | malloc、new 动态分配内存 ||| |
| 共享库 | ✅ 共享 | 动态库代码和全局变量 ||| |
| 线程栈 Stack | ❌ 私有 | 普通局部变量、函数参数、返回地址 ||||
| TLS | ❌ 私有 | __thread / thread_local 变量 ||| |
| 寄存器 | ❌ 私有 | 线程上下文,切换时保存恢复 ||| |

互斥量mutex(锁)

作用:

保护共享资源,保证临界区代码同一时间只有一个线程执行,解决竞态条件(race condition)

同一个进程多个线程共享全局变量、堆内存。 像 cnt++ 不是一条 CPU 指令,是三步:

  1. 把 cnt 从内存读到寄存器
  2. 寄存器 + 1
  3. 写回内存

线程切换插在中间,就会出现数据丢失、结果错乱 ,这就是竞态。 锁就是用来把这一组操作打包成原子操作。

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<pthread.h>
#include<unistd.h>

int count = 100;

void *route(void *argc)
{
    char *id = (char*)argc;
    while(1)
    {
        if(count > 0)
        {
            usleep(1000);
            printf("%s sells ticket: %d\n", id, count);
            count--;
        }
        else
        {
            break;
        }
    }

    return nullptr;
}

int main()
{
    pthread_t t1,t2,t3,t4;

    pthread_create(&t1,NULL,route,(void*)"thread 1");
    pthread_create(&t2,NULL,route,(void*)"thread 2");
    pthread_create(&t3,NULL,route,(void*)"thread 3");
    pthread_create(&t4,NULL,route,(void*)"thread 4");

    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    pthread_join(t3,NULL);
    pthread_join(t4,NULL);

    return 0;
}

互斥量(锁)的接口

静态初始化
cpp 复制代码
pthread_mutex_t mutex;
// 静态初始化(推荐全局/静态锁)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态分配
cpp 复制代码
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const 
pthread_mutexattr_t *restrict attr)
  • mutex:锁变量地址
  • attr:锁属性,NULL = 默认属性
  • 返回值:成功返回 0,失败返回错误码
销毁锁
cpp 复制代码
int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 用完释放资源;销毁前必须保证没有线程持有这个锁
  • 静态初始化的锁,程序退出前也建议 destroy
加锁
cpp 复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 锁空闲:直接拿到锁,返回
  • 锁已经被别的线程持有:阻塞休眠,直到锁释放

⚠️ 同一个线程重复 lock 默认会死锁(普通锁)

解锁
cpp 复制代码
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 释放锁,唤醒等待这个锁的线程
  • 谁加锁,尽量谁解锁;不能解锁一个没持有的锁(未定义行为)
非阻塞加锁
cpp 复制代码
int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • 锁空闲:拿到锁,返回 0
  • 锁被占用:不阻塞,直接返回 EBUSY 适合不想卡住线程的场景,轮询尝试加锁
限时阻塞加锁
cpp 复制代码
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
                            const struct timespec *restrict abstime);
  • 阻塞等待,直到拿到锁 或者 到达设置的绝对时间超时
  • 超时返回 ETIMEDOUT,防止永久死锁
cpp 复制代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<pthread.h>
#include<unistd.h>

int count = 100;
pthread_mutex_t mutex;

void *route(void *argc)
{
    char *id = (char*)argc;
    while(1)
    {
        pthread_mutex_lock(&mutex);
        if(count > 0)
        {
            usleep(1000);
            printf("%s sells ticket: %d\n", id, count);
            count--;\
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }

    return nullptr;
}

int main()
{
    pthread_t t1,t2,t3,t4;

    pthread_create(&t1,NULL,route,(void*)"thread 1");
    pthread_create(&t2,NULL,route,(void*)"thread 2");
    pthread_create(&t3,NULL,route,(void*)"thread 3");
    pthread_create(&t4,NULL,route,(void*)"thread 4");

    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    pthread_join(t3,NULL);
    pthread_join(t4,NULL);

    return 0;
}
锁的封装
cpp 复制代码
#pragma once
#include<iostream>
#include<pthread.h>

namespace LockModule
{
    class Mutex
    {
    public:
        Mutex(const Mutex&) = delete;
        const Mutex& operator = (const Mutex&) = delete;

        Mutex()
        {
            int n = ::pthread_mutex_init(&_lock, nullptr);
            (void)n;
        }
        ~Mutex()
        {
            int n = ::pthread_mutex_destroy(&_lock);
            (void)n;
        }
        void Lock()
        {
            int n = ::pthread_mutex_lock(&_lock);
            (void)n;
        }
        pthread_mutex_t *LockPtr()
        {
            return &_lock;
        }
        void Unlock()
        {
            int n = ::pthread_mutex_unlock(&_lock);
            (void)n;
        }
    private:
        pthread_mutex_t _lock;
    };
    class LockGuard
    {
    public:
        LockGuard(Mutex &mtx):_mtx(mtx)
        {
            _mtx.Lock();
        }
        ~LockGuard()
        {
            _mtx.Unlock();
        }
    private:
        Mutex &_mtx;
    };
} 

线程同步

条件变量

当⼀个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

例如⼀个线程访问队列时,发现队列为空,它只能等待,只到其它线程将⼀个节点添加到队列 中。这种情况就需要⽤到条件变量

同步概念与竞态条件

• 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从⽽有效避免 饥饿问题,叫做同步

• 竞态条件:因为时序问题,⽽导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也 不难理解

条件变量接口

定义

cpp 复制代码
// 静态初始化(全局/静态)
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

// 动态初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
                      const pthread_condattr_t *restrict attr);

销毁

cpp 复制代码
int pthread_cond_destroy(pthread_cond_t *cond);

等待

cpp 复制代码
int pthread_cond_wait(pthread_cond_t *restrict cond,
                      pthread_mutex_t *restrict mutex);
  1. 自动解锁 mutex
  2. 线程阻塞,放到条件变量等待队列,等待唤醒
  3. 收到唤醒信号后,自动重新加锁 mutex,函数返回,继续往下执行

⚠️ 必须在已经持有 mutex 的情况下调用! ⚠️ 必须用 while 循环判断条件,不能用 if!(存在虚假唤醒 spurious wakeup)

pthread_cond_wait 用于阻塞等待条件信号;调用时先释放互斥锁让别的线程可以抢到锁、修改条件,收到唤醒信号后,自动重新获取互斥锁再返回;必须配合 mutex,并且要用 while 循环处理虚假唤醒。

唤醒等待线程

cpp 复制代码
//唤醒一个等待线程
int pthread_cond_signal(pthread_cond_t *cond);

//唤醒全部等待线程
int pthread_cond_broadcast(pthread_cond_t *cond);
cpp 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void *active(void *argc)
{
    std::string name = static_cast<const char *>(argc);
    while(1)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex);
        printf("%s is active!\n", name.c_str());
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, nullptr, active, (void*)"thread-1");
    pthread_create(&tid2, nullptr, active, (void*)"thread-2");
    pthread_create(&tid3, nullptr, active, (void*)"thread-3");

    sleep(1);
    while(true)
    {
        printf("main wakeup thread...\n");
        //pthread_cond_signal(&cond);//唤醒单线程
        pthread_cond_broadcast(&cond);//唤醒多线程
        sleep(1);
    }

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
}

条件变量封装

cpp 复制代码
#pragma once

#include<iostream>
#include<pthread.h>
#include"Mutex.hpp"

namespace CondModule
{
    using namespace LockModule;

    class Cond
    {
    public:
        Cond()
        {
            int n = ::pthread_cond_init(&_cond, nullptr);
            (void)n;
        }
        void Wait(Mutex &mutex)
        {
            int n = ::pthread_cond_wait(&_cond, mutex.LockPtr());
        }
        void Notify()
        {
            int n = ::pthread_cond_signal(&_cond);
            (void)n;
        }
        void NotifyAll()
        {
            int n = ::pthread_cond_broadcast(&_cond); 
        }
        ~Cond()
        {
            int n = ::pthread_cond_destroy(&_cond);
        }
    private:
        pthread_cond_t _cond;
    };
}

生产消费模型

核心:

生产者负责往缓冲区放数据,消费者从缓冲区拿数据;缓冲区满,生产者阻塞;缓冲区空,消费者阻塞。

cpp 复制代码
#pragma once

#include <iostream>
#include <queue>
#include <pthread.h>
#include "Mutex.hpp"
#include "Cond.hpp"

namespace BlockQueueModule
{
    using namespace LockModule;
    using namespace CondModule;
    static const int gcap = 10;

    template <typename T>
    class BlockQueue
    {
    private:
        bool IsFull() 
        {
             return _q.size() == _cap;
        }
        bool IsEmpty() 
        {
             return _q.empty(); 
        }

    public:
        BlockQueue(int cap = gcap) : _cap(cap), _cwait_num(0), _pwait_num(0)
        {
        }
        void Equeue(const T &in)
        {
            LockGuard lockguard(_mutex);
            while (IsFull())
            {
                std::cout << "生产者进入等待..." << std::endl;

                _pwait_num++;
                _productor_cond.Wait(_mutex);
                _pwait_num--;

                std::cout << "生产者被唤醒..." << std::endl;
            }
            _q.push(in);

            if (_cwait_num)
            {
                std::cout << "唤醒消费者..." << std::endl;
                _consumer_cond.Notify();
            }
        }
        void Pop(T *out)
        {
            LockGuard lockguard(_mutex);
            while (IsEmpty())
            {
                std::cout << "消费者进入等待..." << std::endl;
                _cwait_num++;
                _consumer_cond.Wait(_mutex);
                _cwait_num--;
                std::cout << "消费者被唤醒.." << std::endl;
            }
            *out = _q.front();
            _q.pop();

            if (_pwait_num)
            {
                std::cout << "唤醒生产者..." << std::endl;
                _productor_cond.Notify();
            }
        }
        ~BlockQueue()
        {
        }

    private:
        std::queue<T> _q;     // 保护数据的容器,临界资源
        int _cap;             // bq的最大容量
        Mutex _mutex;         // 互斥
        Cond _productor_cond; // 生产者条件变量
        Cond _consumer_cond;  // 消费者条件变量

        int _cwait_num;
        int _pwait_num;
    };
}

POSIX信号量

信号量本质:一个计数器 + 等待队列,可以用来做【互斥】,也可以做【同步(线程顺序控制)】 互斥锁(mutex)只能 0/1;信号量计数器可以是大于 1 的数值

信号量接口

初始化信号量

cpp 复制代码
int sem_init(sem_t *sem, int pshared, unsigned int value);
  • sem:信号量变量
  • pshared:
    • 0:线程间共享(同进程内线程),最常用
    • 非 0:进程间共享(放在共享内存)
  • value:信号量初始值
  • 返回 0 成功

注意:信号量没有静态初始化方式,必须 sem_init

等待

cpp 复制代码
int sem_wait(sem_t *sem);
  • 计数器 > 0:计数器 -=1,直接返回
  • 计数器 == 0:阻塞等待 ,直到别的线程sem_post唤醒它

等价于:拿资源,拿不到就等

发布

cpp 复制代码
int sem_post(sem_t *sem);
  • 计数器 +=1
  • 如果有线程正在 sem_wait 阻塞,会唤醒一个等待的线程

等价于:归还资源

销毁

cs 复制代码
int sem_destroy(sem_t *sem);

线程安全

导致不安全原因

核心:多个线程,并发访问同一个共享资源,并且至少有一个线程是写操作,同时没有同步保护,就会线程不安全。 三个必要条件(同时满足才会不安全):

  1. 共享资源:多个线程访问同一个变量 / 对象(全局变量、静态成员、堆上同一个对象)
  2. 并发修改:至少一个线程对资源做写(修改)操作;只读不会有问题
  3. 缺少同步机制:没有锁、原子变量等手段保护临界区

具体拆解原因

  1. 指令不是原子的 比如 inst = new Singleton()、cnt++,不是一条 CPU 指令,会被拆成多步。线程调度器可以在中间切走线程,造成数据错乱。new 分为三步:分配内存 → 指针赋值 → 调用构造函数。 CPU / 编译器还可能指令重排,打乱执行顺序。
  2. 内存可见性问题 每个 CPU 有缓存,线程修改的变量可能只留在 CPU 缓存,没有刷回主存;其他线程读到旧值。
  3. 指令重排 编译器、CPU 为了性能优化,会调整代码执行顺序,保证单线程逻辑不变,但多线程下逻辑错乱。

一句话:多线程并发读写共享资源,缺少同步保护 → 线程不安全。

单例模式

饿汉VS懒汉

  • 饿汉:程序启动直接创建实例,天然线程安全;缺点:程序一开始就占用资源,不能延迟加载
  • 懒汉 :第一次调用GetInstance()才创建实例,按需加载;默认不安全,需要加手段保证线程安全

饿汉式

cpp 复制代码
class Singleton {
public:
    static Singleton* GetInstance() {
        return &inst_;
    }
private:
    // 私有化构造、拷贝、赋值,防止外部创建
    Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton inst_; // 全局静态变量,main之前就构造好
};
Singleton Singleton::inst_;
  • 实例在 main 函数之前,静态初始化阶段就完成构造
  • 此时还没有任何用户线程启动,不存在并发竞争 ✅ 安全原理:初始化发生在多线程运行之前,没有并发访问 缺点:不是懒加载

懒汉式

双重检查锁定 DCL(Double-Check Locking,面试最常问)

cpp 复制代码
#include <mutex>
class Singleton {
public:
    static Singleton* GetInstance() {
        if (inst_ == nullptr) {  // 第一次检查:已经创建就不抢锁(提升性能)
            std::lock_guard<std::mutex> lock(mtx_);
            if (inst_ == nullptr) { // 第二次检查:防止多个线程同时闯过第一层if
                inst_ = new Singleton();
            }
        }
        return inst_;
    }
private:
    Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static std::mutex mtx_;
    static Singleton* volatile inst_;
};
std::mutex Singleton::mtx_;
Singleton* volatile Singleton::inst_ = nullptr;

⚠️ 为什么要两层 if?

  • 第一层 if:避免每次调用都加锁,减少锁竞争,提高效率
  • 第二层 if:多个线程同时通过第一层 if,阻塞在锁上;一个线程创建完释放锁,后面拿到锁的线程要再次判断,避免重复创建

⚠️ volatile 的作用(C++11 前)

new Singleton() 编译器 / CPU 可能重排为三步:

  1. 分配内存
  2. 把 inst_指针指向这块内存
  3. 调用构造函数初始化对象

重排后:先赋值指针,后构造 。别的线程看到 inst_不为空,但对象还没构造完成,拿到半成品对象! volatile 阻止编译器优化、禁止指令重排。

✅ C++11 及以后,内存模型规范,std::atomic 也可以替代 volatile。

:C++11 静态局部变量懒汉式

cpp 复制代码
class Singleton {
public:
    static Singleton& GetInstance() {
        // 静态局部变量:第一次进入函数时才初始化
        // C++11标准保证:静态局部变量初始化是线程安全的!
        static Singleton inst;
        return inst;
    }
private:
    Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};
  1. 写法极简,自动线程安全(C++11 标准规定)
  2. 懒加载,第一次调用才创建
  3. 不用手动写锁、不用 volatile
  4. 栈静态变量,程序退出自动析构,不会内存泄漏
  • 不能控制析构时机(程序退出才析构)
  • 如果单例构造抛出异常,有问题
相关推荐
曲径海盗18 小时前
Try-Except怎么用,3个追问解答在自动化下的应用
java·jvm·oracle
寂寞的船4 天前
【Unity知识速递】第10期 Unity GC 零分配实战:从 UnityEngine.Pool 到手动 GC 控制的全链路优化
jvm·unity·c#·游戏引擎·lucene
金銀銅鐵6 天前
同一个 Hello World,Kotlin 和 Java 在字节码层面差在哪
jvm·后端·kotlin
yeccheng7 天前
MySQL 索引超详解:从底层 B+ 树到面试高频考点
java·开发语言·jvm·mysql·面试
On The Road :9 天前
深入理解 JVM 垃圾回收:GC 是什么、如何判定垃圾、四大回收算法全解析
jvm·算法
Yves..9 天前
JVM 学习笔记:内存区域、类加载机制与垃圾回收
jvm
akinggw10 天前
libagentcore - 一个基于c++的AI多agent系统升级到2.0版本
开发语言·jvm·c++
yh弓长10 天前
JVM 核心机制
jvm
tg_xianheyun11 天前
腾讯云国际账号注册代充值适合哪些海外业务场景?
jvm