线程中互斥锁和读写锁相关区别应用示例
一.线程同步与互斥
1. 相关概念:
1.1 同步与互斥的含义?
互斥: 是指对同一资源的访问同时仅允许一个访问者,互斥具有排他性,
互斥无法保证访问者的访问顺序。
同步: 是在互斥的基础上,实现对同一资源的有序访问。
2. 互斥问题及解决方案
2.1 互斥锁(mutex)
互斥锁机制: 互斥锁是内核提供的一个用于共享资源互斥访问的内核资源。
互斥锁有两个状态:上锁,解锁, 线程希望上锁一个已经被
上锁的互斥锁时则线程阻塞,直到互斥锁处于解锁状态,
如果一个线程在共享资源访问前都对一个互斥锁进行上锁操作,
系统仅能满足一个线程上锁操作的请求,其他线程将被阻塞,
这样就可以保证共享资源同一时刻仅有一个访问者进行访问。
互斥锁的操作:
1) 申请初始化互斥锁
1.1 静态初始化: pthread_mutex_t mutex = PTHREAD_MUTEX_INITILAIZER;
1.2 动态初始化
pthread_mutex_init
头文件: #include <pthread.h>
函数原型: int pthread_mutex_init(pthread_mutex_t *mutex,
const pthread_mutexattr_t* attr);
函数功能: 申请初始化互斥锁
函数参数: mutex: [OUT]待初始化的互斥锁
attr: 互斥锁属性,具有以下取值:
PTHREAD_MUTEX_INITIALIZER: 快速互斥锁
PTHREAD_RECURSIVE_MUTEX_INITIALIZER_UP: 递归互斥锁
PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_UP: 检错互斥锁
函数返回值: 成功返回 0
失败返回错误码
2) 上锁
pthread_mutex_lock
头文件: #include <pthread.h>
函数原型: int pthread_mutex_lock(pthread_mutex_t *mutex);
函数功能: 上锁互斥锁
函数参数: mutex: 待上锁的互斥锁
函数返回值: 成功返回 0
失败返回错误码
3) 解锁
pthread_mutex_unlock
头文件: #include <pthread.h>
函数原型: int pthread_mutex_unlock(pthread_mutex_t *mutex);
函数功能: 解锁互斥锁
函数参数: mutex: 待解锁的互斥锁
函数返回值: 成功返回 0
失败返回错误码
4) 回收互斥锁
pthread_mutex_destroy
头文件: #include <pthread.h>
函数原型: int pthread_mutex_destroy(pthread_mutex_t *mutex);
函数功能: 回收互斥锁
函数参数: mutex: 待回收的互斥锁
函数返回值: 成功返回 0
失败返回错误码
互斥锁的实际使用:
1. 由主线程负责申请和回收
2. 线程在共享资源访问前上锁,访问结束后解锁。
2.2 读写锁(rwlock)
读写锁机制:与互斥锁类型,只是读写锁在上锁操作时,细化为读上锁和写上锁两种操作,
读上锁可以让线程共享访问同一资源,写上锁只能独占式访问资源,如果针对
读访问多于写访问的场合,读访问者在同一资源访问前读上锁,不阻塞,资源
可以共享访问。无疑会提高资源的访问效率。
读写锁特点:
1. 读上锁仅在写上锁的状态下阻塞;
2. 写上锁仅在解锁的状态下才能进行;
读写锁的操作
1) 申请初始化读写锁
1.1 静态初始化: pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITILAIZER;
1.2 动态初始化
pthread_rwlock_init
头文件: #include <pthread.h>
函数原型: int pthread_rwlock_init(pthread_rwlock_t *rwlock,
const pthread_rwlockattr_t* attr);
函数功能: 申请初始化读写锁
函数参数: rwlock: [OUT]待初始化的读写锁
attr: 读写锁属性,一般取值NULL
函数返回值: 成功返回 0
失败返回错误码
2) 上锁
2.1 读上锁
pthread_rwlock_rdlock
头文件: #include <pthread.h>
函数原型: int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
函数功能: 读上锁读写锁
函数参数: rwlock: 待操作的读写锁
函数返回值: 成功返回 0
失败返回错误码
2.2 写上锁
pthread_rwlock_wrlock
头文件: #include <pthread.h>
函数原型: int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
函数功能: 写上锁读写锁
函数参数: rwlock: 待操作的读写锁
函数返回值: 成功返回 0
失败返回错误码
3) 解锁
pthread_rwlock_unlock
头文件: #include <pthread.h>
函数原型: int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
函数功能: 解锁读写锁
函数参数: rwlock: 待操作的读写锁
函数返回值: 成功返回 0
失败返回错误码
4) 回收
pthread_rwlock_destroy
头文件: #include <pthread.h>
函数原型: int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
函数功能: 回收 读写锁
函数参数: rwlock: 待操作的读写锁
函数返回值: 成功返回 0
失败返回错误码
读写锁的实际使用:
1. 由主线程负责申请和回收
2. 线程在共享资源访问前,根据对资源的访问方式,选择不同的上锁方式,
若线程对共享资源是进行读访问,则读上锁,否则写上锁。 访问结束后解锁。
使用场景:
1. 读访问者数量 > 1
二、互斥锁示例
#include <unistd.h> // 提供系统调用
#include <pthread.h> // 多线程编程
#include <stdio.h> // 标准输入输出
#include <sched.h> // 线程调度控制// 打印数字的线程函数
void* PrintDigital(void* argp)
{pthread_mutex_t *mutex = (pthread_mutex_t*)argp; // 获取互斥锁指针for(int i = 0; i < 5; i++) // 循环5次{pthread_mutex_lock(mutex); // 获取互斥锁// 打印0-9的数字for(int j = 0; j < 10; j++){printf("%c", '0' + j); // 打印数字字符sched_yield(); // 主动让出CPU,给其他线程运行机会} printf("\n"); // 换行pthread_mutex_unlock(mutex); // 释放互斥锁}return NULL;
}// 打印字母的线程函数
void* PrintAlpha(void* argp)
{pthread_mutex_t *mutex = (pthread_mutex_t*)argp; // 获取互斥锁指针for(int i = 0; i < 5; i++) // 循环5次{pthread_mutex_lock(mutex); // 获取互斥锁// 打印A-Z的字母for(int j = 0; j < 26; j++){printf("%c", 'A' + j); // 打印字母字符sched_yield(); // 主动让出CPU} printf("\n"); // 换行pthread_mutex_unlock(mutex); // 释放互斥锁}return NULL;
}int main(int argc, char** argv)
{pthread_t id[2]; // 两个线程ID// 初始化互斥锁pthread_mutex_t mutex;pthread_mutex_init(&mutex, NULL);// 创建两个线程,都使用同一个互斥锁pthread_create(&id[0], NULL, PrintDigital, &mutex);pthread_create(&id[1], NULL, PrintAlpha, &mutex);// 等待两个线程结束pthread_join(id[0], NULL);pthread_join(id[1], NULL);// 销毁互斥锁pthread_mutex_destroy(&mutex);return 0;
}
关键机制详解
1. 互斥锁(Mutex)机制
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
工作原理:
同一时间只有一个线程能获得锁
其他线程在
pthread_mutex_lock()
处阻塞等待获得锁的线程执行完临界区后调用
pthread_mutex_unlock()
释放锁
2. sched_yield()
函数
sched_yield(); // 主动让出CPU
作用:
当前线程主动放弃CPU,让调度器选择其他线程运行
增加线程间的交替执行频率
使竞争情况更明显
3. 程序执行逻辑
预期的输出模式:
0123456789
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
ABCDEFGHIJKLMNOPQRSTUVWXYZ
...(重复5次)
实际可能的输出:
由于互斥锁的保护,每次输出都是完整的数字序列或完整的字母序列,不会出现混合。
程序执行流程分析
时间线示例:
时间点 线程0(数字) 线程1(字母) 输出
--------------------------------------------------------------------
t0 lock(成功) 开始输出数字
t1 输出0, yield
t2 lock(阻塞) 线程1等待
t3 输出1, yield
... ... ... 继续输出数字
t10 输出9, 换行, unlock
t11 lock(成功) 开始输出字母
t12 输出A, yield
t13 lock(阻塞) 线程0等待
t14 输出B, yield
... ... ... 继续输出字母
t39 输出Z, 换行, unlock
t40 lock(成功) 开始新一轮数字输出
程序演示的概念
1. 线程同步
使用互斥锁确保输出完整性
防止数字和字母交叉输出
2. 临界区保护
// 临界区开始
pthread_mutex_lock(mutex);
// 安全的输出操作
for(int j = 0; j < 10; j++) {printf("%c",'0' + j);sched_yield();
}
printf("\n");
// 临界区结束
pthread_mutex_unlock(mutex);
3. 线程调度
sched_yield()
演示了主动调度展示了线程间的竞争和协作
可能的输出结果
理想情况(严格交替):
0123456789
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
ABCDEFGHIJKLMNOPQRSTUVWXYZ
实际情况(可能的变化):
由于调度器的行为,可能会出现连续几次数字或字母
但每次输出都是完整的序列(感谢互斥锁保护)
程序的教育意义
这个程序很好地演示了:
互斥锁的基本用法 - 保护共享资源(这里是标准输出)
线程同步的重要性 - 避免输出混乱
主动调度的效果 -
sched_yield()
的使用临界区的概念 - 需要互斥访问的代码区域
扩展实验
可以修改程序来观察不同行为:
1. 移除互斥锁
// 注释掉锁操作
// pthread_mutex_lock(mutex);
for(int j = 0; j < 10; j++) {printf("%c",'0' + j);sched_yield();
}
printf("\n");
// pthread_mutex_unlock(mutex);
结果:数字和字母会混合输出,如:0A1B2C3D...
2. 移除 sched_yield()
for(int j = 0; j < 10; j++) {printf("%c",'0' + j);// sched_yield(); // 移除主动调度
}
结果:线程切换频率降低,可能连续输出多行相同类型
这个程序是一个很好的多线程编程教学示例,清晰地展示了同步机制的重要性。
三、读写锁
#include <unistd.h> // 提供系统调用
#include <pthread.h> // 多线程编程
#include <stdio.h> // 标准输入输出
#include <sched.h> // 线程调度控制// 打印数字的线程函数
void* PrintDigital(void* argp)
{pthread_rwlock_t *rwlock = (pthread_rwlock_t*)argp; // 获取读写锁指针for(int i = 0; i < 5; i++) // 循环5次{pthread_rwlock_wrlock(rwlock); // 获取写锁// 打印0-9的数字for(int j = 0; j < 10; j++){printf("%c", '0' + j); // 打印数字字符sched_yield(); // 主动让出CPU,给其他线程运行机会} printf("\n"); // 换行pthread_rwlock_unlock(rwlock); // 释放读写锁}return NULL;
}// 打印字母的线程函数
void* PrintAlpha(void* argp)
{pthread_rwlock_t *rwlock = (pthread_rwlock_t*)argp; // 获取读写锁指针for(int i = 0; i < 5; i++) // 循环5次{pthread_rwlock_wrlock(rwlock); // 获取写锁// 打印A-Z的字母for(int j = 0; j < 26; j++){printf("%c", 'A' + j); // 打印字母字符sched_yield(); // 主动让出CPU} printf("\n"); // 换行pthread_rwlock_unlock(rwlock); // 释放读写锁}return NULL;
}int main(int argc, char** argv)
{pthread_t id[2]; // 两个线程ID// 初始化读写锁pthread_rwlock_t rwlock;pthread_rwlock_init(&rwlock, NULL);// 创建两个线程,都使用同一个读写锁pthread_create(&id[0], NULL, PrintDigital, &rwlock);pthread_create(&id[1], NULL, PrintAlpha, &rwlock);// 等待两个线程结束pthread_join(id[0], NULL);pthread_join(id[1], NULL);// 销毁读写锁pthread_rwlock_destroy(&rwlock);return 0;
}
关键机制详解
1. 读写锁(Read-Write Lock)机制
读写锁的特点:
读锁(rdlock):多个线程可以同时获得读锁
写锁(wrlock):只有一个线程能获得写锁,且获得写锁时不能有读锁
互斥:写锁与其他所有锁(读锁和写锁)互斥
2. 在这个程序中的使用方式
pthread_rwlock_wrlock(rwlock); // 两个线程都获取写锁
当前的使用方式:
两个线程都请求写锁
写锁之间是互斥的,所以行为与普通互斥锁相同
每次只有一个线程能获得锁,输出完整的数字序列或字母序列
3. 读写锁的正确使用场景
读写锁适用于"读多写少"的场景:
// 读者线程
void* reader(void* argp) {pthread_rwlock_rdlock(rwlock); // 获取读锁// 读取共享数据(多个读者可同时进行)pthread_rwlock_unlock(rwlock);return NULL;
}// 写者线程
void* writer(void* argp) {pthread_rwlock_wrlock(rwlock); // 获取写锁// 修改共享数据(独占访问)pthread_rwlock_unlock(rwlock);return NULL;
}
程序执行逻辑
预期的输出:
0123456789
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
ABCDEFGHIJKLMNOPQRSTUVWXYZ
执行流程:
一个线程获得写锁,输出完整序列
释放锁后,另一个线程获得写锁,输出完整序列
交替进行5次
改进建议:演示读写锁的真正优势
如果要真正展示读写锁的价值,可以修改程序:
方案1:添加读者线程
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <sched.h>int shared_data = 0; // 共享数据// 读者线程 - 可以并发执行
void* reader(void* argp) {pthread_rwlock_t *rwlock = (pthread_rwlock_t*)argp;for(int i = 0; i < 10; i++) {pthread_rwlock_rdlock(rwlock); // 获取读锁printf("Reader: shared_data = %d\n", shared_data);pthread_rwlock_unlock(rwlock);sched_yield();}return NULL;
}// 写者线程 - 独占执行
void* writer(void* argp) {pthread_rwlock_t *rwlock = (pthread_rwlock_t*)argp;for(int i = 0; i < 5; i++) {pthread_rwlock_wrlock(rwlock); // 获取写锁shared_data++; // 修改共享数据printf("Writer: updated shared_data to %d\n", shared_data);pthread_rwlock_unlock(rwlock);sched_yield();}return NULL;
}int main() {pthread_t readers[3], writer_thread;pthread_rwlock_t rwlock;pthread_rwlock_init(&rwlock, NULL);// 创建3个读者线程和1个写者线程for(int i = 0; i < 3; i++) {pthread_create(&readers[i], NULL, reader, &rwlock);}pthread_create(&writer_thread, NULL, writer, &rwlock);// 等待所有线程结束for(int i = 0; i < 3; i++) {pthread_join(readers[i], NULL);}pthread_join(writer_thread, NULL);pthread_rwlock_destroy(&rwlock);return 0;
}
方案2:区分读操作和写操作
void* PrintDigital(void* argp) {pthread_rwlock_t *rwlock = (pthread_rwlock_t*)argp;for(int i = 0; i < 5; i++) {// 如果是读取操作,使用读锁(但这里printf实际上是"写"操作)pthread_rwlock_wrlock(rwlock); // 正确,因为输出到终端是"写"操作for(int j = 0; j < 10; j++) {printf("%c", '0' + j);sched_yield();} printf("\n");pthread_rwlock_unlock(rwlock);}return NULL;
}
读写锁 vs 互斥锁
特性 | 互斥锁 (Mutex) | 读写锁 (RWLock) |
---|---|---|
并发性 | 完全互斥 | 读读并发 |
性能 | 一般 | 读多写少场景性能更好 |
使用场景 | 任何需要互斥的场景 | 读操作远多于写操作的场景 |
复杂度 | 简单 | 相对复杂 |
总结
在这个程序中:
技术上正确:使用写锁确实保护了临界区
但未充分利用:没有展示读写锁的并发读特性
效果等同互斥锁:因为两个线程都在请求写锁
如果要真正演示读写锁的优势,需要设计包含多个读者线程的场景,展示读锁的并发性。