互斥锁与条件变量学习与应用小结

互斥锁,也叫互斥量。有以下几个显著的特点:

  1. 唯一性:互斥锁保证在任何给定的时间点,只有一个线程可以获得对临界区资源的访问权。如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量。

  2. 原子性:锁定和解锁互斥锁的操作是原子的,这意味着操作系统(或pthread函数库)保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量。

  3. 非繁忙等待:当一个线程已经锁定了一个互斥量,其他试图锁定这个互斥量的线程将被挂起(不占用任何CPU资源),直到第一个线程解除对这个互斥量的锁定为止。被挂起的线程在锁被释放后会被唤醒并继续执行。

  4. 保护共享资源:互斥锁用于保护临界区资源免受多个线程同时访问和修改的影响,确保数据的完整性和一致性。

    ​ 另外,互斥锁,也成为“协同锁”或“建议锁”。当 A线程对某个全局变量加锁访问,8在访问前尝试加锁,拿不到锁,8阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。

    ​ 虽然它提供了锁定机制来避免多线程同时访问共享资源造成的竞态条件,但并没有强制限定线程必须遵循这一机制。也就是说,即使有互斥锁存在,如果线程不按照规则来访问数据,依然可能造成数据混乱。也就是说,互斥锁的有效性依赖于编程者的合作。因此,编程时需要根据编程人员的规则使用

条件变量不是锁,必须与互斥锁一起配合使用,因此在这同时记录两者的API接口及用法。

互斥锁使用时,有几个技巧小结如下:

  1. 尽量保证锁的粒度,越小越好。(即访问共享数据前,加锁,访问结束要立即解锁,让其他线程能访问到的概率更大,保证较高的并发度
  2. 将互斥锁变量mutex看成整数1,每次上锁即申请资源,mutex--;解锁即释放资源,mutex++,类似于信号量。

常用函数列举如下:

pthread_mutex_t mutex (= PTHREAD_MUTEX_INITIALIZER);                                // 互斥锁变量初始定义,本质是一个结构体,应用时可忽略
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *mutexattr); // 初始化(若已经用宏定义进行初始化,则不需要调用此函数),第二个参数一般用NULL。
//此处的restrict是关键字,表示对于 mutex指针指向的空间操作,均只能由mutex指针完成,不能依靠传递地址靠其他变量完成,换句话说,就是告诉编译器不会有其他指针指向同一块内存,从而允许编译器进行更高效的优化。
int pthread_mutex_lock(pthread_mutex_t *mutex);                                       // 上锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);                                       // 尝试进行上锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);                                    // 解锁
int pthread_mutex_destroy(pthread_cond_t *cond);                                      // 销毁

条件变量的特点罗列如下:

  1. 等待与通知机制:条件变量允许线程在某个特定条件不满足时进入等待状态(等待在条件变量上)。当其他线程改变了条件,并认为等待的线程应该被唤醒时,它会使用条件变量的通知(signal)或广播(broadcast)功能来唤醒等待的线程。
  2. 与互斥锁结合使用:条件变量必须与互斥锁一起使用,以确保在检查和修改条件时的原子性。在调用条件变量的等待函数时,锁定互斥锁,然后检查条件。如果条件不满足,则调用条件变量的等待函数并释放互斥锁,进入等待状态。当条件变量被通知后,线程会重新获取互斥锁并继续执行。
  3. 避免忙等待:使用条件变量可以避免线程在条件不满足时持续检查条件(即忙等待),这样可以节省CPU资源。线程在等待条件变量时会被挂起,直到被其他线程通知。
  4. 广播与通知:条件变量通常提供通知(notify)和广播(notifyAll)功能。通知只会唤醒等待在条件变量上的一个线程,而广播会唤醒所有等待在条件变量上的线程。

常用函数列举如下:

pthread_cond_t cond (= PTHREAD_COND_INITIALIZER);                                                       // 条件变量初始定义
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);                               // 初始化(若已经用宏定义进行初始化,则不需要调用此函数)
int pthread_cond_signal(pthread_cond_t *cond);                                                            // 唤醒一个等待中的线程
int pthread_cond_broadcast(pthread_cond_t *cond);                                                         // 唤醒全部线程
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);                                      // 等待被唤醒
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime); // 等待一段时间(abstime),
int pthread_cond_destroy(pthread_cond_t *cond);                                                           // 销毁

应用举例如下:

