Spec-Zone.ru › Kotlin 2

Проверка и валидация результатов

После выполнения сценариев, сгенерированных для конкурентной структуры данных, Lincheck проверяет результаты в соответствии с заданной моделью проверки (например, линейризуемостью) и при необходимости проверяет конечное состояние структуры данных с помощью предоставленной пользователем функции валидации.

Проверка

В процессе проверки Lincheck пытается найти последовательное выполнение операций конкурентного сценария, которое приводит к тем же результатам, что и конкурентное выполнение:

A diagram of the verification process in Lincheck. Lincheck compares a concurrent execution to different
sequential executions.

В зависимости от модели проверки, могут действовать дополнительные ограничения на последовательное выполнение. Если ни одно последовательное выполнение, соответствующее свойству проверки, не может привести к наблюдаемым результатам, Lincheck сообщает об ошибке.

Последовательная спецификация

По умолчанию в процессе проверки Lincheck строит последовательное выполнение, используя операции конкурентной структуры данных.

Вы можете задать последовательную структуру данных с соответствующими операциями, чтобы:

  • Убедиться, что конкурентная структура данных возвращает те же результаты, что и последовательная.

    Как правило, однопоточные реализации проще потокобезопасных, поэтому их правильность гораздо легче проверить (например, HashMap и ConcurrentHashMap, LinkedList и ConcurrentLinkedQueue).

    Сравнив результаты выполнения двух версий структуры, можно убедиться, что более сложная конкурентная структура ведет себя так же, как и более простая структура в однопоточной среде.

  • Проверить корректность последовательного выполнения и безопасность при конкурентном выполнении в одном тесте.

A diagram of the verification process in Lincheck. Lincheck compares a concurrent execution to
different sequential executions. Sequential executions use the operations of the specified
sequential version of the structure.

Чтобы задать последовательную версию структуры данных:

  1. Реализуйте структуру данных с последовательными версиями всех конкурентных функций, тестируемых Lincheck.

  2. Задайте структуру данных с помощью параметра 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() = { ... }
    

    QuiescentConsistencyVerifier не отслеживает фактические точки покоя. Средство проверки потенциально может не обнаружить ошибки, возникающие на границах точек покоя.

  • SerializabilityVerifier — использует модель сериализуемости, в которой конкурентное выполнение считается допустимым, если существует некоторое последовательное выполнение (в любом порядке), приводящее к тем же результатам, что и конкурентное выполнение, независимо от ограничений «произошло до». Эту модель можно использовать для структур, в которых относительный порядок конкурентных операций не имеет значения.

Сравнение сериализуемости и линейризуемости

Чтобы понять разницу между этими двумя моделями, рассмотрим, как структура данных может быть сериализуемой, но не линейризуемой:

  1. Рассмотрим следующую структуру данных:

    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)
        }
    }
    

    Эта конкурентная структура работает некорректно: она сохраняет элементы, как обычная очередь, но возвращает их в случайном порядке.

  2. Реализуйте последовательную версию очереди, которая правильно сохраняет и возвращает элементы:

    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)
    }
    
  3. Создайте класс теста и объявите операции 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()
    }
  4. Объявите и запустите тест на сериализуемость:

        @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)
    

    Он должен успешно пройти.

  5. Объявите и запустите тест на линейризуемость:

        @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 |          |
    | -------------------- |
    
  6. Проанализируйте результаты.

    Поскольку тест на сериализуемость проходит, существует некоторый последовательный порядок операций SequentialQueue, приводящий к poll(): 1, например:

    An animation of the operations in a two-threaded scenario, reordered into a single-threaded scenario. 
Thread 2 is executed first and has two operations: `put(2), then `put(1). Thread 1 is executed second and 
has two operations: `put(3) then `poll(): 1. The single-threaded scenario has the operations in the 
following order: `put(1), `put(2), `put(3), `poll(): 1. Because `put(1) is now the first operation, 
`poll() returns the value `1 correctly.

    Однако линейризуемость накладывает дополнительные ограничения на порядок операций: если операция A завершается до начала операции B в конкурентном выполнении, то A должна выполняться до B в последовательном выполнении. Пример линейризуемого выполнения выглядит так:

    An animation of the operations in a two-threaded scenario, reordered into a single-threaded scenario. 
Thread 1 is executed first and has two operations: `put(1), then `put(2). Thread 2 is executed second and 
has two operations: `poll(): 1 then `poll(): 2. The single-threaded scenario has the operations in the 
following order: `put(1), `put(2), `poll(): 1, `poll(): 2. All `poll() operations return the correct 
values.

    Поскольку 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}" }
    }

Функция валидации должна:

  • Не принимать аргументы.

  • Выбрасывать исключение, если структура данных находится в недопустимом состоянии.

Что дальше

  • Настройка ограничений генерации аргументов

  • Настройка выполнения операций

  • Проверка гарантий неблокирующего прогресса

15 августа 2026 г.
Гарантии прогрессаПроверка модели

© 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API