线程同步—竞态条件和锁 1.竞态条件 线程同步是并发编程中的一个重要概念,它涉及到多个线程之间如何协调对共享资源的访问,以确保程序的正确性和效率。竞态条件和锁是线程同步中两个关键的概念,它们之间有着紧密的联系和区别。
1.1定义
**当多个线程并发访问和修改同一个共享资源(如全局变量)时,如果没有适当的同步 措施,就会遇到线程同步问题。这种情况下,程序最终的结果依赖于线程执行的具体时序, 导致了竞态条件。 **
**竞态条件(race condition)是一种特定的线程同步问题,指的是两个或者以上进 程或者线程并发执行时,其最终的结果依赖于进程或者线程执行的精确时序。它会导致程 序的行为和输出超出预期,因为共享资源的最终状态取决于线程执行的顺序和时机。为了 确保程序执行结果的正确性和预期一致,需要通过适当的线程同步机制来避免竞态条件。 **
1.2锁 锁是一种同步机制,用于在并发环境中对共享资源进行互斥访问,确保同一时间只有一个线程能够访问共享资源,从而避免竞态条件和数据不一致的问题。常见的锁包括互斥锁(Mutex)、读写锁(RWMutex)等。
1.2.1锁的作用
互斥访问 :通过锁机制,可以确保在任意时刻只有一个线程能够访问共享资源,从而避免多个线程同时修改资源导致的冲突和数据不一致。
保护临界区 :在并发编程中,将访问共享资源的代码段称为临界区。通过加锁,可以保护临界区内的代码不被多个线程同时执行,从而确保数据的一致性和完整性。
1.2.2锁的使用
加锁 :在访问共享资源之前,线程需要获取锁。如果锁已被其他线程持有,则当前线程需要等待直到锁被释放。
访问资源 :在成功获取锁之后,线程可以安全地访问共享资源。
释放锁 :在访问完共享资源后,线程需要释放锁,以便其他线程可以获取锁并访问资源。
1.3竞态条件和锁的关系 竞态条件是并发编程中需要避免的问题,而锁是解决竞态条件的一种有效手段。通过加锁机制,可以确保对共享资源的访问是有序的、互斥的,从而避免竞态条件的发生。然而,锁也会带来一定的性能开销,包括锁的获取和释放、线程的调度等。因此,在设计并发程序时,需要权衡竞态条件的风险和锁的开销,选择合适的同步机制来平衡正确性和性能。
2.竞态案例 下面的程序没有合理控制线程的并发访问,可能会引发竞态条件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #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方法
避免多线程写入一个地址 :其可以通过逻辑上组织业务逻辑实现。
给资源加锁 :使同一时间操作特定资源的线程只有一个。想解决竞争问题,我们需要互斥锁——mutex
3.2锁机制 **锁主要用于互斥,即在同一时间只允许一个执行单元(进程或线程)访问共享资源。 包括上面的互斥锁在内,常见的锁机制共有三种: **
互斥锁(Mutex) :保证同一时刻只有一个线程可以执行临界区的代码。
读写锁(Reader/Writer Locks) :允许多个读者同时读共享数据,但写者的访问 是互斥的。
自旋锁(Spinlocks) :在获取锁之前,线程在循环中忙等待,适用于锁持有时间 非常短的场景,一般是 Linux 内核使用。
4.互斥锁 4.1pthread_mutex_t 4.1.1定义 pthread_mutex_t 是一个定义在头文件中的联合体类型的别名, 其声明如下。
1 2 3 4 5 6 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流程
初始化(pthread_mutex_init) :创建互斥锁并初始化。
锁定(pthread_mutex_lock ):获取互斥锁。如果锁已经被其他线程持有,调用线程将阻塞。
尝试锁定(pthread_mutex_trylock) :尝试获取互斥锁。如果锁已被持有,立即返回而不是阻塞。
解锁(pthread_mutex_unlock) :释放互斥锁,使其可被其他线程获取。
销毁(pthread_mutex_destroy) :清理互斥锁资源。
4.4相关函数 4.4.1pthread_mutex_lock 1 2 3 4 5 6 7 8 #include <pthread.h> int pthread_mutex_lock (pthread_mutex_t *mutex) ;
** **该函数用于锁定指定的互斥锁。如果互斥锁已经被其他线程锁定,调用此函数的线程 将会被阻塞,直到互斥锁变为可用状态。这意味着如果另一个线程持有锁,当前线程将等 待直到锁被释放。
** ** 成功时返回 0;失败时返回错误码。
4.4.2pthread_mutex_trylock 1 2 3 4 5 6 7 8 int pthread_mutex_trylock (pthread_mutex_t *mutex) ;
** **该函数尝试锁定指定的互斥锁。与 pthread_mutex_lock 不同,如果互斥锁已经被其 他线程锁定,pthread_mutex_trylock 不会阻塞调用线程,而是立即返回一个错误码 (EBUSY)。
** **如果成功锁定互斥锁,则返回 0;如果互斥锁已被其他线程锁定,返回 EBUSY;其他 错误情况返回不同的错误码。
4.4.3pthread_mutex_unlock 1 2 3 4 5 6 7 int pthread_mutex_unlock (pthread_mutex_t *mutex) ;
** **该函数用于解锁指定的互斥锁。调用线程必须是当前持有互斥锁的线程;否则,解锁 操作可能会失败。
** **成功时返回 0;失败时返回错误码。
4.4.4pthread_mutex_init ** **pthread_mutex_init
是 POSIX 线程(也称为 pthreads)库中用于初始化互斥锁(mutex)的函数。互斥锁是用于同步线程的工具,它们允许多个线程以受控的方式访问共享资源,以避免数据竞争和其他并发问题。
1 2 3 #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 初始化互斥锁时,实际上是将互斥锁设置 为默认属性和未锁定状态。这种初始化方式适用于简单的同步问题,我们可以通过以下代 码初始化互斥锁。
1 static pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
4.6将 mutex 加入线程 4.6.1案例测试 ** **为了保证计算结果的正确性,很显然,我们应阻塞式获取互斥锁,应调用的是 pthread_mutex_lock 函数。共享变量修改完成后,应该释放锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #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线程间条件切换函数 如果需要两个线程协同工作,可以使用条件变量完成线程切换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include <pthread.h> int pthread_cond_wait (pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex) ;int pthread_cond_timedwait (pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrictabstime) ;int pthread_cond_signal (pthread_cond_t *cond) ;int pthread_cond_broadcast (pthread_cond_t *cond) ;
**使用条件变量时,通常涉及到一个或多个线程等待“条件变量”代表的条件成立, 而另外一些线程在条件成立时触发条件变量。 **
**条件变量的使用必须与互斥锁配合,以保证对共享资源的访问是互斥的。 **
条件变量提供了一种线程间的通信机制,允许线程以无竞争的方式等待特定条件 的发生。
4.7.3条件变量 pthread_cond_t ** **pthread_cond_t 是一个条件变量,它是线程间同步的另一种机制。与 pthread_mutex_t 相同,它也定义在头文件中,其声明如下。
1 2 3 4 5 6 typedef union { struct __pthread_cond_s __data; char __size[__SIZEOF_PTHREAD_COND_T]; __extension__ long long int __align; } pthread_cond_t ;
** **条件变量允许线程挂起执行并释放已持有的互斥锁,等待某个条件变为真。条件变量 总是需要与互斥锁一起使用,以避免出现竞态条件。
用途:
**允许线程等待特定条件的发生。当条件尚未满足时,线程通过条件变量等待,直 到其他线程修改条件并通知条件变量。 **
**通知等待中的线程条件已改变,允许它们重新评估条件。 **
操作:
初始化(pthread_cond_init) :创建并初始化条件变量。
等待(pthread_cond_wait) :在给定的互斥锁上等待条件变量。调用时,线程将 释放互斥锁并进入等待状态,直到被唤醒。
定时等待(pthread_cond_timedwait ):等待条件变量或直到超过指定的时间。
** **信号(pthread_cond_signal) :唤醒至少一个等待该条件变量的线程。
** **广播(pthread_cond_broadcast) :唤醒所有等待该条件变量的线程。
** **销毁(pthread_cond_destroy) :清理条件变量资源。
4.7.4初始化PTHREAD_COND_INITIALIZER 用法:
** PTHREAD_COND_INITIALIZER 是 POSIX 线程(Pthreads)库中定义的一个宏,用于 在声明时 静态初始化**条件变量(pthread_cond_t 类型的变量)。它提供了一种简单、便捷的方式来初始化条件变量,无需调用初始化函数 pthread_cond_init。
** **使用 PTHREAD_COND_INITIALIZER 可以让条件变量在程序启动时即处于可用状态, 这对于全局或静态分配的条件变量尤其有用。
1 static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
注意事项:
使用 PTHREAD_COND_INITIALIZER 静态初始化的条件变量通常不需要调用 pthread_cond_destroy 来销毁。但是,如果条件变量在程序执行期间被重新初始化(通过 pthread_cond_init),那么在不再需要时应使用 pthread_cond_destroy 进行清理。
** PTHREAD_COND_INITIALIZER 只适用于静态或全局变量的初始化。对于动态分 配的条件变量(例如,通过 malloc 分配的条件变量),应使用 pthread_cond_init 函数进行 初始化。**
** PTHREAD_COND_INITIALIZER 提供的是条件变量的默认属性。如果需要自定义 条件变量的属性(例如,改变其 pshared 属性以支持进程间同步),则需要使用 pthread_cond_init 和 pthread_condattr_t 类型的属性对象。**
4.7.5案例测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #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 1 2 3 4 5 6 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 1 2 3 4 5 6 7 8 9 10 11 12 13 int pthread_rwlock_init (pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr) ;
5.2.3pthread_rwlock_destroy 1 2 3 4 5 6 7 8 9 #include <pthread.h> int pthread_rwlock_destroy (pthread_rwlock_t *rwlock) ;
5.2.4pthread_rwlock_rdlock 1 2 3 4 5 6 7 8 int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock) ;
5.2.5 pthread_rwlock_wrlock 1 2 3 4 5 6 7 8 int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock) ;
5.2.6pthread_rwlock_unlock 1 2 3 4 5 6 7 int pthread_rwlock_unlock (pthread_rwlock_t *rwlock) ;
5.3.案例测试 5.3.1写操作不加锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #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 的值加一,但这不是个原子操作, 分成了两步:
先将其值加一,赋给临时变量 tmp,
然后将 tmp 的值赋给 shared_data。 在这两步之间睡眠一秒。
** **这样一来,两个写线程只有按照严格的先后顺序执行, shared_data 的值才会+2 变为 2,触发了竞态条件。 为了观察两次写操作之后的数据,我们在创建写线程之后睡眠两秒,确保大多数情况 下读操作读到的是两次写操作之后的数据。
5.3.2写操作添加读写锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #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 读写操作执行顺序随机 注意:线程的执行顺序是由操 作系统内核调度的,其运行规律并不简单地为“先创建先执行”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #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 1 2 3 4 5 typedef union { char __size[__SIZEOF_PTHREAD_RWLOCKATTR_T]; long int __align; } pthread_rwlockattr_t ;
2.pthread_rwlockattr_init 1 2 3 4 5 6 7 8 #include <pthread.h> int pthread_rwlockattr_init (pthread_rwlockattr_t *attr) ;
3.pthread_rwlockattr_destroy 1 2 3 4 5 6 7 8 #include <pthread.h> int pthread_rwlockattr_destroy (pthread_rwlockattr_t *attr) ;
4.pthread_rwlockattr_setkind_np 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <pthread.h> int pthread_rwlockattr_setkind_np (pthread_rwlockattr_t *attr, int pref) ;
5.4.4 写饥饿案例测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #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 ; }
多次运行后,我们发现,此时读操作总是连续执行的,且读操作休眠未结束时,写操作会被阻塞。与工作原理相符:
读操作可以并发执行,相互之间不必争抢锁,多个读操 作可以同时获得读锁;
只要有一个线程持有读写锁,写操作就会被阻塞。
我们在读操作 中加了 1s 休眠,只要有一个读线程获得锁,在 1s 内写操作是无法执行的,其它读操作就 可以有充足的时间执行,因此读操作就会连续发生,写操作必须等待所有读操作执行完毕 方可获得读写锁执行写操作。这就是使用读写锁时存在的潜在问题:写饥饿。
5.4.5写饥饿解决案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 #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 ; }
** **可以发现,此时的连续六次读操作间夹杂了写操作,不再连续,写操作不必等待所有 读操作完成才可以执行。不必长期等待,写饥饿问题已得到解决。