/**
 * @file name:	互斥锁与条件变量的应用展示
 * @brief	 :  子线程功能详note
 * @author [email protected]
 * @date 2024/06/01
 * @version 1.0 :版本
 * @property :
 * @note
 *          子线程B:当全局变量x和y满足条件时(>100),进行打印输出
 *          子线程C:对共享资源x和y进行每次+50的操作
 *          子线程D:对共享资源x和y进行每次+30的操作,并调整sleep函数与unlock函数的位置,与子线程C进行对比
 * CopyRight (c)  2023-2024   [email protected]   All Right Reseverd
 */

#include <pthread.h> //关于线程API接口的头文件   编译时需要指定  -pthread
#include <stdio.h>
#include <unistd.h>
int x = 0, y = 0; // 共享资源

pthread_cond_t cond_flag = PTHREAD_COND_INITIALIZER; // 此处已用宏定义,就不用初始化函数进行初始化了
pthread_mutex_t mutex_flag = PTHREAD_MUTEX_INITIALIZER;

void *task_B(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&mutex_flag); // 上锁,“申请资源”
        while (x < 100 && y < 100)
        {
            pthread_cond_wait(&cond_flag, &mutex_flag); // 条件阻塞,即当x,y<100时,进行挂起等待其他线程的信号
        }
        printf("x 和y已达到要求:%d %d\n", x, y);
        x = 0;
        y = 0;
        pthread_mutex_unlock(&mutex_flag); // 解锁,“释放资源”
        sleep(2);                          // 为保证输出效果进行延时
    }
}

void *task_C(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&mutex_flag); // 上锁,“申请资源”
        x += 50;
        y += 50;						 //申请到资源后对x和y进行操作
        printf("子线程C对x和y进行操作:%d %d\n", x, y);
        if (x > 100 && y > 100)
        {
            pthread_cond_signal(&cond_flag);
        }								   //满足条件后,发送一条信号给子线程B
        pthread_mutex_unlock(&mutex_flag); // 解锁,“释放资源”
        sleep(2);                          // 为方便展示,进行延迟
    }
}

void *task_D(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&mutex_flag); // 同子线程C
        x += 30;
        y += 30;
        printf("子线程C对x和y进行操作:%d %d\n", x, y);
        if (x > 100 && y > 100)
        {
            pthread_cond_signal(&cond_flag);
        }
        sleep(2);                          // 此处与子线程C进行对比,先sleep,再解锁
        pthread_mutex_unlock(&mutex_flag); 
    }
}
int main(int argc, char const *argv[])
{
    // 1.创建子线程
    pthread_t B_tid;
    pthread_create(&B_tid, NULL, task_B, NULL); // 子线程B
    pthread_t C_tid;
    pthread_create(&C_tid, NULL, task_C, NULL); // 子线程C
    pthread_t D_tid;
    pthread_create(&D_tid, NULL, task_D, NULL); // 子线程C

    // 2.主线程结束
    pthread_exit(NULL);
    return 0; // 主线程在上一条语句已经结束,这条语句永远不会执行
}

上述程序仅运行子线程B与C时,输出结果同预期,如下:

子线程C对x和y进行操作:50 50
子线程C对x和y进行操作:100 100
子线程C对x和y进行操作:150 150
x 和y已达到要求:150 150
子线程C对x和y进行操作:50 50
子线程C对x和y进行操作:100 100
子线程C对x和y进行操作:150 150
x 和y已达到要求:150 150
子线程C对x和y进行操作:50 50
子线程C对x和y进行操作:100 100
子线程C对x和y进行操作:150 150
x 和y已达到要求:150 150

上述程序运行子线程B,C与D时,输出结果如下:

子线程C对x和y进行操作:50 50
子线程C对x和y进行操作:80 80
子线程C对x和y进行操作:110 110
子线程C对x和y进行操作:140 140
子线程C对x和y进行操作:170 170
子线程C对x和y进行操作:200 200
子线程C对x和y进行操作:230 230
子线程C对x和y进行操作:260 260
子线程C对x和y进行操作:290 290
子线程C对x和y进行操作:320 320
子线程C对x和y进行操作:350 350
x 和y已达到要求:350 350
子线程C对x和y进行操作:30 30
子线程C对x和y进行操作:60 60
子线程C对x和y进行操作:90 90
子线程C对x和y进行操作:120 120
子线程C对x和y进行操作:150 150

​ 可见,子线程D抢占资源频繁,子线程C一直在等待资源,而子线程B满足条件收到信号以后,也无法抢到共享资源进行输出,并发度不高。因此,为了提高粒度,需要子线程D在对x和y进行操作后立即进行解锁,然后sleep阻塞,让其他线程有机会得到共享资源,提高并发度。

热门相关:黄金渔场   女配她逆袭了   农家小地主   豪门宠婚:权少夫人萌上天   大首长,小媳妇