我们如何在Kotlin协程中测试并发基元

2021-02-26 10:18:54

我们的许多用户对使用协程编写异步代码的经验感到高兴。这不足为奇,因为使用协程,我们能够编写简单易懂的代码,而几乎所有异步都在后台进行。为简单起见,我们可以将Kotlin中的协程视为具有某些附加功能的超轻量级线程,例如可取消性和结构化并发。但是,协程也使代码更安全。传统的并发编程涉及操纵一个共享的可变状态,这可以说是容易出错的。作为替代,协程可以提供特殊的通信原语(例如Channel)来执行同步。使用渠道和其他几个原语作为构建块,我们可以构建功能非常强大的东西,例如最近推出的Flows。但是,没有什么是免费的。尽管消息传递方法通常更安全,但是实现所需的原语并不是一件容易的事,因此必须尽可能全面地对其进行测试。同时,测试并发代码可能和编写它一样复杂。

这就是为什么我们拥有Lincheck –一种用于在JVM上测试并发数据结构的特殊框架。该框架的主要优点在于,它提供了一种简单且声明性的方式来编写并发测试。您无需声明如何执行测试,而是通过声明所有要检查的操作以及所需的正确性属性(线性化是默认值,但也支持各种扩展)和限制(例如“单用户”)来指定要测试的内容”)。

让我们考虑一个简单的并发数据结构,例如下面介绍的堆栈算法。除了标准的push(value)和pop()操作之外,我们还实现了不可线性化(因此是不正确的)size(),它在成功执行push和pop调用后增加和减少了相应的字段。

类Stack< T> { 私有val top =原子< Node< T>?>(null) 私有val _size =原子(0) fun push(value:T):Unit = top.loop {cur-> val newTop =节点(cur,value) if(top.compareAndSet(cur,newTop)){//尝试添加 _size.incrementAndGet()//<-增量大小 返回 } } 有趣的pop():是吗? = top.loop {cur-> if(cur == null)返回null //堆栈为空吗? if(top.compareAndSet(cur,cur.next)){//尝试检索 _size.decrementAndGet()//<-DECREMENT SIZE 返回当前值 } } val大小:Int get()= _size.value } 类Node< T>(val下一个:Node< T>?,val值:T)

要在没有任何工具的情况下为此堆栈编写并发测试,您必须手动运行并行线程,调用堆栈操作,最后检查一些顺序历史记录可以解释所获得的结果。过去我们使用过这种手动测试,并且所有这些测试至少包含一百行样板代码。但是使用Lincheck,该机器是自动化的,并且测试变得简短而有用。

要使用Lincheck编写并发测试,您需要列出数据结构操作,并使用特殊的@Operation批注对其进行标记。初始状态在构造函数中指定(在此,我们创建一个新的TriebeStack< Int>实例)。之后,我们需要配置测试模式,可以使用测试类上的特殊注释来完成测试– @StressCTest用于压力测试,@ModelCheckingCTest用于模型检查模式。最后,我们可以通过在测试类上调用Linchecker.check(..)函数来运行分析。

@StressCTest @ModelCheckingCTest 类StackTest { 私有值s = TrieberStack< Int>() @Operation fun push(值:整数)= s.push(值) @Operation fun pop()= s.pop() @Operation fun size()= s.size @Test fun runTest()= LinChecker.check(this :: class) }