互斥锁mutex的简单实现

mutex一般用于为一段代码加锁,以保证这段代码的原子性(atomic)操作,即:要么不执行这段代码,要么将这段代码全部执行完毕。

例如,最简单的并发冲突问题就是一个变量自增1:

balance = balance + 1;

表面看这是一条语句,可是在背后的汇编中我们可以看到,指令集操作过程中会引入中间变量来保存右边的值,进而这个操作至少会被扩充为:

int tmp = balance + 1;
balance = tmp;

这就需要一把互斥锁(mutual exclusive, mutex)将这段代码给锁住,使其达到任何一个线程“要么全部执行上述代码,要么不执行这段代码”的效果。这个用法可以表示为:

lock_t mutex;
...
lock(&mutex)
    balance = balance + 1;
unlock(&mutex);

那么,一个自然的问题便是,我如何实现上面的这个lock()函数呢?

乍一看这个问题是非常复杂的,特别是考虑到它能够被适用于各种代码的各种情况。但经过各种简化,这个lock()实现,可以通过几个test和set的组合得以实现。

例如,

typedef struct __lock_t { int flag; } lock_t;

void init(lock_t *mutex) {
    // 0: lock is available
    // 1: lock is held
    mutex->flag = 0;
}

void lock(lock_t *mutex) {
    while (mutex->flag == 1) {  // Test the flag.
        ;    // Wait the lock
    mutex->flag = 1;  // Set the lock, i.e. start to hold lock
}

void unlock(lock_t *mutex) {
    mutex->flag = 0;
}

我第一次看到这个算法的时候非常惊讶,一个本来极其复杂的问题就这么优雅地被解决了。它仅仅涉及到对条件的检验和变量的复制,然后整个问题就这么轻而易举地被攻破了。

当然,我并没能看到上述代码的“坑”,也即是必须依靠指令集级别的支持才能真正做到atomic。这同样说明了并发程序的困难,稍微不注意便会调入一个万劫不复的坑里,并且你还不知道哪里出错了。

上述极端优雅的代码,有一个隐藏的坑,那便是在lock()函数的实现里,while循环那一段其实是可以被乱入的。

假设thread A是第一个运行到此的线程,那么它得到的mutex->flag就肯定是0,于是它继续跳出循环往下运行,希望通过下面的mutex->flag = 1来持有锁,使得其它线程在检测while循环时为真,进而进入循环的等待状态。

可如果在A执行到这个赋值为1的语句之前,又有另外一个thread B运行到了这个while循环部分,由于mutex->flag还未被赋值为1,B同样可以跳出while,从而跟A一样拿到这把锁!这就出现了冲突。

那怎么办呢?仔细后可以发现,其实关键问题就在于:

  • mutex->flag的检测
  • mutex->flag的赋值
    这两个操作必须是不被干扰的,也就是它必须是atomic的,要么这两段代码不被执行,要么这两段代码被不中断地完整执行。

这就需要借助CPU指令集的帮助,来保证上述两条语句的atomic操作,也即是著名的TestAndSet()操作。

int TestAndSet(int *ptr, int new) {
    int old = *ptr;
    *ptr = new;
    return old;
}

CPU的指令集,并不需要支持繁复的各种atomic操作。仅仅支持上面这个函数,各种互斥加锁的情形,便都能够被涵盖。

此时,在回到我们最开始的那个优雅的lock()实现,就可以将其改造为:

typedef struct __lock_t { int flag; } lock_t;

void init(lock_t *lock) {
    // 0: lock is available
    // 1: lock is held
    mutex->flag = 0;
}

void lock(lock_t *mutex) {
    while (TestAndSet(&lock_t->flag, 1) == 1) {
        ;
}

void unlock(lock_t *lock) {
    lock->flag = 0;
}

上述代码极其精巧。乍一看在lock()实现里不是还缺少一行mutex->flag = 1;么?可其实呢,它已经被整合到了TestAndSet()函数中。

这样的支持TestAndSet()的实现,便是最简单的spin lock,弹簧锁。之所以叫弹簧锁,那是因为在各类锁当中,弹簧锁就是最初的被投入工业使用的最简单的实现技术。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
好的,这是一个关于生产者消费者问题的描述。以下是一个简单实现,包含主程序、生产者进程和消费者进程: ```c #include <stdio.h> #include <pthread.h> #include <semaphore.h> #define BUFFER_SIZE 10 char buffer[BUFFER_SIZE]; int buffer_index; pthread_mutex_t mutex; sem_t full, empty; void *producer(void *arg) { while (1) { // 生成数据项 char item = 'A' + rand() % 26; // 若缓冲区满,等待信号量empty sem_wait(&empty); // 获取互斥mutex pthread_mutex_lock(&mutex); // 将数据项放入缓冲区 buffer[buffer_index++] = item; // 释放互斥mutex pthread_mutex_unlock(&mutex); // 发送信号量full sem_post(&full); } } void *consumer(void *arg) { while (1) { // 若缓冲区空,等待信号量full sem_wait(&full); // 获取互斥mutex pthread_mutex_lock(&mutex); // 取出缓冲区中的数据项 char item = buffer[--buffer_index]; // 释放互斥mutex pthread_mutex_unlock(&mutex); // 发送信号量empty sem_post(&empty); // 处理数据项 printf("Consumed item: %c\n", item); } } int main() { buffer_index = 0; // 初始化互斥量和信号量 pthread_mutex_init(&mutex, NULL); sem_init(&full, 0, 0); sem_init(&empty, 0, BUFFER_SIZE); // 创建消费者线程和生产者线程,启动线程 pthread_t producer_thread, consumer_thread; pthread_create(&producer_thread, NULL, producer, NULL); pthread_create(&consumer_thread, NULL, consumer, NULL); // 等待子线程完成后,销毁互斥量和信号量 pthread_join(producer_thread, NULL); pthread_join(consumer_thread, NULL); pthread_mutex_destroy(&mutex); sem_destroy(&full); sem_destroy(&empty); return 0; } ``` 这个程序中,我们首先定义了一个缓冲区数组和一个缓冲区索引变量,用于存放生产者生成的数据项。接着,我们定义了一个互斥mutex和两个信号量full、empty,分别表示缓冲区是否满或空。 在生产者进程中,我们使用while循环不断生成数据项。如果缓冲区已满,使用sem_wait()函数等待信号量empty。获取互斥mutex后,将数据项放入缓冲区中,然后释放互斥mutex。最后,使用sem_post()函数发送信号量full,通知消费者进程可以从缓冲区中取出数据项了。 在消费者进程中,我们也使用while循环不断消费数据项。如果缓冲区为空,使用sem_wait()函数等待信号量full。获取互斥mutex后,从缓冲区中取出数据项,并将缓冲区索引减一。然后,释放互斥mutex,并使用sem_post()函数发送信号量empty,通知生产者进程可以向缓冲区中放入数据项了。最后,我们处理取出的数据项,并输出到控制台上。 在主程序中,我们首先初始化互斥量和信号量。然后,创建消费者线程和生产者线程,并启动线程。最后,使用pthread_join()函数等待子线程完成后,销毁互斥量和信号量。 希望我的回答能够解决你的问题!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值