前言:在多线程编程中,线程互斥是确保数据一致性和程序正确性的核心机制。当多个线程同时访问共享资源时,如果没有适当的互斥保护,就会引发竞态条件和数据竞争问题,导致程序行为不可预测。

一、线程互斥问题与实际案例

1.1 互斥问题的本质

互斥问题本质上是由于多个线程同时访问共享资源(临界资源)而产生的竞态条件。当多个线程执行对共享资源的修改操作时,如果没有适当的保护措施,就可能出现"判断-操作"过程的非原子性,导致多个线程同时进入临界区,从而引发数据不一致问题。

竞态条件是指程序的执行结果依赖于线程调度的不确定顺序。例如,在抢票系统中,多个线程同时检查票数是否大于零,然后进行购票操作,就可能导致票数变为负数。

1.2 临界区

临界区(Critical Section) 是指在多线程或多进程程序中,共享资源被多个线程或进程同时访问和修改的代码区域。为了确保共享资源在多线程或多进程环境中的一致性和正确性,我们需要对访问临界区的操作进行同步控制,以避免发生竞争条件(Race Condition)。

1.3 经典案例分析
1.3.1 抢票系统案例

如下抢票系统经典案例

#include <iostream>
#include <unistd.h>
#include <pthread.h>

int tickets = 100;

void* buyTicket(void* arg) {
    char* name = (char*)arg;
    while (1) {
        if (tickets > 0) {
            usleep(1000);  // 模拟购票操作耗时
            std::cout << name << " buys ticket: " << tickets << std::endl;
            tickets--;
        } else {
            break;
        }
    }
    return nullptr;
}

int main() {
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr, buyTicket, (void*) "Thread1");
    pthread_create(&t2, nullptr, buyTicket, (void*) "Thread2");
    pthread_create(&t3, nullptr, buyTicket, (void*) "Thread3");
    pthread_create(&t4, nullptr, buyTicket, (void*) "Thread4");

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

    return 0;
}

运行结果可能出现:

Thread1 buys ticket: 100
Thread2 buys ticket: 100
Thread3 buys ticket: 99
Thread4 buys ticket: 99
...
Thread1 buys ticket: 1
Thread2 buys ticket: 1
Thread1 buys ticket: 0
Thread2 buys ticket: 0
Thread3 buys ticket: -1
Thread4 buys ticket: -1

很显然,结果中tickets小于0并不是我们想看到的,本质是多个线程执行buyTicket,因为线程调度以及没有对资源做保护,导致有多个线程同时进入if判断中,而tickets--操作不是原子的,此时如果tickets已经为0了,当线程被调度回来时,再对tickets进行减一的话,就会出现负数的情况。

tickets--操作为何不是原子的呢?是因为代码会被编译成多条汇编语句,在操作系统当中该语句的实现其实是分为多步进行的,具体的步骤如下所示:

取出tickets--部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax #
600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) #
600b34 <ticket>

• load :将共享变量tickets从内存加载到寄存器中
• update : 更新寄存器里面的值,执行-1操作
• store :将新值,从寄存器写回共享变量tickets的内存地

因此以上的tickets--就可以理解为,首先是当前所在的线程会将内存当中的数据加载到CPU当中,CPU当中对该数据进行处理之后将其的值保存到对应的寄存器当中,之后再将寄存器当中的值写回到内存中。

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

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

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

1.3.2 银行账户操作案例

如下是一个扩展例子

#include <stdio.h>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>

typedef struct {
    int code;
    double balance;
} Account;

// 取款函数
double withdraw(Account* a, double amt) {
    if (amt > a->balance) {
        return 0.0;
    }
    double balance = a->balance;
    sleep(1);  // 模拟操作耗时
    balance -= amt;
    a->balance = balance;
    return amt;
}

// 存款函数
double deposit(Account* a, double amt) {
    assert(a != NULL);
    if (amt < 0) {
        return 0.0;
    }
    double balance = a->balance;
    sleep(1);  // 模拟操作耗时
    balance += amt;
    a->balance = balance;
    return amt;
}

