当前位置: 首页 > article >正文

Linux线程同步—竞态条件与互斥锁、读写锁(C语言)

线程同步—竞态条件和锁

1.竞态条件

线程同步是并发编程中的一个重要概念,它涉及到多个线程之间如何协调对共享资源的访问,以确保程序的正确性和效率。竞态条件和锁是线程同步中两个关键的概念,它们之间有着紧密的联系和区别。

1.1定义

  • 当多个线程并发访问和修改同一个共享资源(如全局变量)时,如果没有适当的同步 措施,就会遇到线程同步问题。这种情况下,程序最终的结果依赖于线程执行的具体时序, 导致了竞态条件。
  • 竞态条件(race condition)是一种特定的线程同步问题,指的是两个或者以上进 程或者线程并发执行时,其最终的结果依赖于进程或者线程执行的精确时序。它会导致程 序的行为和输出超出预期,因为共享资源的最终状态取决于线程执行的顺序和时机。为了 确保程序执行结果的正确性和预期一致,需要通过适当的线程同步机制来避免竞态条件。

1.2锁

锁是一种同步机制,用于在并发环境中对共享资源进行互斥访问,确保同一时间只有一个线程能够访问共享资源,从而避免竞态条件和数据不一致的问题。常见的锁包括互斥锁(Mutex)、读写锁(RWMutex)等。

1.2.1锁的作用
  1. 互斥访问:通过锁机制,可以确保在任意时刻只有一个线程能够访问共享资源,从而避免多个线程同时修改资源导致的冲突和数据不一致。
  2. 保护临界区:在并发编程中,将访问共享资源的代码段称为临界区。通过加锁,可以保护临界区内的代码不被多个线程同时执行,从而确保数据的一致性和完整性。
1.2.2锁的使用
  1. 加锁:在访问共享资源之前,线程需要获取锁。如果锁已被其他线程持有,则当前线程需要等待直到锁被释放。
  2. 访问资源:在成功获取锁之后,线程可以安全地访问共享资源。
  3. 释放锁:在访问完共享资源后,线程需要释放锁,以便其他线程可以获取锁并访问资源。

1.3竞态条件和锁的关系

竞态条件是并发编程中需要避免的问题,而锁是解决竞态条件的一种有效手段。通过加锁机制,可以确保对共享资源的访问是有序的、互斥的,从而避免竞态条件的发生。然而,锁也会带来一定的性能开销,包括锁的获取和释放、线程的调度等。因此,在设计并发程序时,需要权衡竞态条件的风险和锁的开销,选择合适的同步机制来平衡正确性和性能。

2.竞态案例

下面的程序没有合理控制线程的并发访问,可能会引发竞态条件。

//
// Created by root on 2024/9/19.
//
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define THREAD_COUNT 20000

void *add_thread(void *argv){
    int *num=(int *)argv;
    (*num)++;
    return (void *)0;
}

int main(int argc,char *argv[]){
    pthread_t pid[THREAD_COUNT];
    int num=0;
    for (size_t i = 0; i <THREAD_COUNT ; ++i) {
        pthread_create(pid+i,NULL,add_thread,&num);
    }

    for (int i = 0; i < THREAD_COUNT; ++i) {
        pthread_join(pid[i],NULL);
    }

    printf("%d\n",num);
    return 0;
}

可以看到 20000 个线程对 num 的累加结果是不确定的,没有达到我们的预期值 20000。 这是因为线程之间出现了竞争,不同线程对于 num 的累加操作可能重叠,这就会导致多次 累加操作可能只生效一次。

3.如何避免竞态条件

3.1方法

  1. 避免多线程写入一个地址:其可以通过逻辑上组织业务逻辑实现。
  2. 给资源加锁:使同一时间操作特定资源的线程只有一个。想解决竞争问题,我们需要互斥锁——mutex

3.2锁机制

锁主要用于互斥,即在同一时间只允许一个执行单元(进程或线程)访问共享资源。 包括上面的互斥锁在内,常见的锁机制共有三种:

  1. 互斥锁(Mutex):保证同一时刻只有一个线程可以执行临界区的代码。
  2. 读写锁(Reader/Writer Locks):允许多个读者同时读共享数据,但写者的访问 是互斥的。
  3. 自旋锁(Spinlocks):在获取锁之前,线程在循环中忙等待,适用于锁持有时间 非常短的场景,一般是 Linux 内核使用。

4.互斥锁

4.1pthread_mutex_t

4.1.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 用作线程之间的互斥锁。互斥锁是一种同步机制,用来控制对共享 资源的访问。在任何时刻,最多只能有一个线程持有特定的互斥锁。如果一个线程试图获 取一个已经被其他线程持有的锁,那么请求锁的线程将被阻塞,直到锁被释放

