互斥锁和读写锁

1. 互斥锁

简单来说,互斥锁是用于解决线程在共享资源使用冲突问题。

  • 互斥锁
  • 读写锁

利用互斥锁可以有效的控制线程执行顺序,执行处理和共享资源使用问题。

1.2 互斥锁相关函数
1.2.1 锁类型 pthread_mutex_t
typedef union
{
  struct __pthread_mutex_s __data;
  char __size[__SIZEOF_PTHREAD_MUTEX_T];
  long int __align;
} pthread_mutex_t;
  • 定义互斥锁变量:声明一个 pthread_mutex_t 类型的变量。
  • 初始化互斥锁:使用 pthread_mutex_init 函数对互斥锁进行初始化。
  • 加锁:在访问共享资源之前,使用 pthread_mutex_lock 函数对互斥锁进行加锁操作,以确保只有当前线程可以访问共享资源。
  • 访问共享资源:在加锁之后,线程可以安全地访问和修改共享资源。
  • 解锁:在完成对共享资源的访问后,使用 pthread_mutex_unlock 函数对互斥锁进行解锁操作,以便其他线程可以访问共享资源。
  • 销毁互斥锁:当不再需要互斥锁时,使用 pthread_mutex_destroy 函数销毁互斥锁,释放相关资源。
1.2.2 pthread_mutex_init 互斥锁初始化

函数文档

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
                       const pthread_mutexattr_t *restrict attr);
  • 函数功能:
    • 定义 pthread_mutex_t 类型变量之后,必须进行 pthread_mutex_init 函数初始化操作,从而满足当前线程锁使用需求
  • 函数参数
    • pthread_mutex_t *restrict mutex 对应 pthread_mutex_t 指针地址
    • const pthread_mutexattr_t *restrict attr 初始化 pthread_mutex_t 线程锁类型变量对应的线程锁属性结构数据。通常情况下都是 NULL,利用系统默认值即可
  • 返回值类型
    • 若函数执行成功,返回值为 0。
    • 若执行过程中出现错误,会返回一个非零的错误码。常见的错误码及对应的错误情况如下:
      • EAGAIN:系统资源不足,无法初始化互斥锁。
      • ENOMEM:内存不足,无法分配必要的资源来初始化互斥锁。
      • EPERM:调用者没有足够的权限来设置指定的属性。
1.2.3 pthread_mutex_destroy 互斥锁销毁函数

函数文档

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 函数功能:
    • 销毁/释放当前线程中使用的锁变量
  • 函数参数:
    • pthread_mutex_t *mutex 对应线程锁变量的首地址
  • 返回值类型:
    • 若函数执行成功,返回值为 0。
    • 执行过程中出现错误,会返回一个非零的错误码。
1.2.4 pthread_mutex_lock 互斥锁【落锁】

函数文档

#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • 函数功能:
    • 在线程代码中执行,对应线程锁【落锁】,当前资源有且只允许,一个线程执行,其他线程进行【锁阻塞/锁等待】
  • 函数参数:
    • pthread_mutex_t *mutex 对应线程锁变量的首地址
  • 返回值类型:
    • 若函数执行成功,返回值为 0。
    • 执行过程中出现错误,会返回一个非零的错误码。
1.2.5 pthread_mutex_unlock 互斥锁【开锁】

函数文档

#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 函数功能:
    • 在线程代码中执行,对应线程锁【开锁】,一旦开锁,当前锁变量限制共享资源,开放给其他线程进行【抢占执行】
  • 函数参数:
    • pthread_mutex_t *mutex 对应线程锁变量的首地址
  • 返回值类型:
    • 若函数执行成功,返回值为 0。
    • 执行过程中出现错误,会返回一个非零的错误码。
1.2.6 互斥锁案例代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

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

/*
定义一个全局变量 pthread_mutex_t ,作用锁变量
*/
pthread_mutex_t mutex;

void print(void *arg);
void *thread_fun1(void *arg);
void *thread_fun2(void *arg);

int main(int argc, char const *argv[])
{
    pthread_t thid1 = 0;
    pthread_t thid2 = 0;

    pthread_mutex_init(&mutex, NULL);

    pthread_create(&thid1, NULL, thread_fun1, "Hello World!");
    pthread_create(&thid2, NULL, thread_fun2, "Debug Forever!");

    pthread_join(thid1, NULL);
    pthread_join(thid2, NULL);

    print("Hello World!");

    pthread_mutex_destroy(&mutex);
    return 0;
}

