什么是读者-写者问题?
读者-写者问题是指多个进程对一个共享资源即数据集(如文件或记录集)进行读写操作的问题。把只要求读数据的进程称为读进程,把要求修改数据的进程称为写进程。多个读进程可以同时读此数据集,不需要互斥也不会产生问题,不存在破坏数据完整性、正确性的问题,但是一个写进程不能与其他进程(不管是读进程还是写进程)同时访问此数据集,它们之间必须互斥,否则会破坏数据集的完整性。
解决读者-写者问题的意义
理论上,直接对数据集加锁,就可以实现对多进程时数据集的访问保护。但是在一段时间内并没有写进程,只有较多的读进程的话,对数据集的加锁是不必要的,反而会降低系统的性能。因此读者-写者问题的提出是为了在上述场景下,兼顾数据集访问安全和效率。
读者-写者问题的解决方法
分析
多个读进程可以并发读数据集,因此用一个变量rc记录并发读数据集的进程数。当rc≥1时,禁止写进程对数据集进行写操作,当rc=0时,允许写进程对数据集进行写操作。
rc对于读进程而言,是一个临界资源,因此设置一个互斥信号量mutex,防止多个读进程同时修改rc的值。
数据集对于写进程而言是一个临界资源,因此设置一个互斥信号量db。
相关变量总结
-
mutex: 保护对rc的访问,初值为1
-
db: 控制写进程对数据集的访问,初值为1
-
rc: 公共变量,记录读进程的个数
同步算法
Semaphore mutex=1; //保护对rc的访问
Semaphore db=1; //控制对数据集的访问
int rc=0; //读数据集的读进程个数
main()
{
Cobegin //两个进程并发执行
reader();
writer();
Coend
}
void reader() //读者进程
{
while(true){
P(mutex); //互斥对rc的访问
rc = rc + 1; //增加一个读进程数
if(rc == 1){ //当第一个读进程读数据集时,阻止写进程写
P(db);
}
V(mutex); //恢复对rc的访问
读数据集
P(mutex); //互斥对rc的访问
rc = rc - 1; //读者减少一个
if(rc == 0){ //当最后一个读进程读完数据集时,允许写进程写
V(db);
}
V(mutex); //恢复对rc的访问
}
}
void writer() //写者进程
{
while(true){
P(db); //排斥访问数据集
写数据集;
V(db); //恢复对数据集的访问
}
}
运行分析
当一个读进程在使用数据集时,其他读进程也可以同时进行读操作,读进程的个数由rc来衡量。
此时,如果有一个写进程到来,由于rc>0,因此写进程被挂起,知道所有读进程都结束时,写进程才可以开始写操作。
当一个写进程在写操作时,其他进程(包括读进程和写进程)都会被挂起,直到当前写进程完成。
上述算法确实可以解决读者-写者问题,当然也导致一个潜在的问题——只要有读进程陆续到来,写进程就会被一直挂起直到没有一个读进程为止,因此上述算法也被称为以读者为优先的算法。感兴趣的读者还可以查看以写者为优先的算法。
多面墙的查询问题
笔者在实现弹幕系统的过程中遇到了如下问题。
观众可以通过微信小程序发送弹幕到服务器。在服务器的数据库中,每条弹幕对应不同的记录,因此发送弹幕的请求不是互斥的。并且为了支持尽可能多的观众同时发送弹幕,发送弹幕的请求并发度需要比较高。
与此同时,有n面弹幕墙向服务器查询最新的弹幕,在某一段时间内,这n面墙应该获取到相同的弹幕。由于n的大小是未知的,让每个弹幕记录是否被某一面墙查询过是比较困难的。因此我们让每一面墙维护一个最近查询时间last_query_time,当其向服务器发送查询请求时,服务器只会返回在last_query_time之后通过审核的弹幕。因此每个弹幕只需要记录审核时间checking_time即可。
当服务器处理墙A的查询请求时,如果有新的弹幕B通过审核,则B在这次查询请求和下次查询请求中都不会返回给墙A。则弹幕B就被遗漏了。为了防止弹幕遗漏的发生,当墙A进行查询时,所有的发送弹幕请求都需要被挂起。而当有发送弹幕请求正在被处理时,所有的弹幕查询请求也需要被挂起。
多面墙的查询问题的解决
通过上述描述,读者不难发现多面墙的查询问题与读者-写者问题非常类似。
发送弹幕请求对应读进程,请求数量多,并且彼此不互斥。
查询弹幕请求对应写进程,请求数量少,并且当有查询弹幕请求时,不允许处理发送弹幕请求;当有发送弹幕请求时,不允许有查询弹幕请求。
网友评论