如何使用 ZIO 创建一个简单的 CountDownLatch

150 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情

在本文中,我将解释 aCountDownLatch是什么,以及ZIO如何使您能够创建高效、非阻塞且简单的并发原语。

背景

我最近需要编写一个测试,该测试涉及在进行断言之前等待调用多个异步回调。更准确地说,我正在测试的系统涉及异步生产者/消费者(或发布/订阅)模型,测试看起来像这样(使用zio-test):

testM(“消费产生的记录”){ 
  for { 
    records <- Ref. make ( Set .empty[Record]) 
    _ <- consumeFrom( topic1) { record => records.update 
      (_ + record) 
    }.fork 
    _ <- consumeFrom(topic2) { record => records.update ( 
      _ + record) 复制代码
    }.fork 
    _ <- produceTo(topic1 -> record1, topic2 -> record2) 
    consumed <- records.get 
  } yield assert (consumed, contains (record1) && contains (record2)) 
}

这里的问题是,如果produceToconsumeFrom函数实际上是异步运行的,我的测试就会不稳定,因为在我调用records.get时结果可能仍然是空的。

我们不喜欢不稳定的测试。简单的解决方案是轮询,或者以某个固定的时间间隔重试断言,直到成功。ZIO 为此类用例提供了很棒的工具,例如repeat组合器:

testM(“消费产生的记录”){ 
  for { 
    records <- Ref.make(Set.empty[Record]) 
    _ <- consumeFrom(topic1) { record => records.update 
      (_ + record) 
    }.fork 
    _ <- consumeFrom(topic2) { record => records.update 
      (_ + record) 
    }.fork 
    _ <- produceTo(topic1 -> record1, topic2 -> record2) 
 (_, consumed) <- records.get.repeat( 
      Schedule.spaced (100.millis) && 
        Schedule.doUntil(_.size == 2)) 
   } yield assert(consumed, contains(record1) && contains(record2)) 
}

一种同步辅助工具,允许一个或多个线程等待,直到其他线程中执行的一组操作完成。

ACountDownLatch用给定的计数初始化。该await方法会阻塞,直到当前计数由于调用该countDown方法而变为零,之后所有等待的线程都会被释放,任何后续的awaitreturn 调用都会立即执行。

我的用例似乎非常适合 a CountDownLatch,但是,ZIO 没有提供现成的。它确实提供了我们可以在其上构建的其他原语,例如PromiseRef、和其他。因此,让我们尝试使用 ZIO创建我们自己的并发和非阻塞!Queue``Semaphore``CountDownLatch

应用程序接口

与 Java 相比,我们需要的 API 更简单CountDownLatch,看起来像这样:

trait CountDownLatch {  
def countDown: UIO[Unit]  
def await: UIO[Unit]  
}

我们可以省略一些原来的方法,比如awaitwith timeout:

public boolean await(long timeout, TimeUnit unit)  
throws InterruptedException

因为我们可以使用 ZIO 的内置组合器轻松实现这些语义,这展示了拥有可组合的函数式 API 的强大功能:

latch.await.timeout(1.second)

对于实际的实现,我们只需要 aRef保持当前计数,并Promise在计数达到 0 时发出信号。创建 aRef和 aPromise是一个有效的操作,这意味着创建我们的CountDownLatchwill 也是有效的(这是有道理的,因为它保持内部状态)。

object CountDownLatch {  
def make(count: Int): UIO[CountDownLatch] = for {  
ready <- Promise. *制作*[Nothing, Unit]  
ref <- Ref. *make* (count)  
} yield new CountDownLatch {  
override def countDown: UIO[Unit] =  
ref.update(_ - 1).flatMap {  
案例 0 => ready.succeed()).unit  
case _ => ZIO。*unit  
*}  
  
覆盖 def await: UIO[Unit] = ready.await  
}  
}

只需几行代码,我们就创建了自己的CountDownLatch!让我们使用我们的新实用程序重写我们的原始测试:

*testM*(“消费产生的记录”){  
for {  
records <- Ref. *make* ( *Set* .empty[Record])  
latch <- CountDownLatch。*make* (2)  
_ <- consumeFrom(topic1) { record => records.update  
(_ + record) *> **latch.countDown**  
}.fork  
_ <- consumeFrom(topic2) { record => records.update (  
_ + record) 复制代码*> **latch.countDown**  
}.fork  
_ <- produceTo(topic1 -> record1, topic2 -> record2)  
_ <- **latch.await**  
消耗 < - records.get  
} yield *assert* (consumed, *contains* (record1) &&**(记录 2))  
}

现在我们可以轻松地同步我们的测试行为,从而获得更可靠和一致的测试套件。

结论

ZIO 在编写异步和并发代码时再次证明了它的价值。CountDownLatch我们用很少的代码创建了一个简单且非阻塞的,这对于使我们的测试套件更健壮很有价值。