void print(void *arg)
{
    char *str = (char *)arg;
    /*
    stdout 标准输出,对应的就是终端内容,
    fflush 刷新缓冲区函数,会将输出缓冲区的内容刷新到终端/目标文件
    */

    while (*str != '\0')
    {
        printf("%c", *str);
        str += 1;
        sleep(1);
        fflush(stdout);
    }

    printf("\n");
}

void *thread_fun1(void *arg)
{

    pthread_mutex_lock(&mutex);

    // printf("PThread_ID : %ld ", pthread_self());
    print(arg);

    pthread_mutex_unlock(&mutex);

    return NULL;
}

void *thread_fun2(void *arg)
{
    pthread_mutex_lock(&mutex);

    // printf("PThread_ID : %ld ", pthread_self());
    print(arg);

    pthread_mutex_unlock(&mutex);

    return NULL;
}
1.2.7 死锁

总结

  • 死锁是因为线程落锁之后,后续代码中未对当前线程锁进行解锁操作,导致其他线程无法使用锁限制的共享资源
  • 死锁情况下出现与两个或者两个以上的线程发生,尤其是处理共享资源问题。
  • 死锁问题,在代码执行阶段无法解决,只能在开发阶段避免。
1.3 读写锁
1.3.1 读写锁概述

用于解决在线程中,针对于 IO 操作的读写限制。关注的功能函数为系统调用的 write 和 read 函数,或者针对性处理的标准输入输出资源。

  • 多线程情况下,A 线程读取,B 线程读取,C 线程读取,ABC 三个线程可以并行执行。
  • 多线程情况下,A 线程读取,B 线程写入,C 线程读取,ABC 按照数据安全要求,无法并行执行,需要利用读写锁/互斥锁进行限制。读写锁的处理效率高于互斥锁

在读写锁情况下

  • 允许多个线程进行同步读操作,不允许出现写入操作
  • 如果出现了一个线程执行写入操作,不允许其他任何线程进行读写操作。
1.3.2 读写锁类型 pthread_rwlock_t

pthread_rwlock_t 读写锁数据类型,在 C 语言 Linux 环境开发中,需要指定 POSIX 版本,解决类型无法使用的问题

代码起始位置需要使用宏明确告知当前 POSIX 版本

#define _POSIX_C_SOURCE 200809L  // 指定 POSIX 版本
/*
200809L 指定 POSIX 版本,当前 200809 版本是目前最为常用的 POSIX 协议标注和版本。
*/
1.3.3 pthread_rwlock_init 读写锁初始化函数

函数文档

#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, 
					const pthread_rwlockattr_t *restrict attr);
  • 函数功能:
    • 初始化读写锁操作,提供给函数参数是读写锁变量地址和对应的初始化使用读写锁属性结构体
  • 函数参数:
    • pthread_rwlock_t *restrict rwlock : 读写锁 pthread_rwlock_t 变量地址
    • const pthread_rwlockattr_t *restrict attr : 读写锁属性结构体指针,用于初始化当前读写锁相关的配置,通常情况下是 NULL
  • 返回值:
    • 若函数执行成功,返回值为 0。
    • 若执行过程中出现错误,会返回一个非零的错误码。常见的错误码及对应的错误情况如下:
      • EAGAIN:系统资源不足,无法初始化互斥锁。
      • ENOMEM:内存不足,无法分配必要的资源来初始化互斥锁。
      • EPERM:调用者没有足够的权限来设置指定的属性。
1.3.4 pthread_rwlock_destroy 读写锁销毁函数

函数文档

#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  • 函数功能:
    • 销毁当前代码中,目标读写锁内容
  • 函数参数:
    • pthread_rwlock_t *rwlock : 目标读写锁变量地址
  • 返回类型
    • 成功返回 0
    • 失败返回一个 非 0 的错误值
1.3.5 pthread_rwlock_rdlock

函数文档

#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
  • 函数功能:
    • 当前线程获取到当前读权限锁。会导致线程阻塞,直到获取到读锁
  • 函数参数:
    • pthread_rwlock_t *rwlock : 目标读写锁变量地址
  • 返回值类型
    • 成功返回 0,失败返回错误码。如果已有线程持有写锁,调用线程会被阻塞,直到写锁被释放。
1.3.6 pthread_rwlock_tryrdlock 尝试获取读锁

函数文档 【较多使用】