4.2Mutex的作用

  • 保护共享数据,避免同时被多个线程访问导致的数据不一致问题。
  • 实现线程间的同步,确保线程之间对共享资源的访问按照预定的顺序进行。

4.3流程

  1. 初始化(pthread_mutex_init):创建互斥锁并初始化。
  2. 锁定(pthread_mutex_lock):获取互斥锁。如果锁已经被其他线程持有,调用线程将阻塞。
  3. 尝试锁定(pthread_mutex_trylock):尝试获取互斥锁。如果锁已被持有,立即返回而不是阻塞。
  4. 解锁(pthread_mutex_unlock):释放互斥锁,使其可被其他线程获取。
  5. 销毁(pthread_mutex_destroy):清理互斥锁资源。

4.4相关函数

4.4.1pthread_mutex_lock
#include <pthread.h>
/**
* @brief 获取锁,如果此时锁被占则阻塞
* 
* @param mutex 锁
* @return int 获取锁结果
*/
int pthread_mutex_lock(pthread_mutex_t *mutex);

​ 该函数用于锁定指定的互斥锁。如果互斥锁已经被其他线程锁定,调用此函数的线程 将会被阻塞,直到互斥锁变为可用状态。这意味着如果另一个线程持有锁,当前线程将等 待直到锁被释放。

​ 成功时返回 0;失败时返回错误码。

4.4.2pthread_mutex_trylock
/**
* @brief 非阻塞式获取锁,如果锁此时被占则返回 EBUSY
* 
* @param mutex 锁
* @return int 获取锁结果
*/
int pthread_mutex_trylock(pthread_mutex_t *mutex);

​ 该函数尝试锁定指定的互斥锁。与 pthread_mutex_lock 不同,如果互斥锁已经被其 他线程锁定,pthread_mutex_trylock 不会阻塞调用线程,而是立即返回一个错误码 (EBUSY)。

​ 如果成功锁定互斥锁,则返回 0;如果互斥锁已被其他线程锁定,返回 EBUSY;其他 错误情况返回不同的错误码。

4.4.3pthread_mutex_unlock
/**
* @brief 释放锁
* 
* @param mutex 锁
* @return int 释放锁结果
*/
int pthread_mutex_unlock(pthread_mutex_t *mutex);

​ 该函数用于解锁指定的互斥锁。调用线程必须是当前持有互斥锁的线程;否则,解锁 操作可能会失败。

​ 成功时返回 0;失败时返回错误码。

4.4.4pthread_mutex_init

pthread_mutex_init 是 POSIX 线程(也称为 pthreads)库中用于初始化互斥锁(mutex)的函数。互斥锁是用于同步线程的工具,它们允许多个线程以受控的方式访问共享资源,以避免数据竞争和其他并发问题。

#include <pthread.h>  
  
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • mutex 指向要初始化的互斥锁对象的指针。这是一个类型为 pthread_mutex_t 的变量,通常在你定义它的时候就被声明为全局或静态变量,以便多个线程可以访问它。

  • attr 是一个指向 pthread_mutexattr_t 类型的指针,用于指定互斥锁的属性。大多数情况下,如果你不需要特殊的互斥锁属性,可以将此参数设置为 NULL,此时互斥锁将使用默认属性进行初始化。

  • 成功时,pthread_mutex_init 返回 0

  • 出错时,返回错误码。这些错误码可以包括但不限于 EAGAIN(资源暂时不可用,但这不是 pthread_mutex_init 的典型错误)、EINVAL(参数无效,例如 mutex 是一个无效的地址),或 ENOMEM(内存不足,无法初始化互斥锁)。

4.5初始化互斥锁

​ PTHREAD_MUTEX_INITIALIZER 是 POSIX 线程(Pthreads)库中定义的一个宏,用于静态初始化互斥锁(mutex)。这个宏为互斥锁提供了一个初始状态,使其准备好被锁 定和解锁,而不需要在程序运行时显式调用初始化函数。

​ 当我们使用 PTHREAD_MUTEX_INITIALIZER 初始化互斥锁时,实际上是将互斥锁设置 为默认属性和未锁定状态。这种初始化方式适用于简单的同步问题,我们可以通过以下代 码初始化互斥锁。

static pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

4.6将 mutex 加入线程

4.6.1案例测试

​ 为了保证计算结果的正确性,很显然,我们应阻塞式获取互斥锁,应调用的是 pthread_mutex_lock 函数。共享变量修改完成后,应该释放锁。

//
// Created by root on 2024/9/19.
//
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define THREAD_COUNT 20000

//初始化互斥锁
static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

void *add_thread(void *argv){
    int *num=(int *)argv;
    //加锁,同一时间只有一个线程可以访问
    pthread_mutex_lock(&mutex);
    (*num)++;
    //解锁
    pthread_mutex_unlock(&mutex);
    return (void *)0;
}

