Как мне выполнить модульное тестирование многопоточного кода?

До сих пор я избегал кошмара, связанного с тестированием многопоточного кода, поскольку это кажется слишком большим минным полем. Я хотел бы спросить, как люди приступили к тестированию кода, который полагается на потоки для успешного выполнения, или как люди приступили к тестированию таких проблем, которые проявляются только тогда, когда два потока взаимодействуют определенным образом?

Это кажется действительно ключевой проблемой для программистов сегодня, было бы полезно объединить наши знания об этом imho.

Я думал о том, чтобы задать вопрос по этой же проблеме. Хотя Уилл приводит много хороших моментов ниже, я думаю, что мы можем добиться большего. Я согласен, что не существует единого «подхода» к тому, чтобы справиться с этим чисто. Однако «тестирование как можно лучше» устанавливает очень низкую планку. Я вернусь со своими выводами.

Zach Burlingame 24.09.2008 23:10

Не могли бы вы дать ссылку на свой предыдущий вопрос, связанный с модульным тестированием?

Andrew Grimm 06.05.2010 07:55

@ Эндрю Гримм: stackoverflow.com/questions/11060/…

jkp 06.05.2010 10:58
stackoverflow.com/questions/4418373/…
Toby 26.08.2011 18:30

Я думаю, важно отметить, что этому вопросу 8 лет, а библиотеки приложений за это время прошли довольно долгий путь. В «современную эпоху» (2016 г.) многопоточная разработка возникает в основном во встроенных системах. Но если вы работаете над настольным или телефонным приложением, сначала изучите альтернативы. Среды приложений, такие как .NET, теперь включают инструменты для управления или значительного упрощения, вероятно, 90% распространенных сценариев многопоточности. (asnync / await, PLinq, IObservable, TPL ...). Многопоточный код сложен. Если вы не изобретаете велосипед, вам не нужно его повторно тестировать.

Paul Williams 11.05.2016 18:06

В Java: пакет java.util.concurrent содержит несколько плохо известных классов, которые могут помочь в написании детерминированных тестов JUnit. Посмотрите - CountDownLatch - Семафор - Обменник

Synox 20.01.2010 14:25
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
726
6
172 234
27

Ответы 27

У меня была неудачная задача тестирования многопоточного кода, и это определенно самые сложные тесты, которые я когда-либо писал.

При написании тестов я использовал комбинацию делегатов и событий. По сути, все дело в использовании событий PropertyNotifyChanged с WaitCallback или каким-то ConditionalWaiter, который опрашивает.

Я не уверен, что это был лучший подход, но он сработал для меня.

Действительно крутой! В своих модульных тестах (C++) я разбил это на несколько категорий в соответствии с используемым шаблоном параллелизма:

  1. Модульные тесты для классов, которые работают в одном потоке и не поддерживают потоки - легко, тестируйте как обычно.

  2. Модульные тесты для Мониторинг объектов (те, которые выполняют синхронизированные методы в потоке управления вызывающей стороны), которые предоставляют синхронизированный общедоступный API - создают экземпляры нескольких фиктивных потоков, которые используют API. Постройте сценарии, которые реализуют внутренние условия пассивного объекта. Включите один более продолжительный тест, который в течение длительного периода времени, по сути, справляется с задачей из нескольких потоков. Я знаю, что это ненаучно, но это вселяет уверенность.

  3. Модульные тесты для Активные объекты (те, которые инкапсулируют свой собственный поток или потоки управления) - аналогично пункту 2 выше с вариациями в зависимости от дизайна класса. Общедоступный API может быть блокирующим или неблокирующим, вызывающие абоненты могут получать фьючерсы, данные могут поступать в очереди или должны быть удалены из очереди. Здесь возможно множество комбинаций; белый ящик прочь. По-прежнему требуется несколько фиктивных потоков для выполнения вызовов тестируемого объекта.

Как в сторону:

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

Первые 2 ссылки в ответе мертвы - "404 Не Найдено".

Pang 27.10.2020 03:50

Послушайте, нет простого способа сделать это. Я работаю над многопоточным проектом. События поступают из операционной системы, и я должен обрабатывать их одновременно.

Самый простой способ справиться с тестированием сложного многопоточного кода приложения: если он слишком сложен для тестирования, вы делаете это неправильно. Если у вас есть единственный экземпляр, на который воздействуют несколько потоков, и вы не можете протестировать ситуации, когда эти потоки переступают друг друга, то ваш дизайн необходимо переделать. Это так просто и так сложно.

Есть много способов программирования для многопоточности, которые позволяют избежать одновременного выполнения потоков через экземпляры. Самый простой - сделать все ваши объекты неизменяемыми. Конечно, обычно это невозможно. Таким образом, вы должны определить те места в вашем дизайне, где потоки взаимодействуют с одним и тем же экземпляром, и уменьшить их количество. Таким образом вы изолируете несколько классов, в которых действительно имеет место многопоточность, что снизит общую сложность тестирования вашей системы.

Но вы должны понимать, что даже делая это, вы все равно не можете протестировать каждую ситуацию, когда два потока наступают друг на друга. Для этого вам нужно будет запустить два потока одновременно в одном и том же тесте, а затем точно контролировать, какие строки они выполняют в любой момент. Лучшее, что вы можете сделать, - это смоделировать эту ситуацию. Но это может потребовать от вас написания кода специально для тестирования, а это в лучшем случае полшага к истинному решению.

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

Посмотрите, как обстоят дела в настоящее время (и, вероятно, так и будет в будущем), лучший способ протестировать многопоточные приложения - это максимально снизить сложность многопоточного кода. Сведите к минимуму области, в которых взаимодействуют потоки, проверьте как можно лучше и используйте анализ кода для выявления опасных областей.

