81-互斥量 mutex

依然以抢票问题为例。前面的文章从提出问题,到发现问题,而本文则是解决问题。通常解决问题的方式不止一种,但是为了避免复杂化,本文只讲互斥量。

1. 互斥量

1.1 基本概念

为了确保同一时间只有一个线程访问数据,在访问共享资源前需要对互斥量上锁。一旦对互斥量上锁后,任何其他试图再次对互斥量上锁的线程都会被阻塞,即进入等待队列

上面的文字用伪代码表示:

lock(&mutex);
// 访问共享资源
unlock(&mutex);

有些同学可能觉得通过代码很容易实现,其实不然。比方说下面这样:

// 加锁
if (flag == 0) {
  flag == 1;
}
// 访问共享资源
// 解锁
flag == 0;

上面这种做法是错误的,你有没有想过,标记 flag 也是共享资源?

实际上,有一种称之为 peterson 的算法可以解决两线程互斥问题,它的原理并不容易,如果你对此有兴趣,请参考《深入理解互斥锁的实现》

1.2 互斥量的数据类型

pthread 中,互斥量是用 pthread_mutex_t 数据类型表示的,通常它是一个结构体。在使用它前,必须先对它进行初始化。有两种方法可以对它进行初始化:

  • 通过静态分配的方法,将它设置为常量 PTHREAD_MUTEX_INITIALIZER.
  • 使用函数 pthread_mutex_init 进行初始化,如果是用此种方法初始化的互斥量,用完后还需要使用 pthread_mutex_destroy 对其进行回收。
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
  const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutext_t *mutex);

在上面的函数中,有两点需要提一下:

  • (1) restrict 关键字的含义:访问指针 mutex 指针的内容的唯一方法是使用 mutex 指针。通常这是告诉编译器:除了 mutex 指针指向这个内存,再也没别的指针指向这里了。
  • (2) pthread_mutexattr_t 类型,用来描述互斥量的属性。现阶段,attr 指针默认设置为 NULL.

1.3 互斥量的加锁和解锁

// 用于加锁的两个函数
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);

// 解锁只有下面这一种方法
int pthread_mutex_unlock(pthread_mutex_t *mutex);

在第 1.1 节中说,如果试图对一个已加锁的互斥量上锁,会让线程阻塞进入等待队列,实际上这是对 pthread_mutex_lock 函数说的。

如果使用 pthread_mutex_trylock,无论互斥量之前有没有上锁,线程会立即返回而不会阻塞,它是通过返回值来判断是否上锁成功:

  • 如果 pthread_mutex_trylock 返回 0, 表示上锁成功。
  • 如果 pthread_mutex_trylock 返回 EBUSY,表示上锁失败。

所以这两种上锁的函数唯一区别就是一个是阻塞函数,另一个是非阻塞函数。不过通常不使用非阻塞版本的,它会浪费 cpu,除非你别有用意。

有同学可能会好奇,为什么我们自己用一个共享的 flag 变量做标记不行,而这里的 lock 函数却可以做到?这是因为 lock 函数对 mutex 的操作是原子的,所谓的原子操作,就是要么一次执行成功,要么一次执行失败。而你使用全局 flag 变量,是做不到这一点的,从 if (flag == 0) 到 flag = 1的赋值操作是分成了两个步骤,翻译成汇编语句那就需要更多条了。所以如果你想自己实现这样的原子操作,就只能使用汇编语句来编写啦,有可能的话,后面我自己用代码来实现一个!(当然用关中断也可以,只不过没必要如此麻烦。)

2. 解决抢票问题

说了一大堆的概念,小伙伴可能已经迫不急待的想看代码了,这的确是一种速度最快的方式^_^

2.1 程序清单

// solve.c
#include <unistd.h>
#include <stdio.h>
#include <pthread.h>

int tickets = 3;
// 使用静态初始化的方式初始化一把互斥锁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void* allen(void* arg) {
  int flag = 1;
  while(flag) {
    // 上锁
    pthread_mutex_lock(&lock);
    int t = tickets;
    usleep(1000*20);// 20ms
    if (t > 0) {
      printf("allen buy a ticket\n");
      --t;
      usleep(1000*20);// 20ms
      tickets = t;
    }   
    else flag = 0;
    // 解锁
    pthread_mutex_unlock(&lock);
    usleep(1000*20);// 20ms
  }
  return NULL;
}

void* luffy(void* arg) {
  int flag = 1;
  while(flag) {
    // 上锁
    pthread_mutex_lock(&lock);
    int t = tickets;
    usleep(1000*20);
    if (t > 0) {
      printf("luffy buy a ticket\n");
      --t;
      usleep(1000*20);// 20ms
      tickets = t;
    }   
    else flag = 0;
    // 解锁
    pthread_mutex_unlock(&lock);
    usleep(1000*20);// 20ms
  }
  return NULL;
}
int main() {
  pthread_t tid1, tid2;
  pthread_create(&tid1, NULL, allen, NULL);
  pthread_create(&tid2, NULL, luffy, NULL);
  pthread_join(tid1, NULL);
  pthread_join(tid2, NULL);
  return 0;
}

2.2 编译和运行

$ gcc solve.c -o solve -lpthread
$ ./slove


这里写图片描述
图1 运行结果

