信号量机制实现读者写者问题(思路剖析+Java代码实现+验证)

写在前面:

Java中:

  1. 我们用这样的代码新建一个信号量:Semaphore mutex = new Semaphore(1);
  2. P操作(wait)的代码为:mutex.acquire();
  3. V操作(signal)的代码为:mutex.release();

本文章的内容:

  1. 读者写者问题(读者优先)
  2. 读者写者问题(公平)
  3. 读者写者问题(写者优先)

 
及其:

  1. 思路
  2. 验证

 
 
 
 

读者优先

思路:
读者写者问题大概是:对于临界文件的访问,不允许写-写,不允许写-读,但允许读-读
核心难点在于:读者和读者之间是不互斥的——那么,单单使用一个wr读写互斥锁,是不足以满足需求的
我们的解决方案是:既然读者之间不互斥,我们可以让只有第一个进入的读者需要P(rw),只有最后一个退出的读者需要V(rw)
即:第二、第三…之后的读者都跳过了P(rw),从而不被阻塞!
 
考虑下面一种情况:
Writer一个不小心没抢过Reader,rw锁被Reader拿走。之后,如果Reader源源不断的到来,源源不断的跳过P(rw),源源不断的使得readerCnt++…
那么,直到最后一个Reader退出释放rw,所有的Writer将永远因为得不到rw而阻塞,即发生饥饿——体会一下,这是不是读者优先?

SharedData.java

public class SharedData {
    public Semaphore rw = new Semaphore(1);			// 读写锁(本质是个互斥锁)
    public Semaphore mutex = new Semaphore(1);		// 读者计数器の互斥锁
    public int readerCnt = 0;						// 读者计数器
}

Writer.java

public class Writer implements Runnable {

    SharedData sharedData;

    public Writer(SharedData sharedData){
        this.sharedData = sharedData;
    }

    private void write(){
        System.out.println(Thread.currentThread().getName() + " Writing...");
    }