Анализ кода хорош, если вы имеете дело с языком / фреймворком, который это позволяет. EG: Findbugs обнаружит очень простые и легкие проблемы общего параллелизма со статическими переменными. Чего он не может найти, так это одноэлементных шаблонов проектирования, он предполагает, что все объекты могут быть созданы несколько раз. Этот плагин совершенно не подходит для таких фреймворков, как Spring.

Zombies 28.08.2013 03:38

ThreadSafe (contemplateltd.com/threadsafe, доступны бесплатные пробные версии) - это статический анализатор, который специализируется именно на параллелизме Java. Он намного лучше, чем FindBugs, при обнаружении проблем параллелизма. См. В infoq.com/articles/… примеры ошибок параллелизма, которые он обнаруживает в приложениях с открытым исходным кодом, включая Apache JMeter и K9Mail. (Раскрытие информации: ThreadSafe - это коммерческий инструмент, и я соучредитель Contemplate, компании, которая его производит.)

dsannella 17.06.2014 01:37

@DonSannella: (Раскрытие информации: у нас есть ограничения для саморекламы)

user1228 17.06.2014 16:37

на самом деле есть лекарство: активные объекты. drdobbs.com/parallel/prefer-using-active-objects-instead-of-‌ н /…

Dill 08.01.2015 22:49

Хотя это хороший совет, я все еще спрашиваю: «Как мне протестировать те минимальные области, где требуется несколько потоков?»

Bryan Rayner 27.05.2016 23:29

«Если это слишком сложно для тестирования, значит, вы делаете это неправильно» - нам всем придется погрузиться в унаследованный код, который мы не писали. Чем именно это наблюдение кому-то помогает?

Ronna 12.09.2016 14:49

Статический анализ, вероятно, хорошая идея, но это не проверка. Этот пост действительно не отвечает на вопрос о том, как тестировать.

Warren Dew 19.10.2016 04:25

Очень хорошо сказано. Добавление: если вам удастся сохранить дизайн, дружественный к тестам, лучший способ написать тесты - это имитировать планировщик и сериализовать инструкции для случаев, которые будут встречены в дикой природе. Меньше случаев = меньше тестов = надежный код.

Yarek T 08.01.2021 16:17

Пит Гудлифф имеет серию по коду модульное тестирование резьбовых.

Это тяжело. Я выбираю более простой выход и стараюсь, чтобы код потоковой передачи был абстрагирован от фактического теста. Пит действительно упоминает, что я поступаю неправильно, но либо я правильно разделился, либо мне просто повезло.

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

Don Kirkby 19.09.2008 00:19

У меня также были серьезные проблемы с тестированием многопоточного кода. Затем я нашел действительно классное решение в «xUnit Test Patterns» Джерарда Месароса. Шаблон, который он описывает, называется Скромный объект.

По сути, он описывает, как вы можете выделить логику в отдельный, простой для тестирования компонент, отделенный от его среды. После того, как вы проверили эту логику, вы можете протестировать сложное поведение (многопоточность, асинхронное выполнение и т. д.)

Мне нравится писать два или более тестовых метода для выполнения в параллельных потоках, и каждый из них выполняет вызовы тестируемого объекта. Я использовал вызовы Sleep () для координации порядка вызовов из разных потоков, но это не совсем надежно. Это также намного медленнее, потому что вам нужно спать достаточно долго, чтобы время обычно работало.

Я нашел Многопоточная библиотека TC Java из той же группы, которая написала FindBugs. Он позволяет указать порядок событий без использования Sleep (), и это надежно. Еще не пробовал.

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

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

Обновлять: Я немного поиграл с библиотекой Multithreaded TC Java, и она работает хорошо. Я также перенес некоторые из его функций в версию .NET, которую я назвал TickingTest.

Для Java ознакомьтесь с главой 12 JCIP. Есть несколько конкретных примеров написания детерминированных многопоточных модульных тестов, по крайней мере, для проверки правильности и инвариантов параллельного кода.

«Доказать» потокобезопасность с помощью модульных тестов гораздо сложнее. Я считаю, что для этого лучше использовать автоматическое интеграционное тестирование на различных платформах / конфигурациях.

Я сделал много этого, и да, это отстой.

Несколько советов:

  • GroboUtils для запуска нескольких тестовых потоков
  • alphaWorks ConTest для инструментальных классов, чтобы вызвать изменение чередования между итерациями
  • Создайте поле throwable и проверьте его в tearDown (см. Листинг 1). Если вы поймаете плохое исключение в другом потоке, просто назначьте его throwable.
  • Я создал класс utils в листинге 2 и нашел его бесценным, особенно waitForVerify и waitForCondition, которые значительно увеличат производительность ваших тестов.
  • Используйте AtomicBoolean в своих тестах. Он потокобезопасен, и вам часто понадобится последний ссылочный тип для хранения значений из классов обратного вызова и т.п. См. Пример в листинге 3.
  • Убедитесь, что у вашего теста тайм-аут (например, @Test(timeout=60*1000)), поскольку тесты параллелизма иногда могут зависать навсегда, когда они сломаны.

Листинг 1:

@After
public void tearDown() {
    if ( throwable != null )
        throw throwable;
}

Листинг 2:

import static org.junit.Assert.fail;
import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Random;
import org.apache.commons.collections.Closure;
import org.apache.commons.collections.Predicate;
import org.apache.commons.lang.time.StopWatch;
import org.easymock.EasyMock;
import org.easymock.classextension.internal.ClassExtensionHelper;
import static org.easymock.classextension.EasyMock.*;

import ca.digitalrapids.io.DRFileUtils;

/**
 * Various utilities for testing
 */
