多线程同步中的生产者消费者问题

生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),它在多线程同步问题中是一个经典案例,那么什么是生产者-消费者问题?

生产者-消费者问题是什么?

实际上,生产者-消费者问题描述了共享固定大小缓冲区的两个线程(即“生产者”和“消费者”)在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时还向里面加入数据,消费者也不会在缓冲区中空时继续消耗数据。

如何解决该问题呢?

通常采用进程间通信的方法解决该问题,让生产者在缓冲区满时休眠(或者放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。但是如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。

为什么要使用生产者消费者模式?

在多线程开发中,如果生产者生产数据的速度很快,而消费者消费数据的速度很慢,那么生产者就必须等待消费者消费完了数据才能够继续生产数据;同理如果消费者的速度大于生产者那么消费者就会进入等待状态。所以为了达到生产者和消费者生产数据和消费数据之间的平衡,就需要一个缓冲区用来存储生产者生产的数据,所以就引入了生产者-消费者模式。,实际上,缓冲区就是为了平衡生产者和消费者的处理能力,起到一个数据缓存的作用,同时也达到了一个解耦的作用。如果缓冲区已经满了,则生产者线程阻塞;如果缓冲区为空,那么消费者线程阻塞。

生产者-消费者模式的特点

  • 保证生产者不会在缓冲区满的时候继续向缓冲区放入数据,而消费者也不会在缓冲区空的时候,消耗数据
  • 当缓冲区满的时候,生产者会进入休眠状态,当下次消费者开始消耗缓冲区的数据时,生产者才会被唤醒,开始往缓冲区中添加数据;当缓冲区空的时候,消费者也会进入休眠状态,直到生产者往缓冲区中添加数据时才会被唤醒。

生产消费者模型

一个系统中,存在生产者和消费者两种角色,他们通过有限的共享数据缓冲区进行通信,从而实现线程间的协作。

  • 生产者(Producer):负责生成数据并放入共享的数据缓冲区中。
  • 消费者(Consumer):负责从共享的数据缓冲区中取出数据并进行处理。
  • 数据缓冲区(Buffer):用于生产者和消费者之间进行数据交换的共享空间,通常是一个队列或者缓冲池。

应用场景

  • 生产者与消费者速度不一致:例如,生产者生成数据的速度可能远远大于消费者处理数据的速度,或者相反。通过共享的数据缓冲区,可以实现生产者和消费者的解耦,从而有效地平衡生产者与消费者之间的速度差异。
  • 多个生产者和消费者
  • 控制并发访问:通过使用同步机制或者阻塞队列等方式,可以确保生产者和消费者之间的数据交换是线程安全的,避免出现竞态条件和数据不一致的情况。
  • 异步处理任务:生产者负责将任务放入任务队列,而消费者则负责从任务队列中取出任务并进行处理。通过这种方式,可以实现任务的异步提交和处理,提高系统的响应速度和吞吐量。

优点

  • 解耦:假设生产者和消费者分别是两个类。若生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(耦合)。如果消费者的代码发生变化,可能会影响到生产者。但是采用生产者与消费者模式,两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。
  • 支持并发(concurrency):生产者直接调用消费者的某个方法,会产生阻塞,在消费者的方法没有返回之前,生产者一直等待。但是采用生产者与消费者模式,生产者和消费者可以是两个独立的并发主体(常见并发类型有进程和线程两种)。生产者把制造出来的数据往缓冲区一丢,就可以再去生产下一个数据。基本上不用依赖消费者的处理速度。
  • 异步:当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。
  • 支持分布式:生产者和消费者通过队列进行通讯,不需要运行在同一台机器上,在分布式环境中可以通过redis的list作为队列,而消费者只需要轮询队列中是否有数据。同时还能支持集群的伸缩性,当某台机器宕掉的时候,不会导致整个集群宕掉

多生产者和多消费者

使用多个线程来生产数据,同样可以使用多个消费线程来消费数据。而更复杂的情况是,消费者消费的数据,有可能需要继续处理,于是消费者处理完数据之后,它又要作为生产者把数据放在新的队列里,交给其他消费者继续处理。

常见的内存缓冲区

  • 队列(FIFO):在线程方式下,生产者和消费者各自是一个线程。生产者把数据写入队列头,消费者从队列尾部读出数据。当队列为空,消费者就休息;当队列满,生产者就休息。但是在每次写入时,可能涉及到堆内存的分配;在每次读出时,可能涉及堆内存的释放。假如生产者和消费者都频繁的写入和读出,内存分配的开销是非常大的。频繁的内存分配不但增加系统的开销,更使得内存碎片不断增多,非常不利于程序长期运行。
  • 环形缓冲区:使用一段固定长度的内存,在内存用尽后,剩余未存的数据从这段内存的起始位置开始存放。这样反复使用内存,能使得我们能使用更少的内存块做更多的事情,并且对内存的管理更加方便更加安全。

生产者-消费者模式的实现

要保证以下:

  • 容器中数据状态的一致性:当一个消费者执行了读取数据之后,此时容器为空,但是还没来得及更新容器的大小,那么另外一个消费者来了之后以为不等于0,那么继续读取,从而造成了了状态的不一致性
  • 消费者和生产者之间的同步和协作关系:当容器里面没有数据的时候,消费者不会继续读取(释放锁,进入阻塞);当生产者添加了一条数据之后,重新唤醒消费者,消费者重新获取到容器的锁,继续执行读取;当容器里面满的时候,生产者不继续写入(释放锁,进入阻塞),一旦消费者读取了一条数据,此时应该唤醒生产者重新获取到容器的锁,继续写入。
  • 在访问共享区资源时,为避免多个线程同时访问资源造成混乱,需要对共享资源加锁,从而保证某一时刻只有一个线程在访问共享资源。
    对于容器状态的同步可以使用以下几种方法:
  • Object的wait() / notify()方法
  • Semaphore的acquire()/release()方法
  • BlockingQueue阻塞队列方法
  • Lock和Condition的await() / signal()方法
  • PipedInputStream/ PipedOutputStream
  • 34
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值