int main(int argc,char *argv[]){
    pthread_t pid[THREAD_COUNT];
    int num=0;
    for (size_t i = 0; i <THREAD_COUNT ; ++i) {
        pthread_create(pid+i,NULL,add_thread,&num);
    }

    for (int i = 0; i < THREAD_COUNT; ++i) {
        pthread_join(pid[i],NULL);
    }

    printf("%d\n",num);
    return 0;
}
4.6.2注意

​ 上述代码中,互斥锁 counter_mutex 并未被显式销毁,但这通常不会引起资源泄露问题。上述程序在所有线程执行完毕后直接结束,进程结束时,操作系统会回收该进程的所有资源,包括内存、打开的文件描述符和互斥锁等。因此即便没有显式销毁互斥锁也不 会有问题。

​ 在某些情况下,确实需要显式销毁互斥锁资源。如果互斥锁是动态分配的(使用 pthread_mutex_init 函数初始化),或者互斥锁会被跨多个函数或文件使用,不再需要时必须显式销毁它。但对于静态初始化,并且在程序结束时不再被使用的互斥锁(上述程 序中的 counter_mutex),显式销毁不是必需的。

4.7条件变量

4.7.1restrict关键字

​ restrict 是一个 C99 标准引入的关键字,用于修饰指针,它的作用是告诉编译器, 被修饰的指针是编译器所知的唯一一个可以在其作用域内用来访问指针所指向的对象的方 法。这样一来,编译器可以放心地执行代码优化,因为不存在其他的别名(即其他指向同 一内存区域的指针)会影响到这块内存的状态。

​ restrict 声明了一种约定,主要目的是允许编译器在生成代码时做出优化假设,而 不是在程序的不同部分间强制执行内存访问的规则。程序员需要确保遵守 restrict 的约 定,编译器则依赖这个约定来进行优化。如果 restrict 约定被违反,可能导致未定义行 为。

​ 函数参数使用 restrict 修饰,相当于约定:函数执行期间,该参数指向的内存区域 不会被其它指针修改。

4.7.2线程间条件切换函数

如果需要两个线程协同工作,可以使用条件变量完成线程切换。

#include <pthread.h>
/**
* @brief 调用该方法的线程必须持有 mutex 锁。调用该方法的线程会阻塞并临时释放
mutex 锁,并等待其他线程调用 pthread_cond_signal 或 pthread_cond_broadcast
唤醒。被唤醒后该线程会尝试重新获取 mutex 锁。
* 
* @param cond 指向条件变量的指针。条件变量用于等待某个条件的发生。通过某一
cond 等待的线程需要通过同一 cond 的 signal 唤醒
* @param mutex 与条件变量配合使用的互斥锁的指针。在调用 pthread_cond_wait
之前,线程必须已经获得了这个互斥锁。
* @return int 成功时返回 0;失败时返回错误码,而非-1。错误码可能包括 EINVAL、
EPERM 等,具体取决于错误的性质。
*/
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t
*restrict mutex);
/**
* @brief 同 pthread_cond_wait 相似,但是它添加了超时机制。如果在指定的
abstime 时间内条件变量没有被触发,函数将返回一个超时错误(ETIMEDOUT)。
* 
* @param cond 指向条件变量的指针
* @param mutex 与条件变量配合使用的互斥锁的指针
* @param abstime 指向 timespec 结构的指针,表示等待条件变量的绝对超时时间。
timespec 结构包含秒和纳秒两部分,指定了从某一固定点(如 UNIX 纪元,1970 年 1 月
1 日)开始的时间。
* @return int 成功时返回 0;如果超时则返回 ETIMEDOUT;其他错误情况返回相应的
错误码。
*/
int pthread_cond_timedwait(pthread_cond_t *restrict cond, 
pthread_mutex_t *restrict mutex, const struct timespec *restrict
abstime);
/**
* @brief 唤醒因 cond 而阻塞的线程,如果有多个线程因为 cond 阻塞,那么随机唤醒
一个。如果没有线程在等待,这个函数什么也不做。
* 
* @param cond 指向条件变量的指针
* @return int 成功时返回 0;失败时返回错误码
*/
int pthread_cond_signal(pthread_cond_t *cond);
/**
* @brief 唤醒所有正在等待条件变量 cond 的线程。如果没有线程在等待,这个函数什
么也不做。
* 
* @param cond 指向条件变量的指针。
* @return int 成功时返回 0;失败时返回错误码。
*/
int pthread_cond_broadcast(pthread_cond_t *cond);
  1. 使用条件变量时,通常涉及到一个或多个线程等待“条件变量”代表的条件成立, 而另外一些线程在条件成立时触发条件变量。
  2. 条件变量的使用必须与互斥锁配合,以保证对共享资源的访问是互斥的。
  3. 条件变量提供了一种线程间的通信机制,允许线程以无竞争的方式等待特定条件 的发生。
4.7.3条件变量 pthread_cond_t

