开启掘金成长之旅!这是我参与「掘金日新计划 · 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))
}
这里的问题是,如果produceTo和consumeFrom函数实际上是异步运行的,我的测试就会不稳定,因为在我调用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))
}
一种同步辅助工具,允许一个或多个线程等待,直到其他线程中执行的一组操作完成。
A
CountDownLatch用给定的计数初始化。该await方法会阻塞,直到当前计数由于调用该countDown方法而变为零,之后所有等待的线程都会被释放,任何后续的awaitreturn 调用都会立即执行。
我的用例似乎非常适合 a CountDownLatch,但是,ZIO 没有提供现成的。它确实提供了我们可以在其上构建的其他原语,例如Promise、Ref、和其他。因此,让我们尝试使用 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我们用很少的代码创建了一个简单且非阻塞的,这对于使我们的测试套件更健壮很有价值。