public abstract class DRTestUtils
{
    static private Random random = new Random();

/** Calls {@link #waitForCondition(Integer, Integer, Predicate, String)} with
 * default max wait and check period values.
 */
static public void waitForCondition(Predicate predicate, String errorMessage) 
    throws Throwable
{
    waitForCondition(null, null, predicate, errorMessage);
}

/** Blocks until a condition is true, throwing an {@link AssertionError} if
 * it does not become true during a given max time.
 * @param maxWait_ms max time to wait for true condition. Optional; defaults
 * to 30 * 1000 ms (30 seconds).
 * @param checkPeriod_ms period at which to try the condition. Optional; defaults
 * to 100 ms.
 * @param predicate the condition
 * @param errorMessage message use in the {@link AssertionError}
 * @throws Throwable on {@link AssertionError} or any other exception/error
 */
static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms, 
    Predicate predicate, String errorMessage) throws Throwable 
{
    waitForCondition(maxWait_ms, checkPeriod_ms, predicate, new Closure() {
        public void execute(Object errorMessage)
        {
            fail((String)errorMessage);
        }
    }, errorMessage);
}

/** Blocks until a condition is true, running a closure if
 * it does not become true during a given max time.
 * @param maxWait_ms max time to wait for true condition. Optional; defaults
 * to 30 * 1000 ms (30 seconds).
 * @param checkPeriod_ms period at which to try the condition. Optional; defaults
 * to 100 ms.
 * @param predicate the condition
 * @param closure closure to run
 * @param argument argument for closure
 * @throws Throwable on {@link AssertionError} or any other exception/error
 */
static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms, 
    Predicate predicate, Closure closure, Object argument) throws Throwable 
{
    if ( maxWait_ms == null )
        maxWait_ms = 30 * 1000;
    if ( checkPeriod_ms == null )
        checkPeriod_ms = 100;
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    while ( !predicate.evaluate(null) ) {
        Thread.sleep(checkPeriod_ms);
        if ( stopWatch.getTime() > maxWait_ms ) {
            closure.execute(argument);
        }
    }
}

/** Calls {@link #waitForVerify(Integer, Object)} with <code>null</code>
 * for {@code maxWait_ms}
 */
static public void waitForVerify(Object easyMockProxy)
    throws Throwable
{
    waitForVerify(null, easyMockProxy);
}

/** Repeatedly calls {@link EasyMock#verify(Object[])} until it succeeds, or a
 * max wait time has elapsed.
 * @param maxWait_ms Max wait time. <code>null</code> defaults to 30s.
 * @param easyMockProxy Proxy to call verify on
 * @throws Throwable
 */
static public void waitForVerify(Integer maxWait_ms, Object easyMockProxy)
    throws Throwable
{
    if ( maxWait_ms == null )
        maxWait_ms = 30 * 1000;
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    for(;;) {
        try
        {
            verify(easyMockProxy);
            break;
        }
        catch (AssertionError e)
        {
            if ( stopWatch.getTime() > maxWait_ms )
                throw e;
            Thread.sleep(100);
        }
    }
}

/** Returns a path to a directory in the temp dir with the name of the given
 * class. This is useful for temporary test files.
 * @param aClass test class for which to create dir
 * @return the path
 */
static public String getTestDirPathForTestClass(Object object) 
{

    String filename = object instanceof Class ? 
        ((Class)object).getName() :
        object.getClass().getName();
    return DRFileUtils.getTempDir() + File.separator + 
        filename;
}

static public byte[] createRandomByteArray(int bytesLength)
{
    byte[] sourceBytes = new byte[bytesLength];
    random.nextBytes(sourceBytes);
    return sourceBytes;
}

/** Returns <code>true</code> if the given object is an EasyMock mock object 
 */
static public boolean isEasyMockMock(Object object) {
    try {
        InvocationHandler invocationHandler = Proxy
                .getInvocationHandler(object);
        return invocationHandler.getClass().getName().contains("easymock");
    } catch (IllegalArgumentException e) {
        return false;
    }
}
}

Листинг 3:

@Test
public void testSomething() {
    final AtomicBoolean called = new AtomicBoolean(false);
    subject.setCallback(new SomeCallback() {
        public void callback(Object arg) {
            // check arg here
            called.set(true);
        }
    });
    subject.run();
    assertTrue(called.get());
}

Тайм-аут - хорошая идея, но если время теста истекло, любые более поздние результаты в этом прогоне являются подозрительными. В тесте с тайм-аутом все еще могут быть запущены некоторые потоки, которые могут вас испортить.

Don Kirkby 20.10.2010 09:59

Другой способ (вроде) тестирования многопоточного кода и очень сложных систем в целом - это Fuzz-тестирование. Это не очень хорошо, и он не найдет всего, но, вероятно, будет полезен и его просто сделать.

Цитировать:

Fuzz testing or fuzzing is a software testing technique that provides random data("fuzz") to the inputs of a program. If the program fails (for example, by crashing, or by failing built-in code assertions), the defects can be noted. The great advantage of fuzz testing is that the test design is extremely simple, and free of preconceptions about system behavior.

...

Fuzz testing is often used in large software development projects that employ black box testing. These projects usually have a budget to develop test tools, and fuzz testing is one of the techniques which offers a high benefit to cost ratio.

...

However, fuzz testing is not a substitute for exhaustive testing or formal methods: it can only provide a random sample of the system's behavior, and in many cases passing a fuzz test may only demonstrate that a piece of software handles exceptions without crashing, rather than behaving correctly. Thus, fuzz testing can only be regarded as a bug-finding tool rather than an assurance of quality.

Этот вопрос был опубликован некоторое время, но до сих пор нет ответа ...

Ответ kleolb02 хороший. Я постараюсь вдаваться в подробности.