​ pthread_cond_t 是一个条件变量,它是线程间同步的另一种机制。与 pthread_mutex_t 相同,它也定义在头文件中,其声明如下。

typedef union
{ 
 struct __pthread_cond_s __data; 
 char __size[__SIZEOF_PTHREAD_COND_T];
 __extension__ long long int __align; 
} pthread_cond_t; 

​ 条件变量允许线程挂起执行并释放已持有的互斥锁,等待某个条件变为真。条件变量 总是需要与互斥锁一起使用,以避免出现竞态条件。

用途:

  • 允许线程等待特定条件的发生。当条件尚未满足时,线程通过条件变量等待,直 到其他线程修改条件并通知条件变量。
  • 通知等待中的线程条件已改变,允许它们重新评估条件。

操作:

  1. 初始化(pthread_cond_init):创建并初始化条件变量。
  2. 等待(pthread_cond_wait):在给定的互斥锁上等待条件变量。调用时,线程将 释放互斥锁并进入等待状态,直到被唤醒。
  3. 定时等待(pthread_cond_timedwait):等待条件变量或直到超过指定的时间。
  4. 信号(pthread_cond_signal):唤醒至少一个等待该条件变量的线程。
  5. 广播(pthread_cond_broadcast):唤醒所有等待该条件变量的线程。
  6. 销毁(pthread_cond_destroy):清理条件变量资源。
4.7.4初始化PTHREAD_COND_INITIALIZER

用法:

​ PTHREAD_COND_INITIALIZER 是 POSIX 线程(Pthreads)库中定义的一个宏,用于 在声明时静态初始化条件变量(pthread_cond_t 类型的变量)。它提供了一种简单、便捷的方式来初始化条件变量,无需调用初始化函数 pthread_cond_init。

​ 使用 PTHREAD_COND_INITIALIZER 可以让条件变量在程序启动时即处于可用状态, 这对于全局或静态分配的条件变量尤其有用。

static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

注意事项:

  1. 使用 PTHREAD_COND_INITIALIZER 静态初始化的条件变量通常不需要调用 pthread_cond_destroy 来销毁。但是,如果条件变量在程序执行期间被重新初始化(通过 pthread_cond_init),那么在不再需要时应使用 pthread_cond_destroy 进行清理。
  2. PTHREAD_COND_INITIALIZER 只适用于静态或全局变量的初始化。对于动态分 配的条件变量(例如,通过 malloc 分配的条件变量),应使用 pthread_cond_init 函数进行 初始化。
  3. PTHREAD_COND_INITIALIZER 提供的是条件变量的默认属性。如果需要自定义 条件变量的属性(例如,改变其 pshared 属性以支持进程间同步),则需要使用 pthread_cond_init 和 pthread_condattr_t 类型的属性对象。
4.7.5案例测试
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count = 0;
// 初始化锁
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 初始化条件变量
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 生产者线程
void *producer(void *arg) 
{ 
 int item = 1; 
 while (1) 
 { 
 // 获取互斥锁 拿不到等
 pthread_mutex_lock(&mutex);
 // 如果缓冲区满,等消费者读取
 while (count == BUFFER_SIZE)
 { 
 // 暂停线程 等待唤醒
 pthread_cond_wait(&cond, &mutex);
 } 
 // 能到这里说明缓冲区不满了 可以写一个
 buffer[count++] = item++; 
 printf("白月光发送一个幸运数字%d\n", buffer[count - 1]);
 // 通知消费者可以消费数据了
 // 唤醒消费者 同时解锁
 pthread_cond_signal(&cond);
 pthread_mutex_unlock(&mutex);
 } 
}
void *consumer(void *arg) 
{ 
 while (1) 
 { 
 // 获取互斥锁 拿不到等
 pthread_mutex_lock(&mutex);
 // 如果缓冲区为空,则等待生产者生产数据
 while (count == 0) 
 { 
 // 暂停线程 等待唤醒
 pthread_cond_wait(&cond, &mutex);
 } 
 printf("我收到了幸运数字 %d\n", buffer[--count]);
 // 通知生产者可以发送数据了
 // 唤醒生产者 同时解锁
 pthread_cond_signal(&cond);
 pthread_mutex_unlock(&mutex);
 } 
}
int main()
{ 
 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);
 return 0; 
} 

可以看到 producer 线程生产数据,consumer 线程消费数据,二者交替工作。

5.读写锁

读写锁(Read-Write Lock)在多线程编程中是一种用于控制对共享资源访问的同步机制。它的主要特点是允许多个读操作并发进行,同时又能保证写操作的独占性。以下是关于读写锁的详细解析:

5.1基本概念

5.1.1定义
  • 读锁(Read Lock):允许多个线程同时持有读锁,进行读取操作。如果当前没有线程持有写锁,则读者可以获取读锁。
  • 写锁(Write Lock):写锁是独占的,在同一时间只能有一个线程持有写锁。写锁禁止任何其它读者或写者访问共享资源。
