互斥锁与条件变量

互斥锁

互斥锁的作用

锁的种类有很多,包括互斥锁、文件上、读写锁等等,这些锁各有各的应用场景,这里我们主要探讨互斥锁的作用。 互斥锁指代互相排斥,它是最基本的同步形式,主要作用可以区分为以下两种,分别是:

  • 保护共享数据。

    在多线程/多进程编程中,多个线程/进程同时访问同一个数据,为了保护数据操作的准确性,需要加锁来实现保证这段数据在任何时刻只有且仅有一个线程/进程能够操作它。

  • 保持操作的互斥。

    一个程序有多个操作,但是同一时间内只有一个操作能执行,使用互斥锁,A/B两个操作就能控制A执行的时候B不能执行,同理B执行的时候A不能执行。

初始化锁

POSIX互斥锁被声明为具有pthread_mutex_t数据类型的变量,有两种方式来初始化它。

  • 使用常值PTHREAD_MUTEX_INITIALIZER静态初始化

    1
    
    pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER
  • 动态初始化

    1
    2
    3
    
    //原型: int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr *attr);
    pthread_mutex_t lock;/* 互斥锁定义 */
    pthread_mutex_init(&lock,NULL);/* 动态初始化,	成功返回0,失败返回非0 */

动态创建互斥锁的时候attr参数指定的是新建互斥锁的属性,如果参数为NULL,则使用默认的互斥锁属性互斥锁属性详解请看我的另外一篇文章互斥锁属性详解

加锁

多个线程/进程在对互斥锁上锁的过程中,如果某个线程已经锁住了互斥锁,其他线程使用pthread_mutex_lock给这个互斥锁上锁将会阻塞到它解锁为止,如果使用非阻塞加锁函数pthread_mutex_trylock加锁,且该互斥锁已锁住,则它就会返回一个EBUSY错误。

  • 阻塞加锁

    1
    2
    
     // 原型: int pthread_mutex_lock(pthread_mutex_t *mutex)
     pthread_mutex_lock(&lock);
  • 非阻塞加锁

    1
    2
    
    // 原型: int pthread_mutex_trylock(pthread_mutex_t *mutex)
    pthread_mutex_trylock(&lock);

解锁

使用该函数解锁的前提是互斥锁处于锁定状态,而且调用本函数的线程/进程必须是给这个互斥锁加锁的线程/进程,也就是常说的解铃还须系铃人。

```
// 原型: int pthread_mutex_unlock(pthread_mutex_t *mutex)
pthread_mutex_unlock(&lock);
```

如果多个线程/进程阻塞在等待同一个互斥锁上,那么当该互斥锁解锁时,哪一个线程/进程会开始运行呢? 这里要涉及到线程/进程的优先级调度,这个是操作系统维护的,我们先不讨论,大家只要知道操作系统为每个线程/进程都赋予了不同的优先级,同步函数(互斥锁,读写锁,信号量)将唤醒优先级最高的被阻塞线程/进程。

销毁锁

注意pthread_mutex_destory 必须是和pthread_mutex_init 成对出现的,缺一不可。 使用PTHREAD_MUTEX_INITIALIZER 初始化的互斥锁不需要调用该方法释放。 这里还有一个要注意的地方,调用pthread_mutex_destory后,存储互斥锁的相关内存并不会被释放,需要手动调用free来释放该内存

```
// 原型: int pthread_mutex_destory(pthread_mutex_t *mutex)
pthread_mutex_destory(&lock);
```

条件变量

如果说互斥锁是用来上锁的,那么条件变量则是用来等待。这两种不同的类型在同步的时候都需要。 条件变量用来自动阻塞一个线程直到某个特殊的情况发生,可以理解成一种"事件通知",互斥锁可用的时候,使用条件变量来通知,这个通知可以是单个通知也可以是广播通知。 通常互斥锁和条件变量同时使用。

条件变量的原则

  1. 等待条件变量总是返回被锁住的互斥量。
  2. 条件变量的作用是发送信号,而不是互斥。
  3. 一个条件变量应该只与一个"状态描述"相关联
  4. 所有并发等待一个条件变量的现成必须指定同一个互斥量。
  5. 条件变量提供了单播pthread_cond_signal和广播pthread_cond_broadcast两种模式,基于更安全,更高效等因素,优先考虑使用"广播“方式。
  6. 线程发送信号或者广播条件变量时看到的内存数据,同样也可以被唤醒的其他线程看到,但是在发送信号或广播后写入内存的数据不会被唤醒的线程看到,即使写操作发生在线程被唤醒之前。
  7. 一个内存地址只能保持一个值,不要让线程竞争已获得优先访问权。
  8. 在等待线程醒来的时候,检查其"状态"是否为真是不错的主意,同事应该总是在一个循环中等待条件变量。
  9. 条件变量是程序用来等待某个"状态"为真的机制.

初始化条件变量

条件变量与互斥锁一样,有两种创建方式,静态和动态。

  • 静态方式:

    pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

  • 动态方式:

    1
    
    int pthread_cond_init (pthread_cond_t *cond,const pthread_condattr_t *attr);

尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常为NULL,且被忽略。

等待条件变量被唤醒

1
int pthread_cond_wait (pthread_cond_t *cond,pthread_mutex_t *mutex);

pthread_cond_wait函数原子的执行了前2个动作。

  1. 给互斥锁解锁。
  2. 把调用线程投入睡眠,直到另外某个线程调用函数pthread_cond_signal,pthread_cond_broadcast产生了唤醒信号.
  3. 返回之前,重新给互斥锁上锁,如果未上锁成功则一直阻塞到上锁成功为止。 使用函数表示可以写成如下形式:
1
2
3
pthread_mutex_unlock(mtx)
wait()
pthread_mutex_lock(mtx)

以上代码第一行和第二行是原子性的,第三行有可能不是原子性,不影响。

等待条件变量被唤醒(带超时)

等待条件变量cond被唤醒,直到由一个信号或广播,或绝对时间abstime到才唤醒该线程

1
int pthread_cond_timedwait (pthread_cond_t  *cond,pthread_mutex_t *mutex,const struct timespec *abstime);

逻辑与上面的pthread_cond_wait一样,多了一个参数超时时间,这个时间是时间戳绝对时间,比如你需要休眠3秒钟,abstime的值不是3,而是当前时间加3秒。到了指定时间还未收到信号,则返回ETIMEDOUT,继续执行接下来的逻辑。

常见的错误码:

  • EINVAL 同时等待不同的互斥量; /cond,mutex/abstime无效;互斥量没有被主线程占有
  • ETIMEDOUT abstime指定绝对时间超时

信号通知条件变量

1
int pthread_cond_signal (pthread_cond_t *cond);

广播条件变量

唤醒所有等待该条件变量的线程,即等待者

1
int pthread_cond_broadcast (pthread_cond_t *cond);

销毁条件变量

1
int pthread_cond_destroy (pthread_cond_t *cond)

参考

0%