Есть способ, который я практикую для кода C#. Для модульных тестов вы должны уметь программировать тесты воспроизводимый, что является самой большой проблемой в многопоточном коде. Итак, мой ответ направлен на принудительное использование асинхронного кода в тестовой системе, которая работает синхронно.

Это идея из книги Джерарда Месардоса «Тестовые шаблоны xUnit» и называется «Скромный объект» (стр. 695): вы должны отделить основной логический код и все, что пахнет асинхронным кодом, друг от друга. Это приведет к классу основной логики, который работает синхронно.

Это дает вам возможность протестировать код базовой логики способом синхронный. У вас есть полный контроль над временем вызовов, которые вы выполняете в основной логике, и, таким образом, вы можете выполнять тесты воспроизводимый. И это ваша выгода от разделения базовой логики и асинхронной логики.

Эту базовую логику необходимо обернуть другим классом, который отвечает за асинхронный прием вызовов базовой логики и делегаты этих вызовов базовой логики. Производственный код будет обращаться только к основной логике через этот класс. Поскольку этот класс должен делегировать только вызовы, это очень «тупой» класс без особой логики. Таким образом, вы можете сохранить как минимум свои модульные тесты для этого асинхронного рабочего класса.

Все, что выше этого (тестирование взаимодействия между классами), является тестами компонентов. Также в этом случае у вас должен быть полный контроль над временем, если вы будете придерживаться шаблона «Скромный объект».

Но иногда, если потоки хорошо взаимодействуют друг с другом, тоже что-то нужно тестировать, не так ли? Я определенно отделю основную логику от асинхронной части после прочтения вашего ответа. Но я все еще собираюсь протестировать логику через асинхронные интерфейсы с обратным вызовом work-on-all-thread-made-made.

CopperCash 06.04.2015 10:17

Кажется, это отлично подходит для однопоточных программ и алгоритмов, которые имеют некоторую форму параллелизма, но на самом деле не взаимодействуют друг с другом. Я не думаю, что это сработает при тестировании действительно параллельного алгоритма.

Nicolas Bousquet 25.07.2020 21:24

Большую часть прошлой недели я провел в университетской библиотеке, изучая отладку параллельного кода. Основная проблема заключается в том, что параллельный код не является детерминированным. Обычно академическая отладка попадает в один из трех лагерей:

  1. Отслеживание событий / воспроизведение. Для этого требуется монитор событий, а затем просмотр отправленных событий. В среде UT это будет включать отправку событий вручную как часть теста, а затем выполнение патологоанатомических обзоров.
  2. Сценарий. Здесь вы взаимодействуете с запущенным кодом с набором триггеров. "На x> foo, baz ()". Это можно интерпретировать в среде UT, где у вас есть система времени выполнения, запускающая данный тест при определенном условии.
  3. Интерактивный. Очевидно, что это не сработает в ситуации автоматического тестирования. ;)

Теперь, как заметили вышеупомянутые комментаторы, вы можете спроектировать свою параллельную систему в более детерминированном состоянии. Однако, если вы этого не сделаете, вы снова вернетесь к проектированию последовательной системы.

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

Удачи и продолжайте работать над проблемой.

Я обрабатываю модульные тесты многопоточных компонентов так же, как и любые модульные тесты, то есть с инверсией структур управления и изоляции. Я занимаюсь разработкой на .Net-арене, и из коробки потоки (среди прочего) очень сложно (я бы сказал, почти невозможно) полностью изолировать.

Поэтому я написал обертки, которые выглядят примерно так (упрощенно):

public interface IThread
{
    void Start();
    ...
}

public class ThreadWrapper : IThread
{
    private readonly Thread _thread;
     
    public ThreadWrapper(ThreadStart threadStart)
    {
        _thread = new Thread(threadStart);
    }

    public Start()
    {
        _thread.Start();
    }
}
    
public interface IThreadingManager
{
    IThread CreateThread(ThreadStart threadStart);
}

public class ThreadingManager : IThreadingManager
{
    public IThread CreateThread(ThreadStart threadStart)
    {
         return new ThreadWrapper(threadStart)
    }
}

Оттуда я могу легко внедрить IThreadingManager в свои компоненты и использовать выбранную мной структуру изоляции, чтобы заставить поток вести себя так, как я ожидал, во время теста.

До сих пор это отлично работало для меня, и я использую тот же подход для пула потоков, вещей в System.Environment, Sleep и т. д. И т. Д.

+1. Жаль, что dotnet Все еще так плохо поддерживает этот подход. Приходится писать оболочки для таких обыденных вещей, как Task.Delay.

Yarek T 08.01.2021 16:20

Есть несколько неплохих инструментов. Вот краткое изложение некоторых из них.

Некоторые хорошие инструменты статического анализа включают FindBugs (дает некоторые полезные подсказки), JLint, Java Pathfinder (JPF и JPF2) и Богор.

МногопоточныйTC - неплохой инструмент динамического анализа (интегрированный в JUnit), где вам нужно настроить свои собственные тестовые примеры.

Интересен Конкурс от IBM Research. Он инструментирует ваш код, вставляя все виды поведения, изменяющие поток (например, сон и выход), чтобы попытаться случайным образом обнаружить ошибки.

ВРАЩЕНИЕ - действительно крутой инструмент для моделирования ваших Java (и других) компонентов, но вам нужна какая-то полезная среда. Его сложно использовать как есть, но он чрезвычайно эффективен, если вы знаете, как его использовать. Многие инструменты используют SPIN под капотом.

MultithreadedTC, вероятно, является наиболее распространенным, но некоторые из перечисленных выше инструментов статического анализа определенно заслуживают внимания.

Ожидание также может быть полезен для написания детерминированных модульных тестов. Это позволяет вам дождаться обновления какого-либо состояния где-то в вашей системе. Например:

await().untilCall( to(myService).myMethod(), greaterThan(3) );