从图 1 中可以看到,3 张票是被正常的抢走了,没有产生多抢的现象。luffy 还是比较厉害一点,抢了 2 张票,而 allen 只抢到了一张票,悲催……

3. 总结

  • 理解什么是互斥量,它的作用是什么
  • 掌握两种初始化互斥量的方法
  • 掌握两种对互斥量加锁的方法
  • 掌握解锁方法

练习 1:使用 pthread_mutex_init 函数初始化互斥量,记得用完后回收。
练习 2:使用非阻塞版本的加锁函数修改本文中的实验。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本题中涉及到了三个线程和一个互斥,需要注意线程优先级和互斥的使用。同时需要设置一个全局变来对三个线程进行标记,以便观察它们的运行状态。 首先,我们需要定义三个线程和一个互斥: ```c static rt_thread_t thread1 = RT_NULL; static rt_thread_t thread2 = RT_NULL; static rt_thread_t thread3 = RT_NULL; static rt_mutex_t mutex; ``` 然后,我们需要设置三个线程的优先级,创建并启动这三个线程: ```c static char thread1_stack[1024]; static void thread1_entry(void* parameter) { rt_thread_delay(100); rt_kprintf("Thread2 priority is %d, Thread3 priority is %d\n", rt_thread_get_priority(thread2), rt_thread_get_priority(thread3)); } static char thread2_stack[1024]; static void thread2_entry(void* parameter) { rt_kprintf("Thread2 priority is %d\n", rt_thread_get_priority(thread2)); rt_thread_delay(50); rt_mutex_take(&mutex, RT_WAITING_FOREVER); rt_mutex_release(&mutex); } static char thread3_stack[1024]; static void thread3_entry(void* parameter) { rt_kprintf("Thread3 priority is %d\n", rt_thread_get_priority(thread3)); rt_mutex_take(&mutex, RT_WAITING_FOREVER); for (int i = 0; i < 500; ++i) { rt_thread_delay(1); } rt_mutex_release(&mutex); } void thread_init() { rt_err_t result; result = rt_mutex_init(&mutex, "mutex", RT_IPC_FLAG_FIFO); RT_ASSERT(result == RT_EOK); thread1 = rt_thread_create("thread1", thread1_entry, RT_NULL, sizeof(thread1_stack), 9, 20); RT_ASSERT(thread1 != RT_NULL); result = rt_thread_startup(thread1); RT_ASSERT(result == RT_EOK); thread2 = rt_thread_create("thread2", thread2_entry, RT_NULL, sizeof(thread2_stack), 10, 20); RT_ASSERT(thread2 != RT_NULL); result = rt_thread_startup(thread2); RT_ASSERT(result == RT_EOK); thread3 = rt_thread_create("thread3", thread3_entry, RT_NULL, sizeof(thread3_stack), 11, 20); RT_ASSERT(thread3 != RT_NULL); result = rt_thread_startup(thread3); RT_ASSERT(result == RT_EOK); } ``` 最后,我们需要设置一个全局变来对三个线程进行标记,以便观察它们的运行状态: ```c static uint32_t thread_flags = 0; #define THREAD1_FLAG (1 << 0) #define THREAD2_FLAG (1 << 1) #define THREAD3_FLAG (1 << 2) #define SET_THREAD_FLAG(flag) do { \ rt_enter_critical(); \ thread_flags |= (flag); \ rt_exit_critical(); \ } while (0) #define CLEAR_THREAD_FLAG(flag) do { \ rt_enter_critical(); \ thread_flags &= ~(flag); \ rt_exit_critical(); \ } while (0) ``` 在每个线程的入口函数中,我们需要在适当的位置设置标志位: ```c static char thread1_stack[1024]; static void thread1_entry(void* parameter) { rt_thread_delay(100); rt_kprintf("Thread2 priority is %d, Thread3 priority is %d\n", rt_thread_get_priority(thread2), rt_thread_get_priority(thread3)); SET_THREAD_FLAG(THREAD1_FLAG); } static char thread2_stack[1024]; static void thread2_entry(void* parameter) { rt_kprintf("Thread2 priority is %d\n", rt_thread_get_priority(thread2)); rt_thread_delay(50); rt_mutex_take(&mutex, RT_WAITING_FOREVER); rt_mutex_release(&mutex); SET_THREAD_FLAG(THREAD2_FLAG); } static char thread3_stack[1024]; static void thread3_entry(void* parameter) { rt_kprintf("Thread3 priority is %d\n", rt_thread_get_priority(thread3)); rt_mutex_take(&mutex, RT_WAITING_FOREVER); for (int i = 0; i < 500; ++i) { rt_thread_delay(1); } rt_mutex_release(&mutex); SET_THREAD_FLAG(THREAD3_FLAG); } ``` 可以在主函数中添加一个循环,不断检查标志位的值,以观察三个线程的运行状态: ```c int main(void) { thread_init(); while (1) { rt_kprintf("Thread1 %s running\n", (thread_flags & THREAD1_FLAG) ? "is" : "is not"); rt_kprintf("Thread2 %s running\n", (thread_flags & THREAD2_FLAG) ? "is" : "is not"); rt_kprintf("Thread3 %s running\n", (thread_flags & THREAD3_FLAG) ? "is" : "is not"); rt_thread_delay(1000); } return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值