5.1.2工作原理
  • 读共享:允许多个线程同时持有读锁,进行读取操作,提高了程序的并发性能。
  • 写独占:写操作必须独占,当一个线程获得写锁后,其它线程无法获得读锁或写锁,保证了写操作的数据一致性。
  • 读写互斥:当有线程持有写锁或请求写锁时,其它线程不能获取读锁,以避免数据冲突。
  • 防止写饥饿:大多数实现通过适当的锁调度策略,确保写线程不会因为连续的读操作而永久等待。
5.1.3优点
  • 提高并发性:读写锁允许多个读线程同时访问数据,从而提高了并发访问的吞吐量。
  • 数据保护:确保在写线程对数据进行修改时,能够排他性地访问数据,避免数据不一致的问题。
  • 适用场景广泛:特别适用于读操作远多于写操作的场景,如缓存系统、数据库读取优化等。
5.1.4注意事项
  • 死锁和优先级倒置:在使用读写锁时,要小心死锁和优先级倒置等问题,尤其是在涉及多个锁的复杂操作中。
  • 锁泄露:必须确保每一个锁定操作都有对应的解锁操作,否则可能会引起锁泄露,导致程序死锁或降低并发性能。
5.1.5实际应用
  • 缓存系统:允许多个线程同时进行读取操作,提高缓存的读取性能,同时确保只有一个线程可以进行写入操作,以维护缓存的一致性。
  • 数据库访问:控制多线程对数据库的访问,读线程可以获取读锁来进行读取操作,而写线程需要获取写锁来进行写入操作。
  • 文件系统操作:确保多个线程可以同时进行读取操作,提高文件读取的并发性能,同时保证只有一个线程可以进行写入操作,以确保文件的一致性。

5.2相关调用

5.2.1pthread_rwlock_t
typedef union
{ 
 struct __pthread_rwlock_arch_t __data; 
 char __size[__SIZEOF_PTHREAD_RWLOCK_T];
 long int __align; 
} pthread_rwlock_t;
5.2.2pthread_rwlock_init
/**
* @brief 为 rwlock 指向的读写锁分配所有需要的资源,并将锁初始化为未锁定状态。
读写锁的属性由 attr 参数指定,如果 attr 为 NULL,则使用默认属性。当锁的属性为默
认时,可以通过宏 PTHREAD_RWLOCK_INITIALIZER 初始化,即
* pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; 效果和调用当前方
法并为 attr 传入 NULL 是一样的
* 
* @param rwlock 读写锁
* @param attr 读写锁的属性
* @return int 成功则返回 0,否则返回错误码
*/
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const
pthread_rwlockattr_t *restrict attr);
5.2.3pthread_rwlock_destroy
#include <pthread.h>
/**
* @brief 销毁 rwlock 指向的读写锁对象,并释放它使用的所有资源。当任何线程持有
锁的时候销毁锁,或尝试销毁一个未初始化的锁,结果是未定义的。
* 
* @param rwlock
* @return int 
*/
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
5.2.4pthread_rwlock_rdlock
/**
* @brief 应用一个读锁到 rwlock 指向的读写锁上,并使调用线程获得读锁。如果写线
程持有锁,调用线程无法获得读锁,它会阻塞直至获得锁。
* 
* @param rwlock 读写锁
* @return int 成功返回 0,失败返回错误码
*/
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
5.2.5 pthread_rwlock_wrlock
/**
* @brief 应用一个写锁到 rwlock 指向的读写锁上,并使调用线程获得写锁。只要任意
线程持有读写锁,则调用线程无法获得写锁,它将阻塞直至获得写锁。
* 
* @param rwlock 读写锁
* @return int 成功返回 0,失败返回错误码
*/
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
5.2.6pthread_rwlock_unlock
/**
* @brief 释放调用线程锁持有的 rwlock 指向的读写锁。
* 
* @param rwlock 读写锁
* @return int 成功返回 0.失败返回错误码
*/
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

5.3.案例测试

5.3.1写操作不加锁
//
// Created by root on 2024/9/20.
//
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_rwlock_t  rwlock;
int shared_data=0;

//回调函数
//写函数
void *lock_write(void *argv){
    int tmp=shared_data+1;
    sleep(1);
    shared_data=tmp;
    printf("当前是%s,shared_data为%d\n",(char *)argv,shared_data);
    return (void *)0;
}

//读函数
void *read_write(void *argv){
    pthread_rwlock_rdlock(&rwlock);
    printf("当前是%s,shared_data为%d\n",(char *)argv,shared_data);
    pthread_rwlock_unlock(&rwlock);
    return (void *)0;
}

