Kotlinでマルチスレッドの生産/消費モデルを実装する方法はたくさんあります(それらのほとんどはJavaにも適用されます)
- 同期
- ロック
- BlockingQueue
- セマフォ
- RxJava
- コルーチン
同期
val buffer = LinkedList<Data>()
val MAX = 5 //buffer最大size
val lock = Object()
fun produce(data: Data) {
sleep(2000) // mock produce
synchronized(lock) {
while (buffer.size >= MAX) {
// 当buffer满时,停止生产
// 注意此处使用while不能使用if,因为有可能是被另一个生产线程而非消费线程唤醒,所以要再次检查buffer状态
// 如果生产消费两把锁,则不必担心此问题
lock.wait()
}
buffer.push(data)
// notify方法只唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。
// notifyAll会唤醒所有等待中线程,哪一个线程将会第一个处理取决于操作系统的实现,但是都有机会处理。
// 此处使用notify有可能唤醒的是另一个生产线程从而造成死锁,所以必须使用notifyAll
lock.notifyAll()
}
}
fun consume() {
synchronized(lock) {
while (buffer.isEmpty())
lock.wait() // 暂停消费
buffer.removeFirst()
lock.notifyAll()
}
sleep(2000) // mock consume
}
// 同时启动多个生产、消费线程
repeat(10) {
Thread {
produce(Data()) }.start()
}
repeat(10) {
Thread {
consume() }.start()
}
ロック
Lock over Synchronizedの利点は、複数の生産ライン/消費スレッドがある場合、複数の条件を定義することで、どれをウェイクアップするかを正確に指定できることです。次の例はそれほど複雑ではありません。上記の同期された文言を置き換えるだけです。
val buffer = LinkedList<Data>()
val MAX = 5 //buffer最大size
val lock = ReentrantLock()
val condition = lock.newCondition()
fun produce(data: Data) {
sleep(2000) // mock produce
lock.lock()
while (buffer.size >= 5)
condition.await()
buffer.push(data)
condition.signalAll()
lock.unlock()
}
fun consume() {
lock.lock()
while (buffer.isEmpty())
condition.await()
buffer.removeFirst()
condition.singleAll()
lock.unlock()
sleep(2000) // mock consume
}
BlockingQueue
BlockingQueueが重大な状態に達すると、読み取りと書き込みにより、ロックの解放を待機している現在のスレッドが自動的にブロックされます。これは、この生産/消費シナリオに当然適しています。
val buffer = LinkedBlockingQueue<Data>(5)
fun produce(data: Data) {
sleep(2000) // mock produce
buffer.put(data) //buffer满时自动阻塞
}
fun consume() {
buffer.take() // buffer空时自动阻塞
sleep(2000) // mock consume
}
セマフォ
セマフォは、JUCが提供する共有ロックメカニズムであり、輻輳を制御できます。この機能を使用して、バッファのサイズを制御できます。
// canProduce: 可以生产数量(即buffer可用的数量),生产者调用acquire,减少permit数目
val canProduce = Semaphore(5)
// canConsumer:可以消费数量,生产者调用release,增加permit数目
val canConsume = Semaphore(5)
// 控制buffer访问互斥
val mutex = Semaphore(0)
val buffer = LinkedList<Data>()
fun produce(data: Data) {
if (canProduce.tryAcquire()) {
sleep(2000) // mock produce
mutex.acquire()
buffer.push(data)
mutex.release()
canConsume.release() //通知消费端新增加了一个产品
}
}
fun consume() {
if (canConsume.tryAcquire()) {
sleep(2000) // mock consume
mutex.acquire()
buffer.removeFirst()
mutex.release()
canProduce.release() //通知生产端可以再追加生产
}
}
RxJava
RxJavaは、マルチプロデューサーおよびマルチコンシューマーのシナリオには適していませんが、シングルプロデューサー/コンシューマーのシナリオでは使用できます。
Flowableの圧縮メカニズムを使用して、バッファーの数を制御できます
class Producer : Flowable<Data>() {
override fun subscribeActual(subscriber: org.reactivestreams.Subscriber<in Data>) {
subscriber.onSubscribe(object : Subscription {
override fun cancel() {
//...
}
private val outStandingRequests = AtomicLong(0)
override fun request(n: Long) {
//收到下游通知,开始生产
outStandingRequests.addAndGet(n)
while (outStandingRequests.get() > 0) {
sleep(2000)
subscriber.onNext(Data())
outStandingRequests.decrementAndGet()
}
}
})
}
}
class Consumer : DefaultSubscriber<Data>() {
override fun onStart() {
request(1)
}
override fun onNext(i: Data?) {
sleep(2000) //mock consume
request(1) //通知上游可以增加生产
}
override fun onError(throwable: Throwable) {
//...
}
override fun onComplete() {
//...
}
}
@Test
fun test_rxjava() {
try {
val testProducer = Producer)
val testConsumer = Consumer()
testProducer
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.single())
.blockingSubscribe(testConsumer)
} catch (t: Throwable) {
t.printStackTrace()
}
}
継続/制作
コルティンのチャンネルには、生産者と消費者の間のコミュニケーションを実現できる混雑制御メカニズムがあります。
特にそのようなシナリオに対処するための、チャネルに基づくプロデュースメソッドの公式カプセル化
val produce = produce(Dispatchers.IO, 5) {
repeat(10) {
sleep(2000) //mock produce
val data = Data()
send(data) // 超过5个以上没有消费完时,协程挂起
}
}
produce.consumeEach {
sleep(3000) //mock consume
}
概要
スレッドのプロデューサーとコンシューマーの問題は、両端での通信を処理することに他なりません。ロック、セマフォ、RxJavaのバックプレッシャ、通常のチャネルなどは、すべて代替の通信方法です。