如果多个线程同时调用withdrawdeposit函数,就可能出现账户余额不一致的问题,例如存款和取款操作交错进行导致最终余额异常。

那么为了解决在以上多执行流在访问数据的时候出现数据不一致的问题,那么在Linux当中就提供了互斥量的来实现以上提到锁的概念。

有了互斥量之后就可以将临界资源当中的临界区进行保护

二、互斥量概念与实现原理

2.1 互斥量基本概念

**互斥量(Mutex)**是一种用于实现线程互斥的同步机制,它提供了一种简单的方法来确保同一时刻只有一个线程能够访问共享资源 (临界资源)。互斥量主要有两种状态:锁定(被占用)和解锁(可用)。

2.1.1 互斥量的关键特性
  • 互斥性:同一时刻只能有一个线程持有互斥量 
  • 可重入性:允许同一线程多次获取同一互斥量(递归互斥量)
  • 优先级继承:解决优先级反转问题的关键机制
  • 非阻塞获取:尝试获取互斥量但不阻塞线程的能力
2.2 互斥量的实现原理

互斥量的实现基于原子操作和内核调度机制,其核心是通过原子交换操作实现对共享资源的独占访问。

2.2.1 互斥量的数据结构

在Linux的NPTL(NativePOSIX线程库)实现中,互斥量的数据结构包含以下几个关键字段 :

struct __pthread_mutex_s {
    int __lock;            // 锁的状态标记:0表示未锁定,1表示已锁定(基础状态)
    union {
        int __count;       // 仅用于递归锁(PTHREAD_MUTEX_RECURSIVE):记录同一线程加锁次数(解锁需对应次数)
        int __owner;       // 用于错误检查锁/普通锁:记录当前持有锁的线程ID(避免非法解锁)
    } __data;
    int __nusers;          // 等待该锁的线程数量(辅助调度唤醒)
    struct __pthread_internal_list __waiters;  // 等待锁的线程链表(阻塞队列,存放等待线程的控制块)
    struct __pthread_mutexattr_s __attr;       // 互斥锁的属性(如类型、协议等,对应初始化时的属性设置)
    long int __g1_pad;     // 缓存行填充(padding):避免相邻数据共享CPU缓存行,减少缓存伪共享
    long int __g2_pad;     // 同上,确保结构体大小对齐到CPU缓存行(通常64字节),提升访问效率
};
2.2.2 互斥量的底层操作

互斥量的加锁和解锁操作在底层通过原子指令实现。例如,在x86架构上,加锁操作可能对应以下伪汇编代码 :

lock:
    movb $0, %al
    xchgb %al, mutex
    if(al寄存器的内容 > 0)
        {return 0;} 
    else
        挂起等待;
        goto lock;
unlock:
    movb $1, mutex
    唤醒等待 Mutex 的线程;
    return 0;

当线程调用pthread_mutex_lock,它会尝试通过原子交换将互斥量的值从1变为0。如果交换成功(原值为1),则线程获得锁;如果交换失败(原值为0),则线程被阻塞并加入等待队列。

通过以上就可以看出锁实现的实现本质是将内存当中的变量交换对应线程上寄存器当中,而不是拷贝,那么这时就能保证对应的变量只有一份,而谁申请到了该变量就能访问临界区。

当线程调用pthread_mutex_unlock,它会把mutex的值重新变为1并唤醒等待 Mutex 的线程。

三、互斥量的使用方法与代码实现

3.1 互斥量的类型与属性

Linux提供了多种互斥量类型,每种类型适用于不同的场景:

互斥量类型特性适用场景
PTHREAD_MUTEX_INITIALIZER静态初始化,自动销毁全局或静态变量
PTHREAD_MUTEX_NORMAL动态初始化,需手动销毁局部变量
THREAD_MUTEX_ERRORCHECK错误检查型,可检测非法操作需要严格错误检查的场景
PTHREAD_MUTEX_RECURSIVE递归型,允许同一线程多次获取锁递归函数或复杂逻辑
3.1.1 设置互斥量属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);

