互斥锁或自旋锁要么是加锁状态、要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁有 3 种状态:读模式下的加锁状态(以下简称读加锁状态)、写模式下的加锁状态(以下简称写加锁状态)和不加锁状态(见),一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。因此可知,读写锁比互斥锁具有更高的并行性!
读写锁有如下两个规则:
- 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞。
- 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放锁为止。
虽然各操作系统对读写锁的实现各不相同,但当读写锁处于读模式加锁状态,而这时有一个线程试图以写模式获取锁时,该线程会被阻塞;而如果另一线程以读模式获取锁,则会成功获取到锁,对共享资源进行读操作。
所以,读写锁非常适合于对共享数据读的次数远大于写的次数的情况。当读写锁处于写模式加锁状态时,它所保护的数据可以被安全的修改,因为一次只有一个线程可以在写模式下拥有这个锁;当读写锁处于读模式加锁状态时,它所保护的数据就可以被多个获取读模式锁的线程读取。所以在应用程序当中,使用读写锁实现线程同步,当线程需要对共享数据进行读操作时,需要先获取读模式锁(对读模式锁进行加锁), 当读取操作完成之后再释放读模式锁(对读模式锁进行解锁);当线程需要对共享数据进行写操作时,需要先获取到写模式锁,当写操作完成之后再释放写模式锁。
读写锁也叫做共享互斥锁。当读写锁是读模式锁住时,就可以说成是共享模式锁住。当它是写模式锁住时,就可以说成是互斥模式锁住。
读写锁初始化
与互斥锁、自旋锁类似,在使用读写锁之前也必须对读写锁进行初始化操作,读写锁使用 pthread_rwlock_t 数据类型表示,读写锁的初始化可以使用宏 PTHREAD_RWLOCK_INITIALIZER 或者函数 pthread_rwlock_init(),其初始化方式与互斥锁相同,譬如使用宏 PTHREAD_RWLOCK _INITIALIZER 进行初始化必须在定义读写锁时就对其进行初始化:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
对于其它方式可以使用 pthread_rwlock_init()函数对其进行初始化,当读写锁不再使用时,需要调用 pthread_rwlock_destroy()函数将其销毁,其函数原型如下所示:
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
使用这两个函数同样需要包含头文件,调用成功返回 0,失败将返回一个非 0 值的错误码。
参数 rwlock 指向需要进行初始化或销毁的读写锁对象。对于 pthread_rwlock_init()函数,参数 attr 是一 个 pthread_rwlockattr_t *类型指针,指向 pthread_rwlockattr_t 对象。pthread_ rwlockattr_t 数据类型定义了读写锁的属性,若将参数 attr 设置为 NULL,则表示将读写锁的属性设置为默认值,在这种情况下其实就等价于 PTHREAD_RWLOCK_INITIALIZER 这种方式初始化,而不同之处在于,使用宏不进行错误检查。
当读写锁不再使用时,需要调用 pthread_rwlock_destroy()函数将其销毁。
读写锁初始化使用示例:
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
......
pthread_rwlock_destroy(&rwlock);
读写锁上锁和解锁
以读模式对读写锁进行上锁,需要调用 pthread_rwlock_rdlock()函数;以写模式对读写锁进行上锁,需 要调用 pthread_rwlock_wrlock()函数。不管是以何种方式锁住读写锁,均可以调用 pthread_rwlock_unlock()函 数解锁,其函数原型如下所示:
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
参数 rwlock 指向读写锁对象。调用成功返回 0,失败返回一 个非 0 值的错误码。 当读写锁处于写模式加锁状态时,其它线程调用 pthread_rwlock_rdlock()或 pthread_rwlock_wrlock()函数均会获取锁失败,从而陷入阻塞等待状态;
当读写锁处于读模式加锁状态时,其它线程调用 pthread_rwlock_rdlock()函数可以成功获取到锁,如果调用 pthread_rwlock_wrlock()函数则不能获取到锁,从而陷入阻塞等待状态。 如果线程不希望被阻塞,可以调用 pthread_rwlock_tryrdlock()和 pthread_rwlock_trywrlock()来尝试加锁, 如果不可以获取锁时,两者均会立马返回错误,错误码为 EBUSY。其函数原型如下所示:
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
参数 rwlock 指向需要加锁的读写锁,加锁成功返回 0,加锁失败则返回 EBUSY。
使用示例
示例代码演示了使用读写锁来实现线程同步,全局变量 g_count 作为线程间的共享变量,主线程中创建了 5 个读取 g_count 变量的线程,它们使用同一个函数 read_thread,这 5 个线程仅仅对 g_count 变量进行读取,并将其打印出来,连带打印线程的编号(1~5);主线程中还创建了 5 个写 g_count 变量的线程,它们使用同一个函数 write_thread,write_thread 函数中会将 g_count 变量的值进行累加,循环 10 次,每次将 g_count 变量的值在原来的基础上增加 20,并将其打印出来,连带打印线程的编号(1~5)。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_rwlock_t rwlock;
static int g_count = 0;
static void *read_thread(void *arg){
int number = *((int *)arg);
int j;
for (j = 0; j < 10; j++) {
pthread_rwlock_rdlock(&rwlock); //以读模式获取锁
printf("读线程<%d>, g_count=%d\n", number+1, g_count);
pthread_rwlock_unlock(&rwlock);//解锁
sleep(1);
}
return (void *)0;
}
static void *write_thread(void *arg){
int number = *((int *)arg);
int j;
for (j = 0; j < 10; j++) {
pthread_rwlock_wrlock(&rwlock); //以写模式获取锁
printf("写线程<%d>, g_count=%d\n", number+1, g_count+=20);
pthread_rwlock_unlock(&rwlock);//解锁
sleep(1);
}
return (void *)0;
}
static int nums[5] = {0, 1, 2, 3, 4};
int main(int argc, char *argv[]){
pthread_t tid[10];
int j;
/* 对读写锁进行初始化 */
pthread_rwlock_init(&rwlock, NULL);
/* 创建 5 个读 g_count 变量的线程 */
for (j = 0; j < 5; j++)
pthread_create(&tid[j], NULL, read_thread, &nums[j]);
/* 创建 5 个写 g_count 变量的线程 */
for (j = 0; j < 5; j++)
pthread_create(&tid[j+5], NULL, write_thread, &nums[j]);
/* 等待线程结束 */
for (j = 0; j < 10; j++)
pthread_join(tid[j], NULL);//回收线程
/* 销毁自旋锁 */
pthread_rwlock_destroy(&rwlock);
exit(0);
}
编译测试,其打印结果如下:
在这个例子中,我们演示了读写锁的使用,但仅作为演示使用,在实际的应用编程中,需要根据应用场景来选择是否使用读写锁。
读写锁的属性
读写锁与互斥锁类似,也是有属性的,读写锁的属性使用 pthread_rwlockattr_t 数据类型来表示,当定义 pthread_rwlockattr_t 对象时,需要使用 pthread_rwlockattr_init()函数对其进行初始化操作,初始化会将 pthread_rwlockattr_t 对象定义的各个读写锁属性初始化为默认值;当不再使用 pthread_rwlockattr_t 对象时, 需要调用 pthread_rwlockattr_destroy()函数将其销毁,其函数原型如下所示:
#include <pthread.h>
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
参数 attr 指向需要进行初始化或销毁的 pthread_rwlockattr_t 对象;函数调用成功返回 0,失败将返回一 个非 0 值的错误码。
读写锁只有一个进程共享属性,它与互斥锁以及自旋锁的进程共享属性相同。Linux 下提供了相应的函数用于设置或获取读写锁的共享属性 。 函数pthread_rwlockattr_getpshared() 用于从 pthread_rwlockattr_t 对象中获取共享属性,函数 pthread_rwlockattr_setpshared()用于设置 pthread_rwlockattr_t 对象中的共享属性,其函数原型如下所示:
#include <pthread.h>
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
函数 pthread_rwlockattr_getpshared()参数和返回值:
attr:指向 pthread_rwlockattr_t 对象;
pshared:调用 pthread_rwlockattr_getpshared()获取共享属性,将其保存在参数 pshared 所指向的内存中;
返回值:成功返回 0,失败将返回一个非 0 值的错误码。 函数 pthread_rwlockattr_setpshared()参数和返回值:
attr:指向 pthread_rwlockattr_t 对象;
pshared:调用 pthread_rwlockattr_setpshared()设置读写锁的共享属性,将其设置为参数 pshared 指定的值。参数 pshared 可取值如下:
- PTHREAD_PROCESS_SHARED:共享读写锁。该锁可以在多个进程中的线程之间共享;
- PTHREAD_PROCESS_PRIVATE:私有读写锁。只有本进程内的线程才能够使用该读写锁,这是读写锁共享属性的默认值。
返回值:调用成功的情况下返回 0;失败将返回一个非 0 值的错误码。 使用方式如下:
pthread_rwlock_t rwlock; //定义读写锁
pthread_rwlockattr_t attr; //定义读写锁属性
/* 初始化读写锁属性对象 */
pthread_rwlockattr_init(&attr);
/* 将进程共享属性设置为 PTHREAD_PROCESS_PRIVATE */
pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_PRIVATE);
/* 初始化读写锁 */
pthread_rwlock_init(&rwlock, &attr);
......
/* 使用完之后 */
pthread_rwlock_destroy(&rwlock); //销毁读写锁
pthread_rwlockattr_destroy(&attr); //销毁读写锁属性对象
总结
“Linux线程同步”系列到此就告一段落,介绍了线程同步的几种不同的方法,包括互斥锁、条件变量、自旋锁以及读写锁,当然,除此之外, 线程同步的方法其实还有很多,譬如信号量、屏障等等。 在实际应用开发当中,用的最多的还是互斥锁和条件变量,当然具体使用哪一种线程同步方法还是得根据场景来进行选择。