    @Override
    public void run() {
        while (true){
            try {
            	// 对于写者,直接用「读写锁」锁一下就行了,读者稍稍会麻烦一些 >_<
                sharedData.rw.acquire();
                write();
                sharedData.rw.release();

				// // 睡一小会儿,不然while死循环刷得太快了
                Thread.sleep(new Random().nextInt(2));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Reader.java

public class Reader implements Runnable {

    SharedData sharedData;

    public Reader(SharedData sharedData){
        this.sharedData = sharedData;
    }

    private void read(){
        System.out.println(Thread.currentThread().getName() + " Reading...");
    }

    @Override
    public void run() {
        while (true){
            try {
                // 第一个(cnt==0)读者进行P操作
                sharedData.mutex.acquire();
                if(sharedData.readerCnt == 0){
                    sharedData.rw.acquire();
                }
                sharedData.readerCnt++;
                sharedData.mutex.release();
                
                // 开始读 >_<
                read();
                
                // 最后一个(cnt==0)读者进行V操作
                sharedData.mutex.acquire();
                sharedData.readerCnt--;
                if(sharedData.readerCnt == 0){
                    sharedData.rw.release();
                }
                sharedData.mutex.release();
				
				// 睡一小会儿,不然while死循环刷得太快了
                Thread.sleep(new Random().nextInt(2));
                
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Test.java

public class Test {
    public static void main(String[] args) {
    	// 共享数据(信号量计数器,啥的...)
        SharedData sharedData = new SharedData();
        
		// 1写者
        new Thread(new Writer(sharedData)).start();
        // 9读者
        new Thread(new Reader(sharedData)).start();
        new Thread(new Reader(sharedData)).start();
        new Thread(new Reader(sharedData)).start();
        new Thread(new Reader(sharedData)).start();
        new Thread(new Reader(sharedData)).start();
        new Thread(new Reader(sharedData)).start();
        new Thread(new Reader(sharedData)).start();
        new Thread(new Reader(sharedData)).start();
        new Thread(new Reader(sharedData)).start();
    }
}

验证:
上面分析过,Reader源源不断地到来会导致Writer的饥饿
那么,我们安排1:9的写/读者比例,模拟这种场景——你会发现,Writer很难得到一次运行(下图)
再取极端的情况,删掉Reader的Thread.sleep,让读者真正“源源不断”——Writer将完全饥饿(自行试一下吧!)

在这里插入图片描述

 
 
 
 

公平读写

思路:
上面的「读者优先」引入了一种思想:通过计数来实现跳过P(rw)——这非常巧妙
这里,我们再引入一种极为巧妙的思想:设置一个queue信号信号量,来实现一个等待队列
 
如何实现所谓“等待队列”的呢?不妨用一个例子,通俗地顺着走一下:(读 - 写 - 读)

  • 未使用queue信号量:Reader到来拿走rw;Writer到来被阻塞;第二个Reader到来,因为readerCnt != 0而跳过P(rw)开始执行——所谓“读者优先”
  • 使用queue信号量:Reader到来拿走queue,接着拿走rw后归还queue;Writer到来拿走queue,接着被P(rw)阻塞;第二个Reader到来,上来就被P(queue)阻塞,它何时才能执行呢?必须等到第一个Reader归还rw,Writer得到rw执行完毕后,归还queue,第二个Reader才得以执行——queue使第二个Reader强制等待,形成一个“Reader-Writer-第二个Reader”的固定执行顺序——所谓“等待队列”

SharedData.java

public class SharedData {
    public Semaphore rw = new Semaphore(1);			// 读写锁(本质是个互斥锁)
    public Semaphore mutex = new Semaphore(1);		// 读者计数器の互斥锁
    public Semaphore queue = new Semaphore(1);		// 队列锁
    public int readerCnt = 0;						// 读者计数器
}

Writer.java

public class Writer implements Runnable {

    SharedData sharedData;

    public Writer(SharedData sharedData){
        this.sharedData = sharedData;
    }

    private void write(){
        System.out.println(Thread.currentThread().getName() + " Writing...");
    }

    @Override
    public void run() {
        while (true){
            try {
            	// 核心逻辑
                sharedData.queue.acquire();
                sharedData.rw.acquire();
                write();
                sharedData.rw.release();
                sharedData.queue.release();
                // 睡一觉zzz
                Thread.sleep(new Random().nextInt(2));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Reader.java

public class Reader implements Runnable {

    SharedData sharedData;

    public Reader(SharedData sharedData){
        this.sharedData = sharedData;
    }

    private void read(){
        System.out.println(Thread.currentThread().getName() + " Reading...");
    }

    @Override
    public void run() {
        while (true){
            try {
                // 第一个(cnt==0)读者进行P操作
                sharedData.queue.acquire();
                sharedData.mutex.acquire();
                if(sharedData.readerCnt == 0){
                    sharedData.rw.acquire();
                }
                sharedData.readerCnt++;
                sharedData.mutex.release();
                sharedData.queue.release();
                
                // 开始读 >_<
                read();
                
                // 最后一个(cnt==0)读者进行V操作
                sharedData.mutex.acquire();
                sharedData.readerCnt--;
                if(sharedData.readerCnt == 0){
                    sharedData.rw.release();
                }
                sharedData.mutex.release();
                
                // 睡一觉zzz
                Thread.sleep(new Random().nextInt(2));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Test.java

public class Test {
    public static void main(String[] args) {
    	// 共享数据(信号量,计数器啥的...)
        SharedData sharedData = new SharedData();
        
		// 5写者
        new Thread(new Writer(sharedData)).start();
        new Thread(new Writer(sharedData)).start();
        new Thread(new Writer(sharedData)).start();
        new Thread(new Writer(sharedData)).start();
		// 5读者
        new Thread(new Reader(sharedData)).start();
        new Thread(new Reader(sharedData)).start();
        new Thread(new Reader(sharedData)).start();
        new Thread(new Reader(sharedData)).start();
    }
}

验证:
既然已经实现了读写公平,那么我们便安排5:5的写/读者比例即可
很直观地可以看出,读者写者得到了“公平”的待遇(下图)

在这里插入图片描述

 
 
 
 

写者优先

思路:
这是读者优先所用思想的再一次运用。为什么Reader得以优先呢?

  • 因为只有第一个Reader会被rw阻塞,之后来到的Reader可以跳过P(rw);而所有的Writer都会被rw阻塞——这就是Reader得到的“特权”
     

仿写句子:如何让Writer得以优先呢?

  • 我们让只有第一个Writer会被queue阻塞,之后来到的Writer可以跳过P(queue);而所有的Reader都会被queue阻塞——这就是Writer得到的“特权”

SharedData.java

public class SharedData {
    public Semaphore rw = new Semaphore(1);			// 读写锁(本质是个互斥锁)
    public Semaphore mutex = new Semaphore(1);		// 读者计数器の互斥锁
    public Semaphore mutex2 = new Semaphore(1);		// 写者计数器の互斥锁
    public Semaphore queue = new Semaphore(1);		// 队列锁
    public int readerCnt = 0;						// 读者计数器
    public int writerCnt = 0;						// 写者计数器
}

Writer.java

public class Writer implements Runnable {

    SharedData sharedData;

    public Writer(SharedData sharedData){
        this.sharedData = sharedData;
    }

    private void write(){
        System.out.println(Thread.currentThread().getName() + " Writing...");
    }

    @Override
    public void run() {
        while (true){
            try {
            	// 只有第一个进入的写者才需要排队
                sharedData.mutex2.acquire();
                if(sharedData.writerCnt == 0){
                    sharedData.queue.acquire();
                }
                sharedData.writerCnt++;
                sharedData.mutex2.release();

                sharedData.rw.acquire();
                write();
                sharedData.rw.release();

				// 最后一个退出的写者释放queue
                sharedData.mutex2.acquire();
                sharedData.writerCnt--;
                if(sharedData.writerCnt == 0){
                    sharedData.queue.release();
                }
                sharedData.mutex2.release();
				
				// 睡一觉zzz
                Thread.sleep(new Random().nextInt(2));
                
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Reader.java

public class Reader implements Runnable {

    SharedData sharedData;

    public Reader(SharedData sharedData){
        this.sharedData = sharedData;
    }

    private void read(){
        System.out.println(Thread.currentThread().getName() + " Reading...");
    }

    @Override
    public void run() {
        while (true){
            try {
                // 所有的读者会被queue阻塞
                sharedData.queue.acquire();
                sharedData.mutex.acquire();
                if(sharedData.readerCnt == 0){
                    sharedData.rw.acquire();
                }
                sharedData.readerCnt++;
                sharedData.mutex.release();
                sharedData.queue.release();
                
                // 开始读 >_<
                read();
                
                sharedData.mutex.acquire();
                sharedData.readerCnt--;
                if(sharedData.readerCnt == 0){
                    sharedData.rw.release();
                }
                sharedData.mutex.release();
                
                // 睡一觉zzz
                Thread.sleep(new Random().nextInt(2));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Test.java

public class Test {
    public static void main(String[] args) {
    	// 共享数据(信号量,计数器啥的...)
        SharedData sharedData = new SharedData();

		// 1读者
        new Thread(new Reader(sharedData)).start();
		// 9写者
        new Thread(new Writer(sharedData)).start();
        new Thread(new Writer(sharedData)).start();
        new Thread(new Writer(sharedData)).start();
        new Thread(new Writer(sharedData)).start();
        new Thread(new Writer(sharedData)).start();
        new Thread(new Writer(sharedData)).start();
        new Thread(new Writer(sharedData)).start();
        new Thread(new Writer(sharedData)).start();
        new Thread(new Writer(sharedData)).start();
    }
}

验证:
和「读者优先」一样的思路。我们设置1:9的读/写比例,模拟“源源不断”的Writer——Reader难以得到一次运行(下图)
再取极端的情况,删掉Writer的Thread.sleep,让读者真正“源源不断”——Reader将完全饥饿(自行试一下吧!)

在这里插入图片描述

 
 
 
 
 
 
 
 

上面为了能直观展示Writer/Reader的执行情况,仅仅打印了一个“writing/reading”作为演示
如果想成为一个真正的读者/写者,可以将write()/read()方法中的代码替换成:

write()

String path = "/Users/你的自定义文件路径/共享文件.txt";

try {
	// 从外部获取一行并写入文件(自动换行)
     BufferedWriter bw = new BufferedWriter(new FileWriter(new File(path), true));
     BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
     bw.write(br.readLine());
     bw.write("\n");
     br.close();
     bw.close();
} catch (IOException e) {
     e.printStackTrace();
}

read()

String path = "/Users/你的自定义文件路径/共享文件.txt";

try {
	 // 从文件读取一行并打印出来(如果文本为空则打印null)
     BufferedReader br = new BufferedReader(new FileReader(new File(path)));
     String line = br.readLine();
     System.out.println(line);
     br.close();
} catch (FileNotFoundException e) {
     e.printStackTrace();
} catch (IOException e){
     e.printStackTrace();
}

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
E N D END END

  • 12
    点赞
  • 64
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
读者优先和者优先问题都是读者问题的变种。在读者优先问题中,如果有读者正在访问共享资源,则者需要等待,直到所有读者完成访问。在者优先问题中,如果有者正在访问共享资源,则读者需要等待,直到者完成访问。 为了解决这两种问题,我们可以使用信号机制实现。具体实现如下: 读者优先问题: ```c #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> sem_t rw_mutex, mutex; int read_count = 0, shared_resource = 0; void *reader(void *arg) { int id = *(int *)arg; while (1) { sem_wait(&mutex); read_count++; if (read_count == 1) sem_wait(&rw_mutex); sem_post(&mutex); printf("Reader %d read shared resource: %d\n", id, shared_resource); sem_wait(&mutex); read_count--; if (read_count == 0) sem_post(&rw_mutex); sem_post(&mutex); sleep(1); } return NULL; } void *writer(void *arg) { int id = *(int *)arg; while (1) { sem_wait(&rw_mutex); shared_resource++; printf("Writer %d write shared resource: %d\n", id, shared_resource); sem_post(&rw_mutex); sleep(3); } return NULL; } int main() { pthread_t threads[5]; int i, readers[3] = {1, 2, 3}, writers[2] = {1, 2}; sem_init(&rw_mutex, 0, 1); sem_init(&mutex, 0, 1); for (i = 0; i < 3; i++) pthread_create(&threads[i], NULL, reader, &readers[i]); for (i = 0; i < 2; i++) pthread_create(&threads[i + 3], NULL, writer, &writers[i]); for (i = 0; i < 5; i++) pthread_join(threads[i], NULL); sem_destroy(&rw_mutex); sem_destroy(&mutex); return 0; } ``` 在这个例子中,我们使用了两个信号 `rw_mutex` 和 `mutex`。其中,`rw_mutex` 用于保证者的互斥访问,`mutex` 用于保证读者计数器的互斥访问。 在读者线程中,我们首先获取 `mutex` 信号,然后将读者计数器加一。如果读者计数器为 1,则获取 `rw_mutex` 信号,以保证者不能同时访问共享资源。然后,我们输出共享资源的值,并将读者计数器减一。如果读者计数器为 0,则释放 `rw_mutex` 信号,以允许者访问共享资源。 在者线程中,我们首先获取 `rw_mutex` 信号,以保证者的互斥访问。然后,我们将共享资源的值加一,并输出该值。最后,我们释放 `rw_mutex` 信号者优先问题: ```c #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> sem_t rw_mutex, mutex; int read_count = 0, write_count = 0, shared_resource = 0; void *reader(void *arg) { int id = *(int *)arg; while (1) { sem_wait(&mutex); if (write_count > 0) sem_post(&mutex); else { read_count++; sem_post(&mutex); printf("Reader %d read shared resource: %d\n", id, shared_resource); sem_wait(&mutex); read_count--; if (read_count == 0) sem_post(&rw_mutex); sem_post(&mutex); } sleep(1); } return NULL; } void *writer(void *arg) { int id = *(int *)arg; while (1) { sem_wait(&mutex); write_count++; if (read_count > 0 || write_count > 1) sem_post(&mutex); else { sem_post(&mutex); sem_wait(&rw_mutex); shared_resource++; printf("Writer %d write shared resource: %d\n", id, shared_resource); sem_post(&rw_mutex); sem_wait(&mutex); write_count--; sem_post(&mutex); } sleep(3); } return NULL; } int main() { pthread_t threads[5]; int i, readers[3] = {1, 2, 3}, writers[2] = {1, 2}; sem_init(&rw_mutex, 0, 1); sem_init(&mutex, 0, 1); for (i = 0; i < 3; i++) pthread_create(&threads[i], NULL, reader, &readers[i]); for (i = 0; i < 2; i++) pthread_create(&threads[i + 3], NULL, writer, &writers[i]); for (i = 0; i < 5; i++) pthread_join(threads[i], NULL); sem_destroy(&rw_mutex); sem_destroy(&mutex); return 0; } ``` 在这个例子中,我们同样使用了两个信号 `rw_mutex` 和 `mutex`。其中,`rw_mutex` 用于保证者的互斥访问,`mutex` 用于保证读者者计数器的互斥访问。 在读者线程中,我们首先获取 `mutex` 信号。如果有者在等待,则释放 `mutex` 信号,以允许者先访问共享资源。否则,我们将读者计数器加一,并释放 `mutex` 信号。然后,我们输出共享资源的值,并将读者计数器减一。如果读者计数器为 0,则释放 `rw_mutex` 信号,以允许者访问共享资源。 在者线程中,我们首先获取 `mutex` 信号。如果有其他读者者在访问共享资源,则释放 `mutex` 信号,以允许其他线程先访问共享资源。否则,我们将者计数器加一,并释放 `mutex` 信号。然后,我们获取 `rw_mutex` 信号,以保证者的互斥访问。在访问共享资源时,我们先将共享资源的值加一,并输出该值。最后,我们释放 `rw_mutex` 信号,并将者计数器减一。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值