#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
  • 函数功能:
    • 当前线程尝试获取到当前读权限锁。不会阻塞代码执行,如果因为其他线程得到读锁,无法获取到当前写锁权限,代码继续执行, 返回对应的错误信息。
  • 函数参数:
    • pthread_rwlock_t *rwlock : 目标读写锁变量地址
  • 返回值类型
    • 若能立即获取读锁,返回 0;若已有线程持有写锁,返回 EBUSY
1.3.7 pthread_rwlock_wrlock

函数文档

#include <pthread.h>
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
  • 函数功能:
    • 当前线程获取锁对应写权限,如果无法获取到写锁,阻塞等待
  • 函数参数:
    • pthread_rwlock_t *rwlock : 目标读写锁变量地址
  • 返回值类型
    • 若能立即获取写锁,返回 0;若已有线程持有读锁或者写锁,返回 EBUSY
1.3.8 pthread_rwlock_destroy 读写锁销毁函数

函数文档

#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  • 函数功能:
    • 销毁当前程序中使用的读写数据
  • 函数参数
    • pthread_rwlock_t *rwlock : 目标读写锁变量地址
  • 返回值类型
    • 销毁读写锁成功,返回 0;如果销毁失败,返回非 0 错误值
1.3.9 读写锁操作流程
  • 定义读写锁变量:声明一个 pthread_rwlock_t 类型的变量,用于后续操作的读写锁对象。
  • 初始化读写锁:使用 pthread_rwlock_init 函数对读写锁进行初始化,若使用默认属性,可将属性参数设为 NULL
  • 获取读锁
    • 若使用 pthread_rwlock_rdlock 函数,当无线程持有写锁时可立即获取读锁;若有线程持有写锁,线程会阻塞直至写锁释放。
    • 若使用 pthread_rwlock_tryrdlock 函数,尝试获取读锁,若有线程持有写锁则立即返回 EBUSY ,不会阻塞线程。
  • 获取写锁
    • 若使用 pthread_rwlock_wrlock 函数,当无线程持有读锁或写锁时可立即获取写锁;若有线程持有读锁或写锁,线程会阻塞直至锁释放。
    • 若使用 pthread_rwlock_trywrlock 函数,尝试获取写锁,若有线程持有读锁或写锁则立即返回 EBUSY ,不会阻塞线程。
  • 访问共享资源
    • 读操作:多个线程可同时持有读锁,并发地进行读操作。
    • 写操作:仅一个线程可持有写锁,期间其他线程不能获取读锁或写锁,保证写操作原子性。
  • 释放锁:使用 pthread_rwlock_unlock 函数释放读锁或写锁,使其他线程可继续访问资源。
  • 销毁读写锁:当不再需要读写锁时,使用 pthread_rwlock_destroy 函数销毁读写锁,释放相关系统资源。
#define _POSIX_C_SOURCE 200809L // 指定 POSIX 版本
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

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

// 读写锁变量
pthread_rwlock_t rwlock;

void *read_threadA(void *arg);
void *read_threadB(void *arg);
void *read_threadC(void *arg);
void *write_thread(void *arg);

