Проверка и валидация результатов
После выполнения сценариев, сгенерированных для конкурентной структуры данных, Lincheck проверяет результаты в соответствии с заданной моделью проверки (например, линейризуемостью) и при необходимости проверяет конечное состояние структуры данных с помощью предоставленной пользователем функции валидации.
Проверка
В процессе проверки Lincheck пытается найти последовательное выполнение операций конкурентного сценария, которое приводит к тем же результатам, что и конкурентное выполнение:
В зависимости от модели проверки, могут действовать дополнительные ограничения на последовательное выполнение. Если ни одно последовательное выполнение, соответствующее свойству проверки, не может привести к наблюдаемым результатам, Lincheck сообщает об ошибке.
Последовательная спецификация
По умолчанию в процессе проверки Lincheck строит последовательное выполнение, используя операции конкурентной структуры данных.
Вы можете задать последовательную структуру данных с соответствующими операциями, чтобы:
-
Убедиться, что конкурентная структура данных возвращает те же результаты, что и последовательная.
Как правило, однопоточные реализации проще потокобезопасных, поэтому их правильность гораздо легче проверить (например,
HashMapиConcurrentHashMap,LinkedListиConcurrentLinkedQueue).Сравнив результаты выполнения двух версий структуры, можно убедиться, что более сложная конкурентная структура ведет себя так же, как и более простая структура в однопоточной среде.
Проверить корректность последовательного выполнения и безопасность при конкурентном выполнении в одном тесте.
Чтобы задать последовательную версию структуры данных:
Реализуйте структуру данных с последовательными версиями всех конкурентных функций, тестируемых Lincheck.
-
Задайте структуру данных с помощью параметра
sequentialSpecification():@Test fun stressTest() = StressOptions() .sequentialSpecification(SequentialQueue::class.java) .check(this::class)
Пример теста Lincheck, в котором однопоточный LinkedList используется в качестве последовательной спецификации для ConcurrentLinkedQueue:
class ConcurrentLinkedQueueTest {
private val s = ConcurrentLinkedQueue<Int>()
@Operation
fun add(value: Int) = s.add(value)
@Operation
fun poll(): Int? = s.poll()
@Test
fun stressTest() = StressOptions()
.sequentialSpecification(SequentialQueue::class.java)
.check(this::class)
}
class SequentialQueue {
private val s = LinkedList<Int>()
fun add(x: Int) = s.add(x)
fun poll(): Int? = s.poll()
}
Модели проверки
По умолчанию Lincheck проверяет результаты конкурентного выполнения на соответствие модели линейризуемости. Чтобы применить другую модель проверки, используйте параметр verifier:
@Test
fun customVerifierTest() = ModelCheckingOptions()
.verifier(SerializabilityVerifier::class.java)
.check(this::class.java)
Lincheck предоставляет следующие классы средств проверки:
LinearizabilityVerifier— параметр по умолчанию. Конкурентное выполнение считается допустимым, если существует последовательное выполнение, сохраняющее отношения «произошло до» между операциями конкурентного выполнения.-
QuiescentConsistencyVerifier— использует модель квази-согласованности, которая работает аналогично модели линейризуемости, но ограничения «произошло до» не применяются к операциям, аннотированным с помощью@QuiescentConsistent:@Operation @QuiescentConsistent fun someOperation() = { ... } SerializabilityVerifier— использует модель сериализуемости, в которой конкурентное выполнение считается допустимым, если существует некоторое последовательное выполнение (в любом порядке), приводящее к тем же результатам, что и конкурентное выполнение, независимо от ограничений «произошло до». Эту модель можно использовать для структур, в которых относительный порядок конкурентных операций не имеет значения.
Сравнение сериализуемости и линейризуемости
Чтобы понять разницу между этими двумя моделями, рассмотрим, как структура данных может быть сериализуемой, но не линейризуемой:
-
Рассмотрим следующую структуру данных:
class ConcurrentQueue { private val elements: MutableList<Int> = ArrayList() fun put(x: Int) = synchronized(this) { elements += x } fun poll(): Int? = synchronized(this) { if (elements.isEmpty()) return null elements.shuffle() elements.removeAt(0) } }Эта конкурентная структура работает некорректно: она сохраняет элементы, как обычная очередь, но возвращает их в случайном порядке.
-
Реализуйте последовательную версию очереди, которая правильно сохраняет и возвращает элементы:
class CorrectSequentialQueue { private val elements: MutableList<Int> = ArrayList() fun put(x: Int) { elements += x } fun poll(): Int? = if (elements.isEmpty()) null else elements.removeAt(0) } -
Создайте класс теста и объявите операции
put()иpoll():@Param(name = "value", gen = IntGen::class, conf = "1:2") class ConcurrentQueueTest { private val q = ConcurrentQueue() @Operation fun put(@Param(name = "value") x: Int) = q.put(x) @Operation fun poll(): Int? = q.poll() } -
Объявите и запустите тест на сериализуемость:
@Test fun serializabilityTest() = ModelCheckingOptions() .actorsBefore(0) .actorsAfter(0) .actorsPerThread(2) .threads(2) // Use the `SerializabilityVerifier` .verifier(SerializabilityVerifier::class.java) // Specify the sequential version of the structure .sequentialSpecification(CorrectSequentialQueue::class.java) .check(this::class.java)Он должен успешно пройти.
-
Объявите и запустите тест на линейризуемость:
@Test fun linearizabilityTest() = ModelCheckingOptions() .actorsBefore(0) .actorsAfter(0) .actorsPerThread(2) .threads(2) // Show the full failed scenario .minimizeFailedScenario(false) // Specify the sequential version of the structure .sequentialSpecification(CorrectSequentialQueue::class.java) .check(this::class.java)Тест должен завершиться с ошибкой и выдать следующий отчет:
| -------------------- | | Thread 1 | Thread 2 | | -------------------- | | | put(2) | | | put(1) | | put(3) | | | poll(): 1 | | | -------------------- |
-
Проанализируйте результаты.
Поскольку тест на сериализуемость проходит, существует некоторый последовательный порядок операций
SequentialQueue, приводящий кpoll(): 1, например:Однако линейризуемость накладывает дополнительные ограничения на порядок операций: если операция
Aзавершается до начала операцииBв конкурентном выполнении, тоAдолжна выполняться доBв последовательном выполнении. Пример линейризуемого выполнения выглядит так:Поскольку Lincheck не может менять порядок операций
put()при проверке, он не может найти последовательное выполнение, удовлетворяющее ограничениям линейризуемости. Поэтому тест завершается с ошибкой.
Валидация
По умолчанию Lincheck не проверяет состояние конкурентной структуры данных после выполнения сгенерированного сценария. Чтобы проверить конечное состояние, используйте аннотацию @Validate для функции валидации в классе теста:
@Validate
fun validate() {
// Check some property of the data structure
// Throw an exception if the check is violated
check(storage.size >= 2) { "Size must be at least 2, but was ${storage.size}" }
}
Функция валидации должна:
Не принимать аргументы.
Выбрасывать исключение, если структура данных находится в недопустимом состоянии.
Что дальше
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/lincheck-results-validation.html

