임의의 코드 테스트하기
임의의 코드 테스트하기
Lincheck는 임의의 동시성 코드를 테스트할 수 있는 runConcurrentTest() 함수를 제공해요. runConcurrentTest() 함수는 동시성 코드 블록을 여러 번 실행하고, 모델 체킹(model checking)을 사용해 가능한 실행 스케줄을 탐색해요.
본문
Lincheck로 동시성 코드를 테스트하려면:
- 테스트 클래스를 만들어요:
class CounterTestWithInvocations {
}
runConcurrentTest()를 사용해 테스트 함수를 멤버 함수로 만들어요:
@Test
fun test() = Lincheck.runConcurrentTest(100_000) {
var counter = 0
// Increments the counter concurrently
val t1 = thread { counter++ }
val t2 = thread { counter++ }
// Waits for the threads to finish
t1.join()
t2.join()
// Checks that both increments have been applied
assertEquals(2, counter)
}
함수 매개변수는 선택 사항이며, 탐색할 실행 스케줄의 수를 지정해요. 기본값은 10_000이에요.
- 테스트를 실행해요. 실패하면 Lincheck는 잘못된 동작으로 이어지는 실행 스케줄이 담긴 보고서를 생성해요:
| ------------------------------------------------------------------------------- |
| Main Thread | Thread 1 | Thread 2 |
| ------------------------------------------------------------------------------- |
| thread(block = Lambda#2): Thread#1 | | |
| thread(block = Lambda#3): Thread#2 | | |
| switch (reason: waiting for Thread 1 to finish) | | |
| | | run() |
| | | counter ➜ 0 |
| | | switch |
| | run() | |
| | counter ➜ 0 | |
| | counter = 1 | |
| | | counter = 1 |
| Thread#1.join() | | |
| Thread#2.join() | | |
| counter.element ➜ 1 | | |
| assertEquals(2, 1): threw AssertionFailedError | | |
| ------------------------------------------------------------------------------- |
예: ConcurrentHashMap 함수 테스트하기
ConcurrentHashMap 함수를 위한 다음 테스트를 살펴봐요:
package org.lincheck.docs
import org.jetbrains.lincheck.*
import kotlin.test.Test
import java.util.concurrent.*
import kotlin.concurrent.*
// This test demonstrates a deadlock caused by two threads
// performing nested `computeIfAbsent` calls in opposite order.
class ConcurrentHashMapDeadlockTest {
@Test
fun test() = Lincheck.runConcurrentTest {
val map = ConcurrentHashMap<String, String>()
// Updates `key2` while locking `key1`.
val thread1 = thread {
map.computeIfAbsent("key1") {
map.computeIfAbsent("key2") { "value2" }
"value1"
}
}
// Updates `key1` while locking `key2`.
val thread2 = thread {
map.computeIfAbsent("key2") {
map.computeIfAbsent("key1") { "value1" }
"value2"
}
}
// Wait until both threads complete.
thread1.join()
thread2.join()
}
}
이 테스트는 교착 상태(deadlock)로 이어지는 실행 스케줄을 Lincheck가 찾아내서 실패해요:
- Thread 2는 key2를 인덱스 1의 버킷에 매핑하고, 이 버킷에 락을 걸고
computeIfAbsent("key1")실행을 시작해요. Thread 2가 key1을 매핑하고 key1이 있는 버킷을 잠그기 전에 실행이 Thread 2에서 Thread 1로 전환돼요. - Thread 1은 key1을 인덱스 0의 버킷에 매핑하고, 이 버킷에 락을 걸고
computeIfAbsent("key2")실행을 시작해요. Thread 1은 key2를 인덱스 1의 버킷에 매핑하고 그 버킷을 잠그려 하지만, 이미 Thread 2가 잠가 놓았어요. 실행이 Thread 1에서 Thread 2로 전환돼요. - Thread 2는 key1이 있는 버킷을 잠그려 하지만, 이미 Thread 1이 잠가 놓았어요.
두 스레드 모두 잠겨 있으므로, 실행은 교착 상태를 만나게 돼요.
다음 단계
Lincheck를 사용해 데이터 구조를 테스트하는 방법을 알아보세요.
더 알아보기
- Lincheck의 모델 체킹 (Model checking in Lincheck)
- Kotlin Multiplatform 프로젝트에서의 Lincheck (Lincheck in Kotlin Multiplatform projects)