или же

await().atMost(5,SECONDS).until(fieldIn(myObject).ofType(int.class), equalTo(1));

Он также поддерживает Scala и Groovy.

await until { something() > 4 } // Scala example

Ожидание блестящее - именно то, что я искал!

Forge_7 17.06.2016 16:37

Взгляните на мой связанный ответ на

Разработка тестового класса для пользовательского барьера

Он предвзято относится к Java, но имеет разумное резюме вариантов.

В итоге, хотя (ИМО) это не использование какой-то причудливой структуры, которая обеспечит правильность, а то, как вы разрабатываете многопоточный код. Разделение проблем (параллелизм и функциональность) имеет огромное значение для повышения уверенности. Развитие объектно-ориентированного программного обеспечения с помощью тестов объясняет некоторые варианты лучше, чем я.

Статический анализ и формальные методы (см. Параллелизм: модели состояний и Java-программы) - это вариант, но я обнаружил, что они имеют ограниченное применение в коммерческой разработке.

Не забывайте, что любые тесты стиля нагрузки / выдержки редко гарантируют выявление проблем.

Удачи!

Вы также должны упомянуть здесь свою библиотеку tempus-fugit, которая helps write and test concurrent code;)

Idolon 17.09.2013 13:13

(если возможно) не используйте потоки, используйте актеров / активные объекты. Легко проверить.

@OMTheEternity может быть, но это все еще лучший ответ imo.

Dill 11.03.2015 22:16

В следующей статье предлагается 2 решения. Обертывание семафора (CountDownLatch) и добавляет такие функции, как извлечение данных из внутреннего потока. Другой способ достижения этой цели - использовать пул потоков (см. «Интересные места»).

Спринклер - Расширенный объект синхронизации

Пожалуйста, объясните подходы здесь, внешние ссылки могут быть мертвыми в будущем.

Uooo 01.08.2013 12:10

Вы можете использовать EasyMock.make ThreadSafe, чтобы сделать тестовый экземпляр потокобезопасным.

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

bennidi 18.09.2014 14:19

Я недавно обнаружил (для Java) инструмент под названием Threadsafe. Это инструмент статического анализа, очень похожий на findbugs, но специально для выявления проблем с многопоточностью. Это не замена для тестирования, но я могу рекомендовать его как часть написания надежной многопоточной Java.

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

Если вы пишете многопоточную Java дать ему шанс.

Как уже говорилось, проверка корректности МТ-кода - довольно сложная задача. В конце концов, все сводится к тому, чтобы в вашем коде не было некорректно синхронизированных гонок данных. Проблема в том, что существует бесконечно много возможностей выполнения потока (чередования), над которыми вы не имеете большого контроля (хотя обязательно прочтите статью это). В простых сценариях можно было бы действительно доказать правильность рассуждениями, но обычно это не так. Особенно, если вы хотите избежать / минимизировать синхронизацию и не использовать самый очевидный / самый простой вариант синхронизации.

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