int main(int argc, char const *argv[])
{
    pthread_t td1 = 0;
    pthread_t td2 = 0;
    pthread_t td3 = 0;
    pthread_t td4 = 0;

    int num = 10;

    // 读写锁变量初始化
    pthread_rwlock_init(&rwlock, NULL);

    pthread_create(&td1, NULL, read_threadA, &num);
    pthread_create(&td2, NULL, read_threadB, &num);
    pthread_create(&td3, NULL, read_threadC, &num);
    pthread_create(&td4, NULL, write_thread, &num);

    pthread_join(td1, NULL);
    pthread_join(td2, NULL);
    pthread_join(td3, NULL);
    pthread_join(td4, NULL);

    // 销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

void *read_threadA(void *arg)
{
    int *num = (int *)arg;

    while (1)
    {
        // 尝试申请读锁
        pthread_rwlock_tryrdlock(&rwlock);

        printf("A 线程读取数据, 读取数据内容为 : %d\n", *num);

        // 读写锁开锁
        pthread_rwlock_unlock(&rwlock);

        sleep(1);
    }
}

void *read_threadB(void *arg)
{
    int *num = (int *)arg;

    while (1)
    {
        // 尝试申请读锁
        pthread_rwlock_tryrdlock(&rwlock);

        printf("B 线程读取数据, 读取数据内容为 : %d\n", *num);

        // 读写锁开锁
        pthread_rwlock_unlock(&rwlock);

        sleep(1);
    }
}


void *read_threadC(void *arg)
{
    int *num = (int *)arg;

    while (1)
    {
        // 尝试申请读锁
        pthread_rwlock_tryrdlock(&rwlock);

        printf("C 线程读取数据, 读取数据内容为 : %d\n", *num);

        // 读写锁开锁
        pthread_rwlock_unlock(&rwlock);

        sleep(1);
    }
}

void *write_thread(void *arg)
{
    int *num = (int *)arg;

    while (1)
    {
        // 申请写锁
        pthread_rwlock_wrlock(&rwlock);

        // 修改线程外部变量数据内容,观察效果
        *num += 1;
        printf("【写入线程】修改数据,当前 num 的数据内容为 : %d\n", *num);

        // 读写锁开锁
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
}

2. 条件变量

2.1 条件变量概述

pthread_cond_t 对应单词是 pthread condition type,条件变量不是锁,但是可以控制互斥锁,配合互斥锁使用。

  • 条件变量是一个阻塞行为。
  • 条件变量,满足线程继续执行
  • 条件变量不满足,当前线程进入阻塞状态。【打开互斥锁】允许其他线程进入正常线程执行抢占行为。

消费者和生产者模式。

2.2 条件变量
2.2.1 pthread_cond_t 类型

条件变量类型,要求提供的头文件是 <pthread.h>

2.2.2 pthread_cond_init 初始化条件变量

函数文档

#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, 
                      const pthread_condattr_t *restrict attr);
  • 函数功能:
    • 初始化 pthread_cond_t 条件变量数据,参数需要条件变量地址和条件变量初始化属性参数,通常情况下,属性参数为 NULL ,采用系统默认模式
  • 函数参数:
    • pthread_cond_t *restrict cond : pthread_cond_t 条件变量类型数据地址
    • const pthread_condattr_t *restrict attr : 条件变量初始化属性参数,默认 NULL
  • 返回值
    • 初始化成功,返回 0
    • 失败返回 非 0 错误码
2.2.3 pthread_cond_destroy 初始化条件销毁

函数文档

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *restrict cond);
  • 函数功能:
    • 销毁当前程序中使用的条件变量数据
  • 函数参数:
    • pthread_cond_t *restrict cond : pthread_cond_t 条件变量类型数据地址
  • 返回值
    • 初始化成功,返回 0
    • 失败返回 非 0 错误码
2.2.4 pthread_cond_wait 等待条件满足

函数文档

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, 
                      pthread_mutex_t *restrict mutex);
  • 函数功能:
    • 【阻塞】等待一个条件变量
        1. 阻塞等待条件变量 cond 满足
        1. 【释放】已掌握的 mutex 对应互斥锁进行解锁操作,相当于 pthread_mutex_unlock(&mutex);
      • 以上两个步骤操作是【原子操作】,不可分隔,必须整体执行完毕
    • 当前线程进入一个【阻塞休眠状态】,必须等待其他线程进行唤醒操作/条件变量满足,才会被唤醒之后,解除阻塞,同时申请获取锁对象,执行当前线程。
  • 函数参数:
    • pthread_cond_t *restrict cond : 条件变量指针
    • pthread_mutex_t *restrict mutex : 互斥锁变量指针
  • 返回值
    • 成功返回 0
    • 失败返回一个 非 0 错误码
2.2.5 pthread_cond_signal 条件变量休眠线程随机唤醒一个线程

函数文档

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
  • 函数功能:
    • 唤醒一个与当前条件变量相关的处于,条件变量阻塞状态情况下的线程程序。具体唤醒哪一个,看系统情况,无法明确限制。
  • 函数参数:
    • pthread_cond_t *restrict cond : 条件变量指针
  • 返回值:
    • 成功返回 0
    • 失败返回一个 非 0 错误码
  • 【注意】pthread_cond_signal 有可能会出现多类线程任务,唤醒效果不达预期,导致整个进程中,没有线程工作,代码GG!
2.2.6 pthread_cond_broadcast 条件变量唤醒所有与之相关线程

函数文档

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
  • 函数功能:

    • 唤醒所有与当前条件变量相关的处于,条件变量阻塞状态情况下的线程程序。
  • 函数参数:

    • pthread_cond_t *restrict cond : 条件变量指针
  • 返回值:

    • 成功返回 0
    • 失败返回一个 非 0 错误码
2.2.7 补充函数

函数文档