int main(int argc,char *argv[]){
    //动态初始化读写锁
    pthread_rwlock_init(&rwlock,NULL);

    //创建线程
    pthread_t write_tid1,write_tid2,read_tid1,read_tid2,read_tid3;
    //创建写线程
    pthread_create(&write_tid1,NULL,lock_write,(void*)"write1");
    pthread_create(&write_tid2,NULL,lock_write,(void*)"write2");
    sleep(3);
    //创建读线程
    pthread_create(&read_tid1,NULL,read_write,(void *)"read1");
    pthread_create(&read_tid2,NULL,read_write,(void *)"read2");
    pthread_create(&read_tid3,NULL,read_write,(void *)"read3");
    //主线等待子线程结束
    pthread_join(write_tid1,NULL);
    pthread_join(write_tid2,NULL);
    pthread_join(read_tid1,NULL);
    pthread_join(read_tid2,NULL);
    pthread_join(read_tid3,NULL);

    //动态调用锁要手动销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

​ 我们在每个写操作中都将共享变量 shared_data 的值加一,但这不是个原子操作, 分成了两步:

  1. 先将其值加一,赋给临时变量 tmp,
  2. 然后将 tmp 的值赋给 shared_data。 在这两步之间睡眠一秒。

​ 这样一来,两个写线程只有按照严格的先后顺序执行, shared_data 的值才会+2 变为 2,触发了竞态条件。 为了观察两次写操作之后的数据,我们在创建写线程之后睡眠两秒,确保大多数情况 下读操作读到的是两次写操作之后的数据。

5.3.2写操作添加读写锁
//
// Created by root on 2024/9/20.
//
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_rwlock_t  rwlock;
int shared_data=0;

//回调函数
//写函数
void *lock_write(void *argv){
    //加写锁
    pthread_rwlock_wrlock(&rwlock);
    int tmp=shared_data+1;
    sleep(1);
    shared_data=tmp;
    printf("当前是%s,shared_data为%d\n",(char *)argv,shared_data);
    //解锁
    pthread_rwlock_unlock(&rwlock);
    return (void *)0;
}

//读函数
void *read_write(void *argv){
    pthread_rwlock_rdlock(&rwlock);
    printf("当前是%s,shared_data为%d\n",(char *)argv,shared_data);
    pthread_rwlock_unlock(&rwlock);
    return (void *)0;
}

int main(int argc,char *argv[]){
    //动态初始化读写锁
    pthread_rwlock_init(&rwlock,NULL);

    //创建线程
    pthread_t write_tid1,write_tid2,read_tid1,read_tid2,read_tid3;
    //创建写线程
    pthread_create(&write_tid1,NULL,lock_write,(void*)"write1");
    pthread_create(&write_tid2,NULL,lock_write,(void*)"write2");
    sleep(3);
    //创建读线程
    pthread_create(&read_tid1,NULL,read_write,(void *)"read1");
    pthread_create(&read_tid2,NULL,read_write,(void *)"read2");
    pthread_create(&read_tid3,NULL,read_write,(void *)"read3");
    //主线等待子线程结束
    pthread_join(write_tid1,NULL);
    pthread_join(write_tid2,NULL);
    pthread_join(read_tid1,NULL);
    pthread_join(read_tid2,NULL);
    pthread_join(read_tid3,NULL);

    //动态调用锁要手动销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    return 0;
}
5.3.3 读写操作执行顺序随机

注意:线程的执行顺序是由操 作系统内核调度的,其运行规律并不简单地为“先创建先执行”。

//
// Created by root on 2024/9/20.
//
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_rwlock_t  rwlock;
int shared_data=0;

//回调函数
//写函数
void *lock_write(void *argv){
    //加写锁
    printf("%s加写锁\n",(char *)argv);
    pthread_rwlock_wrlock(&rwlock);
    int tmp=shared_data+1;
    shared_data=tmp;
    printf("当前是%s,shared_data为%d\n",(char *)argv,shared_data);
    //解锁
    printf("%s解写锁\n",(char *)argv);
    pthread_rwlock_unlock(&rwlock);
    return (void *)0;
}

//读函数
void *read_write(void *argv){
    printf("%s加读锁\n",(char *)argv);
    pthread_rwlock_rdlock(&rwlock);
    printf("当前是%s,shared_data为%d\n",(char *)argv,shared_data);
    printf("%s解读锁\n",(char *)argv);
    pthread_rwlock_unlock(&rwlock);
    return (void *)0;
}

int main(int argc,char *argv[]){
    //动态初始化读写锁
    pthread_rwlock_init(&rwlock,NULL);

    //创建线程
    pthread_t write_tid1,write_tid2,read_tid1,read_tid2,read_tid3;
    //创建写线程
    pthread_create(&write_tid1,NULL,lock_write,(void*)"write1");
    //创建读线程
    pthread_create(&read_tid1,NULL,read_write,(void *)"read1");
    pthread_create(&read_tid2,NULL,read_write,(void *)"read2");
    //在读线程中加入写线程
    pthread_create(&write_tid2,NULL,lock_write,(void*)"write2");
    pthread_create(&read_tid3,NULL,read_write,(void *)"read3");
    //主线等待子线程结束
    pthread_join(write_tid1,NULL);
    pthread_join(write_tid2,NULL);
    pthread_join(read_tid1,NULL);
    pthread_join(read_tid2,NULL);
    pthread_join(read_tid3,NULL);

    //动态调用锁要手动销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

5.4写饥饿机制

5.4.1问题描述

​ 读写锁的写饥饿问题(Writer Starvation)是指在使用读写锁时,写线程可能无限 期地等待获取写锁,因为读线程持续地获取读锁而不断地推迟写线程的执行。这种情况通 常在读操作远多于写操作时出现。

5.4.2解决方案

Linux 提供了可以修改的属性 pthread_rwlockattr_t默认情况下,属性中指定的 策略为**“读优先”,当写操作阻塞时,读线程依然可以获得读锁,从而在读操作并发较高 时导致写饥饿问题。我们可以尝试将策略更改为“写优先”**,当写操作阻塞时,读线程无 法获取锁,避免了写线程持有锁的时间持续延长,使得写线程获取锁的等待时间显著降低, 从而避免写饥饿问题。

5.4.3相关调用
1.pthread_rwlockattr_t
typedef union
{ 
 char __size[__SIZEOF_PTHREAD_RWLOCKATTR_T];
 long int __align;
} pthread_rwlockattr_t; 
2.pthread_rwlockattr_init
#include <pthread.h>
/**
* @brief 用所有属性的默认值初始化 attr 指向的属性对象
* 
* @param attr 读写锁属性对象指针
* @return int 成功返回 0,失败返回错误码
*/
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
3.pthread_rwlockattr_destroy
#include <pthread.h>
/**
* @brief 销毁读写锁属性对象
* 
* @param attr 读写锁属性对象指针
* @return int 成功返回 0,失败返回错误码
*/
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
4.pthread_rwlockattr_setkind_np
#include <pthread.h>
/**
* @brief 将 attr 指向的属性对象中的"锁类型"属性设置为 pref 规定的值
* 
* @param attr 读写锁属性对象指针
* @param pref 希望设置的锁类型,可以被设置为以下三种取值的其中一种
* PTHREAD_RWLOCK_PREFER_READER_NP: 默认值,读线程拥有更高优先级。当存在阻
塞的写线程时,读线程仍然可以获得读写锁。只要不断有新的读线程,写线程将一直保持"
饥饿"。
* PTHREAD_RWLOCK_PREFER_WRITER_NP: 写线程拥有更高优先级。这一选项被 glibc
忽略。
* PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP: 写线程拥有更高优先级,在
当前系统环境下,它是有效的,将锁类型设置为该值以避免写饥饿。
* @return int 成功返回 0,失败返回非零的错误码
*/
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);

5.4.4 写饥饿案例测试

//
// Created by root on 2024/9/20.
//
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_rwlock_t  rwlock;
int shared_data=0;

//回调函数
//写函数
void *lock_write(void *argv){
    //加写锁
    printf("%s加写锁\n",(char *)argv);
    pthread_rwlock_wrlock(&rwlock);
    int tmp=shared_data+1;
    shared_data=tmp;
    printf("当前是%s,shared_data为%d\n",(char *)argv,shared_data);
    //解锁
    printf("%s解写锁\n",(char *)argv);
    pthread_rwlock_unlock(&rwlock);
    return (void *)0;
}

//读函数
void *read_write(void *argv){
    printf("%s加读锁\n",(char *)argv);
    pthread_rwlock_rdlock(&rwlock);
    printf("当前是%s,shared_data为%d\n",(char *)argv,shared_data);
    printf("%s解读锁\n",(char *)argv);
    sleep(1);
    pthread_rwlock_unlock(&rwlock);
    return (void *)0;
}

int main(int argc,char *argv[]){
    //动态初始化读写锁
    pthread_rwlock_init(&rwlock,NULL);

    //创建线程
    pthread_t write_tid1,write_tid2,read_tid1,read_tid2,read_tid3;
    //创建写线程
    pthread_create(&write_tid1,NULL,lock_write,(void*)"write1");
    //创建读线程
    pthread_create(&read_tid1,NULL,read_write,(void *)"read1");
    pthread_create(&read_tid2,NULL,read_write,(void *)"read2");
    //在读线程中加入写线程
    pthread_create(&write_tid2,NULL,lock_write,(void*)"write2");
    pthread_create(&read_tid3,NULL,read_write,(void *)"read3");
    //主线等待子线程结束
    pthread_join(write_tid1,NULL);
    pthread_join(write_tid2,NULL);
    pthread_join(read_tid1,NULL);
    pthread_join(read_tid2,NULL);
    pthread_join(read_tid3,NULL);

    //动态调用锁要手动销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

多次运行后,我们发现,此时读操作总是连续执行的,且读操作休眠未结束时,写操作会被阻塞。与工作原理相符:

  1. 读操作可以并发执行,相互之间不必争抢锁,多个读操 作可以同时获得读锁;
  2. 只要有一个线程持有读写锁,写操作就会被阻塞。

我们在读操作 中加了 1s 休眠,只要有一个读线程获得锁,在 1s 内写操作是无法执行的,其它读操作就 可以有充足的时间执行,因此读操作就会连续发生,写操作必须等待所有读操作执行完毕 方可获得读写锁执行写操作。这就是使用读写锁时存在的潜在问题:写饥饿。

5.4.5写饥饿解决案例

//
// Created by root on 2024/9/20.
//
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_rwlock_t  rwlock;
int shared_data=0;

//回调函数
//写函数
void *lock_write(void *argv){
    //加写锁
    printf("%s加写锁\n",(char *)argv);
    pthread_rwlock_wrlock(&rwlock);
    int tmp=shared_data+1;
    shared_data=tmp;
    printf("当前是%s,shared_data为%d\n",(char *)argv,shared_data);
    //解锁
    printf("%s解写锁\n",(char *)argv);
    pthread_rwlock_unlock(&rwlock);
    return (void *)0;
}

//读函数
void *read_write(void *argv){
    printf("%s加读锁\n",(char *)argv);
    pthread_rwlock_rdlock(&rwlock);
    printf("当前是%s,shared_data为%d\n",(char *)argv,shared_data);
    printf("%s解读锁\n",(char *)argv);
    sleep(1);
    pthread_rwlock_unlock(&rwlock);
    return (void *)0;
}

int main(int argc,char *argv[]){
    //初始化读写锁属性对象
    pthread_rwlockattr_t attr;
    pthread_rwlockattr_init(&attr);
    //设置属性为写优先
    pthread_rwlockattr_setkind_np(&attr,PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);

    //动态初始化读写锁
    pthread_rwlock_init(&rwlock,&attr);

    //创建线程
    pthread_t write_tid1,write_tid2,read_tid1,read_tid2,read_tid3;
    //创建写线程
    pthread_create(&write_tid1,NULL,lock_write,(void*)"write1");
    //创建读线程
    pthread_create(&read_tid1,NULL,read_write,(void *)"read1");
    pthread_create(&read_tid2,NULL,read_write,(void *)"read2");
    //在读线程中加入写线程
    pthread_create(&write_tid2,NULL,lock_write,(void*)"write2");
    pthread_create(&read_tid3,NULL,read_write,(void *)"read3");
    //主线等待子线程结束
    pthread_join(write_tid1,NULL);
    pthread_join(write_tid2,NULL);
    pthread_join(read_tid1,NULL);
    pthread_join(read_tid2,NULL);
    pthread_join(read_tid3,NULL);

    //动态调用锁要手动销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

​ 可以发现,此时的连续六次读操作间夹杂了写操作,不再连续,写操作不必等待所有 读操作完成才可以执行。不必长期等待,写饥饿问题已得到解决。


http://www.kler.cn/a/320574.html

相关文章:

  • WPF Gif图谱 如果隐藏的话会存在BUG
  • 深挖C++赋值
  • 使用 Python 和 Py2Neo 构建 Neo4j 管理脚本
  • Restful API接⼝简介及为什么要进⾏接⼝压测
  • 前端开发设计模式——责任链模式
  • STM32单片机设计防儿童人员误锁/滞留车内警报系统
  • <Java>String类型变量的使用
  • 基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
  • 【c语言数据结构】栈的详解! 超级详细!(模拟实现,OJ练习题)
  • kubernetes K8S 挂载分布式存储 ceph
  • 算法基础8-双链表
  • Xcode16 iOS18 编译问题适配
  • 微信小程序教程:如何在个人中心实现头像贴纸功能
  • python爬虫解析工具BeautifulSoup(bs4)和CSS选择器——处理HTML和XML数据(7)
  • Windows系统修改Tomcat虚拟机内存参数
  • 《CUDA编程》3.简单CUDA程序的基本框架
  • 计算机毕业设计python+spark知识图谱房价预测系统 房源推荐系统 房源数据分析 房源可视化 房源大数据大屏 大数据毕业设计 机器学习
  • RuoYi-App根据不同角色权限实现功能按钮显隐
  • OpenHarmony(鸿蒙南向)——平台驱动指南【I2C】
  • 简易STL实现 | 红黑树的实现
  • SpringCloud-07 GateWay01 网关技术
  • 使用Okhttp-服务器不支持缓存的解决办法
  • C++之Person类
  • JavaScript中的无穷大
  • 华为静态路由(route-static)
  • 【Unity navigation面板】