Кстати, я думаю, что этот аспект тестирования MT-кода здесь не упоминался: выявить инварианты кода, которые вы можете проверить случайным образом. К сожалению, найти эти инварианты тоже довольно непросто. Кроме того, они могут не сохраняться все время во время выполнения, поэтому вам нужно найти / обеспечить выполнение точек, где вы можете ожидать, что они будут истинными. Приведение выполнения кода в такое состояние также является сложной проблемой (и само по себе может вызвать проблемы с параллелизмом. Уф, это чертовски сложно!

Некоторые интересные ссылки для чтения:

Автор ссылается на рандомизацию при тестировании. Это может быть Быстрая проверка, который был перенесен на многие языки. Вы можете посмотреть разговор о таком тестировании для параллельной системы здесь

Max 24.08.2015 14:11

За последние годы я несколько раз сталкивался с этой проблемой при написании кода обработки потоков для нескольких проектов. Я даю поздний ответ, потому что большинство других ответов, хотя и предоставляют альтернативы, на самом деле не отвечают на вопрос о тестировании. Мой ответ адресован тем случаям, когда нет альтернативы многопоточному коду; Я освещаю проблемы разработки кода для полноты, но также обсуждаю модульное тестирование.

Написание тестируемого многопоточного кода

Первое, что нужно сделать, - отделить код обработки производственного потока от всего кода, который выполняет фактическую обработку данных. Таким образом, обработка данных может быть протестирована как однопоточный код, и единственное, что делает многопоточный код, - это координирует потоки.

Во-вторых, следует помнить, что ошибки в многопоточном коде вероятны; Ошибки, которые проявляются реже, - это ошибки, которые незаметно проникают в рабочую среду, их будет трудно воспроизвести даже в рабочей среде, и, следовательно, они вызовут самые большие проблемы. По этой причине стандартный подход к кодированию, заключающийся в быстром написании кода и последующей его отладке до тех пор, пока он не заработает, является плохой идеей для многопоточного кода; это приведет к созданию кода, в котором простые ошибки исправлены, а опасные ошибки все еще существуют.

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

Написание модульных тестов для многопоточного кода

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

Чтобы правильно протестировать правильное поведение блокировки, тест должен запускать несколько потоков. Чтобы сделать тест повторяемым, мы хотим, чтобы взаимодействия между потоками происходили в предсказуемом порядке. Мы не хотим внешне синхронизировать потоки в тесте, потому что это замаскирует ошибки, которые могут произойти в производственной среде, где потоки не синхронизированы извне. Это оставляет использование временных задержек для синхронизации потоков, это метод, который я успешно использовал всякий раз, когда мне приходилось писать тесты многопоточного кода.

Если задержки слишком короткие, тогда тест становится хрупким, потому что незначительные различия во времени - скажем, между разными машинами, на которых могут выполняться тесты - могут привести к отключению времени и провалу теста. Обычно я начинаю с задержек, которые вызывают сбои тестов, увеличивают задержки, чтобы тест надежно проходил на моей машине разработки, а затем удваивают задержки сверх этого, чтобы тест имел хорошие шансы пройти на других машинах. Это действительно означает, что тест займет макроскопическое количество времени, хотя, по моему опыту, тщательный дизайн теста может ограничить это время не более чем дюжиной секунд. Поскольку в вашем приложении не должно быть очень много мест, требующих кода координации потоков, это должно быть приемлемо для вашего набора тестов.

Наконец, отслеживайте количество ошибок, обнаруженных вашим тестом. Если ваш тест покрывает 80% кода, можно ожидать, что он выловит около 80% ваших ошибок. Если ваш тест хорошо спроектирован, но не обнаруживает ошибок, есть разумная вероятность, что у вас нет дополнительных ошибок, которые проявятся только в рабочей среде. Если тест обнаружит одну или две ошибки, вам все равно может повезти. Кроме того, вы можете подумать о тщательном рассмотрении или даже полном переписывании кода обработки потоков, поскольку вполне вероятно, что код все еще содержит скрытые ошибки, которые будет очень трудно найти, пока код не будет запущен в производство, и очень тогда трудно исправить.

Тестирование может выявить только наличие ошибок, но не их отсутствие. Исходный вопрос касается проблемы с двумя потоками, и в этом случае исчерпывающее тестирование может быть возможным, но часто это не так. Для всего, что выходит за рамки простейших сценариев, вам, возможно, придется укусить пулю и использовать формальные методы - но не пропускайте модульные тесты! Написание правильного многопоточного кода в первую очередь сложно, но не менее сложная проблема - защитить его от регрессии в будущем.

Paul Williams 11.05.2016 16:55

Удивительное резюме одного из наименее понятных способов. Ваш ответ - это настоящая сегрегация, которую обычно не замечают.

prash 15.12.2016 12:24

Десяток секунд - это довольно много, даже если у вас всего несколько сотен тестов такой длины ...

Toby Speight 20.06.2018 18:41

@TobySpeight Тесты длиннее по сравнению с обычными модульными тестами. Я обнаружил, что полдюжины тестов более чем достаточно, если поточный код правильно спроектирован так, чтобы быть как можно более простым - необходимость в нескольких сотнях многопоточных тестов почти наверняка укажет на чрезмерно сложную организацию потоков.

Warren Dew 21.06.2018 20:33

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

Toby Speight 22.06.2018 10:11

Для кода J2E я использовал SilkPerformer, LoadRunner и JMeter для тестирования параллелизма потоков. Все они делают одно и то же. По сути, они предоставляют вам относительно простой интерфейс для администрирования своей версии прокси-сервера, необходимого для анализа потока данных TCP / IP и имитации нескольких пользователей, выполняющих одновременные запросы к вашему серверу приложений. Прокси-сервер может дать вам возможность делать такие вещи, как анализ сделанных запросов, представляя всю страницу и URL-адрес, отправленные на сервер, а также ответ от сервера после обработки запроса.

Вы можете найти некоторые ошибки в небезопасном режиме http, где вы можете, по крайней мере, анализировать данные формы, которые отправляются, и систематически изменять их для каждого пользователя. Но настоящие тесты - это когда вы запускаете https (защищенные слои сокетов). Затем вам также придется бороться с систематическим изменением данных сеанса и файлов cookie, что может быть немного более запутанным.

Самая лучшая ошибка, которую я когда-либо обнаружил при тестировании параллелизма, - это когда я обнаружил, что разработчик полагался на сборку мусора Java, чтобы закрыть запрос на соединение, установленный при входе на сервер LDAP, при входе в систему. Это привело к тому, что пользователи оказались уязвимыми. к сеансам других пользователей и очень запутанным результатам, когда пытались проанализировать, что произошло, когда сервер был поставлен на колени, едва в состоянии завершить одну транзакцию каждые несколько секунд.

В конце концов, вам или кому-то, вероятно, придется взяться за дело и проанализировать код на предмет грубых ошибок, подобных той, о которой я только что упомянул. И открытое обсуждение между отделами, подобное тому, которое произошло, когда мы раскрыли описанную выше проблему, очень полезно. Но эти инструменты - лучшее решение для тестирования многопоточного кода. JMeter имеет открытый исходный код. SilkPerformer и LoadRunner являются собственностью. Если вы действительно хотите знать, является ли ваше приложение потокобезопасным, большие мальчики делают это именно так. Я делал это для очень крупных компаний профессионально, поэтому не гадаю. Я говорю из личного опыта.

Небольшое предостережение: чтобы разобраться в этих инструментах, нужно время. Это не будет просто установкой программного обеспечения и запуском графического интерфейса, если вы уже не знакомы с многопоточным программированием. Я попытался выделить 3 важнейшие категории областей для понимания (формы, данные сеанса и файлов cookie) в надежде, что хотя бы начальное понимание этих тем поможет вам сосредоточиться на быстрых результатах, а не на необходимости читать вся документация.

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

Например, если код тестируемого объекта вызывает новый поток, подобный этому

Class TestedClass {
    public void doAsychOp() {
       new Thread(new myRunnable()).start();
    }
}

Затем насмешка над новыми потоками и последовательное выполнение аргумента runnable могут помочь

@Mock
private Thread threadMock;

@Test
public void myTest() throws Exception {
    PowerMockito.mockStatic(Thread.class);
    //when new thread is created execute runnable immediately 
    PowerMockito.whenNew(Thread.class).withAnyArguments().then(new Answer<Thread>() {
        @Override
        public Thread answer(InvocationOnMock invocation) throws Throwable {
            // immediately run the runnable
            Runnable runnable = invocation.getArgumentAt(0, Runnable.class);
            if (runnable != null) {
                runnable.run();
            }
            return threadMock;//return a mock so Thread.start() will do nothing         
        }
    }); 
    TestedClass testcls = new TestedClass()
    testcls.doAsychOp(); //will invoke myRunnable.run in current thread
    //.... check expected 
}

Параллелизм - это сложное взаимодействие между моделью памяти, оборудованием, кешами и нашим кодом. В случае Java, по крайней мере, такие тесты частично были рассмотрены в основном в jcstress. Известно, что создатели этой библиотеки являются авторами многих функций параллелизма JVM, GC и Java.

Но даже для этой библиотеки необходимо хорошее знание спецификации модели памяти Java, чтобы мы точно знали, что тестируем. Но я думаю, что в центре внимания этих усилий - тесты mircobenchmarks. Не большие бизнес-приложения.

Предполагая, что под "многопоточным" кодом имелось в виду что-то, что

  • с состоянием и изменчивый
  • И доступ / изменение несколькими потоками одновременно

Другими словами, мы говорим о тестировании настраиваемый поточно-ориентированный класс / метод / модуль - который в наши дни должен быть очень редким зверьком.

Поскольку этот зверь встречается редко, в первую очередь нам нужно убедиться, что есть все уважительные причины для его написания.

Шаг 1. Рассмотрите возможность изменения состояния в том же контексте синхронизации.

Сегодня легко написать совместимый параллельный и асинхронный код, в котором операции ввода-вывода или другие медленные операции выгружаются в фоновый режим, но общее состояние обновляется и запрашивается в одном контексте синхронизации. например задачи async / await и Rx в .NET и т. д. - все они тестируемы по дизайну, «настоящие» задачи и планировщики могут быть заменены, чтобы сделать тестирование детерминированным (однако это выходит за рамки вопроса).

Это может показаться очень ограниченным, но этот подход работает на удивление хорошо. В этом стиле можно писать целые приложения без необходимости делать какое-либо состояние потокобезопасным (я делаю).

Шаг 2. Если манипулирование общим состоянием в едином контексте синхронизации абсолютно невозможно.

Убедитесь, что колесо не изобретают заново / точно нет стандартной альтернативы, которая могла бы быть адаптирована для работы. Вероятно, что код очень сплоченный и содержится в одной единице, например. с большой долей вероятности это частный случай некоторой стандартной поточно-ориентированной структуры данных, такой как хэш-карта, коллекция или что-то еще.

Примечание: если код большой / охватывает несколько классов И требует многопоточного манипулирования состоянием, тогда очень высока вероятность того, что дизайн плохой, пересмотрите шаг 1

Шаг 3. Если этот шаг достигнут, нам нужно протестировать наш собственный поточно-ориентированный класс / метод / модуль с отслеживанием состояния.

Буду предельно честен: мне никогда не приходилось писать правильные тесты для такого кода. В большинстве случаев я ухожу на шаге 1, иногда на шаге 2. В последний раз мне приходилось писать собственный потокобезопасный код так много лет назад, что это было до того, как я принял модульное тестирование / вероятно, мне не пришлось бы его писать. в любом случае с текущими знаниями.

Если бы мне действительно пришлось протестировать такой код (наконец, актуальный ответ), я бы попробовал несколько вещей ниже

  1. Недетерминированное стресс-тестирование. например запустите 100 потоков одновременно и убедитесь, что конечный результат согласован. Это более типично для более высокого уровня / интеграционного тестирования сценариев с несколькими пользователями, но также может использоваться на уровне модулей.

  2. Раскройте несколько тестовых «крючков», где тест может внедрить некоторый код, чтобы помочь создать детерминированные сценарии, в которых один поток должен выполнять операцию раньше другого. Каким бы уродливым это ни было, я не могу придумать ничего лучше.

  3. Тестирование с задержкой, чтобы потоки запускались и выполняли операции в определенном порядке. Строго говоря, такие тесты тоже недетерминированы (есть вероятность зависания / остановки сборки сборщика мусора, что может исказить иначе организованные задержки), также это некрасиво, но позволяет избежать ловушек.

По этой теме есть статья, в которой в качестве языка в примере кода используется Rust:

https://medium.com/@polyglot_factotum/rust-concurrency-five-easy-pieces-871f1c62906a

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

Затем, если вы так структурировали свои «компоненты», самый простой способ их проверить - использовать каналы для отправки им сообщений, а затем заблокировать другие каналы, чтобы подтвердить, что компонент отправляет определенные ожидаемые сообщения.

Ссылка на статью полностью написана с использованием модульных тестов.

Это не идеально, но я написал этот помощник для своих тестов на C#:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Proto.Promises.Tests.Threading
{
    public class ThreadHelper
    {
        public static readonly int multiThreadCount = Environment.ProcessorCount * 100;
        private static readonly int[] offsets = new int[] { 0, 10, 100, 1000 };

        private readonly Stack<Task> _executingTasks = new Stack<Task>(multiThreadCount);
        private readonly Barrier _barrier = new Barrier(1);
        private int _currentParticipants = 0;
        private readonly TimeSpan _timeout;

        public ThreadHelper() : this(TimeSpan.FromSeconds(10)) { } // 10 second timeout should be enough for most cases.

        public ThreadHelper(TimeSpan timeout)
        {
            _timeout = timeout;
        }

        /// <summary>
        /// Execute the action multiple times in parallel threads.
        /// </summary>
        public void ExecuteMultiActionParallel(Action action)
        {
            for (int i = 0; i < multiThreadCount; ++i)
            {
                AddParallelAction(action);
            }
            ExecutePendingParallelActions();
        }

        /// <summary>
        /// Execute the action once in a separate thread.
        /// </summary>
        public void ExecuteSingleAction(Action action)
        {
            AddParallelAction(action);
            ExecutePendingParallelActions();
        }

        /// <summary>
        /// Add an action to be run in parallel.
        /// </summary>
        public void AddParallelAction(Action action)
        {
            var taskSource = new TaskCompletionSource<bool>();
            lock (_executingTasks)
            {
                ++_currentParticipants;
                _barrier.AddParticipant();
                _executingTasks.Push(taskSource.Task);
            }
            new Thread(() =>
            {
                try
                {
                    _barrier.SignalAndWait(); // Try to make actions run in lock-step to increase likelihood of breaking race conditions.
                    action.Invoke();
                    taskSource.SetResult(true);
                }
                catch (Exception e)
                {
                    taskSource.SetException(e);
                }
            }).Start();
        }

        /// <summary>
        /// Runs the pending actions in parallel, attempting to run them in lock-step.
        /// </summary>
        public void ExecutePendingParallelActions()
        {
            Task[] tasks;
            lock (_executingTasks)
            {
                _barrier.SignalAndWait();
                _barrier.RemoveParticipants(_currentParticipants);
                _currentParticipants = 0;
                tasks = _executingTasks.ToArray();
                _executingTasks.Clear();
            }
            try
            {
                if (!Task.WaitAll(tasks, _timeout))
                {
                    throw new TimeoutException($"Action(s) timed out after {_timeout}, there may be a deadlock.");
                }
            }
            catch (AggregateException e)
            {
                // Only throw one exception instead of aggregate to try to avoid overloading the test error output.
                throw e.Flatten().InnerException;
            }
        }

        /// <summary>
        /// Run each action in parallel multiple times with differing offsets for each run.
        /// <para/>The number of runs is 4^actions.Length, so be careful if you don't want the test to run too long.
        /// </summary>
        /// <param name = "expandToProcessorCount">If true, copies each action on additional threads up to the processor count. This can help test more without increasing the time it takes to complete.
        /// <para/>Example: 2 actions with 6 processors, runs each action 3 times in parallel.</param>
        /// <param name = "setup">The action to run before each parallel run.</param>
        /// <param name = "teardown">The action to run after each parallel run.</param>
        /// <param name = "actions">The actions to run in parallel.</param>
        public void ExecuteParallelActionsWithOffsets(bool expandToProcessorCount, Action setup, Action teardown, params Action[] actions)
        {
            setup += () => { };
            teardown += () => { };
            int actionCount = actions.Length;
            int expandCount = expandToProcessorCount ? Math.Max(Environment.ProcessorCount / actionCount, 1) : 1;
            foreach (var combo in GenerateCombinations(offsets, actionCount))
            {
                setup.Invoke();
                for (int k = 0; k < expandCount; ++k)
                {
                    for (int i = 0; i < actionCount; ++i)
                    {
                        int offset = combo[i];
                        Action action = actions[i];
                        AddParallelAction(() =>
                        {
                            for (int j = offset; j > 0; --j) { } // Just spin in a loop for the offset.
                            action.Invoke();
                        });
                    }
                }
                ExecutePendingParallelActions();
                teardown.Invoke();
            }
        }

        // Input: [1, 2, 3], 3
        // Ouput: [
        //          [1, 1, 1],
        //          [2, 1, 1],
        //          [3, 1, 1],
        //          [1, 2, 1],
        //          [2, 2, 1],
        //          [3, 2, 1],
        //          [1, 3, 1],
        //          [2, 3, 1],
        //          [3, 3, 1],
        //          [1, 1, 2],
        //          [2, 1, 2],
        //          [3, 1, 2],
        //          [1, 2, 2],
        //          [2, 2, 2],
        //          [3, 2, 2],
        //          [1, 3, 2],
        //          [2, 3, 2],
        //          [3, 3, 2],
        //          [1, 1, 3],
        //          [2, 1, 3],
        //          [3, 1, 3],
        //          [1, 2, 3],
        //          [2, 2, 3],
        //          [3, 2, 3],
        //          [1, 3, 3],
        //          [2, 3, 3],
        //          [3, 3, 3]
        //        ]
        private static IEnumerable<int[]> GenerateCombinations(int[] options, int count)
        {
            int[] indexTracker = new int[count];
            int[] combo = new int[count];
            for (int i = 0; i < count; ++i)
            {
                combo[i] = options[0];
            }
            // Same algorithm as picking a combination lock.
            int rollovers = 0;
            while (rollovers < count)
            {
                yield return combo; // No need to duplicate the array since we're just reading it.
                for (int i = 0; i < count; ++i)
                {
                    int index = ++indexTracker[i];
                    if (index == options.Length)
                    {
                        indexTracker[i] = 0;
                        combo[i] = options[0];
                        if (i == rollovers)
                        {
                            ++rollovers;
                        }
                    }
                    else
                    {
                        combo[i] = options[index];
                        break;
                    }
                }
            }
        }
    }
}

Пример использования:

[Test]
public void DeferredMayBeBeResolvedAndPromiseAwaitedConcurrently_void0()
{
    Promise.Deferred deferred = default(Promise.Deferred);
    Promise promise = default(Promise);

    int invokedCount = 0;

    var threadHelper = new ThreadHelper();
    threadHelper.ExecuteParallelActionsWithOffsets(false,
        // Setup
        () =>
        {
            invokedCount = 0;
            deferred = Promise.NewDeferred();
            promise = deferred.Promise;
        },
        // Teardown
        () => Assert.AreEqual(1, invokedCount),
        // Parallel Actions
        () => deferred.Resolve(),
        () => promise.Then(() => { Interlocked.Increment(ref invokedCount); }).Forget()
    );
}

Другие вопросы по теме