#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                           pthread_mutex_t *restrict mutex,
                           const struct timespec *restrict abstime);
  • 函数功能

    • 【阻塞】等待一个条件变量
        1. 阻塞等待条件变量 cond 满足
        1. 【释放】已掌握的 mutex 对应互斥锁进行解锁操作,相当于 pthread_mutex_unlock(&mutex);
      • 以上两个步骤操作是【原子操作】,不可分隔,必须整体执行完毕
    • 当前线程进入一个【阻塞休眠状态】,有两种唤醒方式,第一种是当前线程被其他线程信号唤醒,或者当前线程指定休眠时间已到。
  • 函数参数

    • pthread_cond_t *restrict cond : 条件变量指针

    • pthread_mutex_t *restrict mutex : 互斥锁变量指针

    • const struct timespec *restrict abstime :

      • struct timespec {
            time_t tv_sec;      /* 秒 */
            long   tv_nsec;     /* 纳秒 */
        };
        
      • 指定当前线程休眠的时间,结构体内容包括 秒 和 纳秒
  • 返回值

    • 成功返回 0
    • 失败返回一个 非 0 错误码

3. 综合案例,消费者生产者

#define _POSIX_C_SOURCE 200809L // 指定 POSIX 版本
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

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

typedef struct goods
{
    char goods_name[64];
    float price;
    int count;
} Goods;

// 互斥锁变量
pthread_mutex_t mutex;

// 条件变量
pthread_cond_t cond;

void *consumer_threadA(void *arg);
void *consumer_threadB(void *arg);
void *supplier_thread(void *arg);

int main(int argc, char const *argv[])
{
    Goods goods = {"小米 Su7 Ultra", 620900, 10};

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);

    pthread_t c_id1 = 0;
    pthread_t c_id2 = 0;
    pthread_t s_id1 = 0;

    pthread_create(&c_id1, NULL, consumer_threadA, &goods);
    pthread_create(&c_id2, NULL, consumer_threadB, &goods);
    pthread_create(&s_id1, NULL, supplier_thread, &goods);

    pthread_join(c_id1, NULL);
    pthread_join(c_id2, NULL);
    pthread_join(s_id1, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    return 0;
}

void *consumer_threadA(void *arg)
{
    Goods *goods = (Goods *)arg;

    while (1)
    {
        sleep(1);

        pthread_mutex_lock(&mutex);

        if (goods->count)
        {
            // 进入买买买模式!
            printf("消费者 A 购买了 : %s, 价格为 : %f,喜笑颜开\n",
                   goods->goods_name, goods->price);

            goods->count -= 1;

            // 唤醒所有与之相关线程
            pthread_cond_broadcast(&cond);
        }
        else
        {
            printf("消费者 A 进入休眠模式\n");

            // 商品数量为 0 ,消费者进入休眠模式,同时开锁
            pthread_cond_wait(&cond, &mutex);
        }

        pthread_mutex_unlock(&mutex);
    }
}

void *consumer_threadB(void *arg)
{
    Goods *goods = (Goods *)arg;

    while (1)
    {
        sleep(1);
        pthread_mutex_lock(&mutex);

        if (goods->count)
        {
            // 进入买买买模式!
            printf("消费者 B 购买了 : %s, 价格为 : %f,喜笑颜开\n",
                   goods->goods_name, goods->price);

            goods->count -= 1;

            // 唤醒所有与之相关线程
            // 尝试 pthread_cond_signal(&cond);
            pthread_cond_broadcast(&cond);
        }
        else
        {
            printf("消费者 B 进入休眠模式\n");
            // 商品数量为 0 ,消费者进入休眠模式,同时开锁
            pthread_cond_wait(&cond, &mutex);
        }

        pthread_mutex_unlock(&mutex);
    }
}

void *supplier_thread(void *arg)
{
    Goods *goods = (Goods *)arg;

    int n = 0;

    while (1)
    {
        sleep(1);

        pthread_mutex_lock(&mutex);
        if (!goods->count)
        {
            // 进入生产模式
            if (n % 2)
            {
                strcpy(goods->goods_name, "小米 Su7 Ultra");
                goods->price = 620900;
            }
            else
            {
                strcpy(goods->goods_name, "极氪 009 光辉");
                goods->price = 789800;
            }

            n += 1;
            goods->count += 5;

            printf("生产者生产了 %s, 价格为 %f, 当前数量为 %d\n",
                   goods->goods_name, goods->price, goods->count);

            // 生产完毕,唤醒消费者
            pthread_cond_broadcast(&cond);
        }
        else
        {
            printf("生产者进入休眠模式\n");
            // 生产者进入休眠模式
            pthread_cond_wait(&cond, &mutex);
        }

        pthread_mutex_unlock(&mutex);
    }
}