// 设置互斥量类型为递归型
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

// 创建互斥量
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);

// 销毁属性
pthread_mutexattr destroy(&attr);
3.2 互斥量的基本使用
3.2.1 初始化互斥量

初始化互斥量有两种方法:
方法1:静态分配:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法2,动态分配:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr);
参数:
        mutex:要初始化的互斥量
        attr:NULL
3.2.2 销毁互斥量

销毁互斥量需要注意:

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销
  •  不要销毁一个已经加锁的互斥量
  •  已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
3.2.3 互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

调用 pthread_ lock 时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
3.2.4 修复抢票系统的正确实现
#include <iostream>
#include <unistd.h>
#include <pthread.h>

int tickets = 100;
pthread_mutex_t ticket_mutex = PTHREAD_MUTEX_INITIALIZER;

void* buyTicket(void* arg) {
    char* name = (char*)arg;
    while (1) {
        // 尝试获取锁
        int rc = pthread_mutex_lock(&ticket_mutex);
        if (rc != 0) {
            std::cerr << "Error: Could not lock mutex (" << rc << ")\n";
            break;
        }

        if (tickets > 0) {
            usleep(1000);
            std::cout << name << " buys ticket: " << tickets << std::endl;
            tickets--;
        } else {
            std::cout << name << " exits due to no tickets left\n";
            break;
        }

        // 释放锁
        rc = pthread_mutex_unlock(&ticket_mutex);
        if (rc != 0) {
            std::cerr << "Error: Could not unlock mutex (" << rc << ")\n";
        }
    }
    return nullptr;
}

int main() {
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr, buyTicket, (void*) "Thread1");
    pthread_create(&t2, nullptr, buyTicket, (void*) "Thread2");
    pthread_create(&t3, nullptr, buyTicket, (void*) "Thread3");
    pthread_create(&t4, nullptr, buyTicket, (void*) "Thread4");

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

    // 销毁互斥量
    pthread_mutex_destroy(&ticket_mutex);

    return 0;
}
3.2.5 递归互斥量的使用示例
#include <iostream>
#include <pthread.h>

std::recursive_mutex recursive_mutex;
int counter = 0;

void recursive_function(int depth) {
    if (depth <= 0) {
        return;
    }

    recursive_mutex.lock();  // 第一次加锁
    counter++;
    std::cout << "Counter incremented to: " << counter << std::endl;
    recursive_mutex.lock();  // 第二次加锁(递归)

    recursive_function(depth - 1);

    recursive_mutex.unlock();  // 第二次解锁
    counter--;
    std::cout << "Counter decremented to: " << counter << std::endl;
    recursive_mutex.unlock();  // 第一次解锁
}

int main() {
    std::thread t1(recursive_function, 3);
    std::thread t2(recursive_function, 2);

    t1.join();
    t2.join();

    return 0;
}
3.3 C++中的互斥量封装

C++11引入了标准线程库,提供了更安全的互斥量封装方式

3.3.1 使用lock_guard自动管理锁
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex mtx;
std::vector<int> shared_vector;

void add_elements(int start, int end) {
    // 使用lock_guard自动加锁和解锁   RAII风格
    std::lock_guard<std::mutex> lock(mtx);

    for (int i = start; i < end; ++i) {
        shared_vector.push_back(i);
        std::cout << "Thread " << std::this_thread::get_id()
                  << " added " << i << std::endl;
    }
}

int main() {
    std::thread t1(add_elements, 0, 5000);
    std::thread t2(add_elements, 5000, 10000);

    t1.join();
    t2.join();

    std::cout << "Vector size: " << shared_vector.size() << std::endl;
    return 0;
}
3.3.2 使用unique_lock实现更灵活的锁管理
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <chrono>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool dataReady = false;
int data = 0;

// 生产者线程
void producer() {
    std::unique_lock<std::mutex> lock(mtx);

    // 生成数据
    data = 42;
    dataReady = true;

    // 通知消费者
    cv.notify_one();
}

// 消费者线程
void consumer() {
    std::unique_lock<std::mutex> lock(mtx);

    // 等待数据准备就绪
    while (!dataReady) {
        cv.wait(lock);  // 释放锁并等待,唤醒后重新加锁
    }

    std::cout << "Data received: " << data << std::endl;
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);

    t1.join();
    t2.join();

    return 0;
}


四、最佳实践与性能优化

4.1 互斥量的最佳实践
  1. 锁的粒度控制:将锁的作用范围限制在最小的必要代码块内,减少锁的持有时间
  2. 避免长持有锁:不要在持有锁时执行可能阻塞的操作(如I/O、网络调用)
  3. 使用RAII封装:优先使用C++的std::lock_guardstd::unique_lock自动管理锁
  4. 设置合理的超时:使用pthread_mutex_lock的超时版本避免无限等待
  5. 避免嵌套锁:如果必须使用嵌套锁,考虑使用递归互斥量
4.2 互斥量的性能优化

下面是对使用互斥量的一些优化,主要是代码用例,感兴趣的可以扩展学习一下。

4.2.1 减少锁竞争
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

// 使用多个互斥量保护不同的数据区域
std::mutex mtx1, mtx2, mtx3, mtx4;
std::vector<int> data1, data2, data3, data4;

void process_data(int region) {
    switch (region) {
        case 1:
            std::lock_guard<std::mutex> lock(mtx1);
            // 处理data1
            break;
        case 2:
            std::lock_guard<std::mutex> lock(mtx2);
            // 处理data2
            break;
        // 其他区域...
    }
}
4.2.2 读写锁的使用
#include <iostream>
#include <thread>
#include <mutex>
#include <shared_mutex>

std::shared_mutex rw_mutex;
std::string shared_data;

// 读线程
void reader() {
    std::shared_lock<std::shared_mutex> lock(rw_mutex);
    // 读取shared_data
    std::cout << "Reader " << std::this_thread::get_id()
              << " reads: " << shared_data << std::endl;
}

// 写线程
void writer() {
    std::lock_guard<std::shared_mutex> lock(rw_mutex);
    // 修改shared_data
    shared_data = "Updated data";
    std::cout << "Writer " << std::this_thread::get_id()
              << " writes: " << shared_data << std::endl;
}

4.2.3 无锁编程技术
#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> counter(0);

void increment() {
    for (int i = 0; i < 10000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

void decrement() {
    for (int i = 0; i < 10000; ++i) {
        counter.fetch_sub(1, std::memory_order_relaxed);
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(decrement);

    t1.join();
    t2.join();

    std::cout << "Final counter value: " << counter << std::endl;
    return 0;
}
4.3 互斥量与条件变量的协同使用
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::mutex mtx;
std::condition_variable cond;
std::queue<int> work_queue;

// 生产者线程
void producer() {
    for (int i = 0; i < 100; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        work_queue.push(i);
        std::cout << "Producer added " << i << std::endl;
        cond.notify_one();  // 通知一个等待的消费者
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }

    // 发送队列结束信号
    std::unique_lock<std::mutex> lock(mtx);
    work_queue.push(-1);  // 使用特殊值表示结束
    cond.notify_all();
}

// 消费者线程
void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cond.wait(lock, [] { return !work_queue.empty(); });

        int val = work_queue.front();
        work_queue.pop();

        if (val == -1) {
            break;
        }

        // 使用资源
        std::cout << "Consumer " << std::this_thread::get_id()
                  << " processed " << val << std::endl;
    }
}


五、总结

​​线程互斥是多线程编程中的基础机制,通过互斥量可以有效保护共享资源的访问。

在实际应用中,开发者需要:

  1. 明确识别临界资源和临界区
  2. 选择合适的互斥量类型和属性
  3. 仔细处理互斥量的获取和释放
  4. 避免死锁、优先级反转和活锁等并发问题
  5. 在性能敏感场景考虑锁优化或无锁编程技术

最后,希望本博客能为大家提供一些帮助,如果出现任何逻辑错误,欢迎指正,共勉呀。


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