В дебагере можно без проблем поймать поток исполнения в правильной точке, а затем, после проведения анализа, перезапустить его. В автоматических тестах эти операции выглядят безумно сложными.
А зачем вообще это нужно?
Построение параллельных систем — дело не самое простое. Необходимо соблюдать баланс между параллельностью и синхронностью. Недосинхронизируешь — потеряешь в стабильности. Пересинхронизируешь — получишь последовательную систему.
Рефакторинг — это вообще прогулка по минному полю.
Если бы не сложность реализации, автоматическое тестирование могло бы здорово помочь. На мой взгляд, в параллельной системе неплохо было бы автоматически проверять одновременную работу отдельных конфликтных участков кода — факт и порядок выполнения.
Вот я наконец и добрался до приостановления и возобновления работы потоков. Как уже говорилось, такой не автоматический механизм существует. Это брейкпоинты. Не будем изобретать новую терминологию — брейкпоинт, значит брейкпоинт.
Свойства автоматических брейкпоинтов:
Правда неудобно? Но надо смириться — ведь без тестирования не возможен рефакторинг. Всегда сначала приходится перебарывать себя… бла-бла-бла. Даже я вскоре понял, что пользоваться этим невозможно. Понял в районе второго десятка написанных тестов. Тесты получаются ненаглядные и сложные. Но…
Сложно — это хорошо. Ведь ничего кроме решения сложности я делать не умею. Немного усилий и получилось такое решение:
При его использовании предыдущий тест выглядеть так:
Свойства диспетчера:
PS: Solution
А зачем вообще это нужно?
Построение параллельных систем — дело не самое простое. Необходимо соблюдать баланс между параллельностью и синхронностью. Недосинхронизируешь — потеряешь в стабильности. Пересинхронизируешь — получишь последовательную систему.
Рефакторинг — это вообще прогулка по минному полю.
Если бы не сложность реализации, автоматическое тестирование могло бы здорово помочь. На мой взгляд, в параллельной системе неплохо было бы автоматически проверять одновременную работу отдельных конфликтных участков кода — факт и порядок выполнения.
Вот я наконец и добрался до приостановления и возобновления работы потоков. Как уже говорилось, такой не автоматический механизм существует. Это брейкпоинты. Не будем изобретать новую терминологию — брейкпоинт, значит брейкпоинт.
public class Breakpoint { [Conditional("DEBUG")] public static void Define(string name){…} } public class BreakCtrl : IDisposable { public string Name { get; private set; } public BreakCtrl(string name) {…} public BreakCtrl From(params Thread[] threads) {…} public void Dispose(){…} public void Run(Thread thread){…} public void Wait(Thread thread){…} public bool IsCapture(Thread thread){…} public Thread AnyCapture(){…} }
Свойства автоматических брейкпоинтов:
- Работают только в режиме отладки (при определенном макросе DEBUG). Мы не должны задумываться, что дополнительный код повлияет на работу системы у конечного пользователя.
- Брейкпоинт срабатывает только если его контролер определен. Ненужные в конкретном тесте брейкпоинты не должны наводить систему (и усложнять тесты).
- Контролер знает, в каком состоянии находится брейкпоинт — удерживает ли он поток.
- Контролер способен заставить брейкпоинт отпустить поток.
- И необязательная привязка к конкретному потоку. Хотим управляем конкретным потоком, хотим всеми сразу.
[TestMethod] public void StopStartThreadsTest_exemple1() { var log = new List<string>(); ThreadStart act1 = () => { Breakpoint.Define("empty"); Breakpoint.Define("start1"); log.Add("after start1"); Breakpoint.Define("step act1"); log.Add("after step act1"); Breakpoint.Define("finish1"); }; ThreadStart act2 = () => { Breakpoint.Define("start2"); log.Add("after start2"); Breakpoint.Define("step act2"); log.Add("after step act2"); Breakpoint.Define("finish2"); }; using (var start1 = new BreakCtrl("start1")) using (var step_act1 = new BreakCtrl("step act1")) using (var finish1 = new BreakCtrl("finish1")) using (var start2 = new BreakCtrl("start2")) using (var step_act2 = new BreakCtrl("step act2")) using (var finish2 = new BreakCtrl("finish2")) { var thr1 = new Thread(act1); thr1.Start(); var thr2 = new Thread(act2); thr2.Start(); start1.Wait(thr1); start2.Wait(thr2); start1.Run(thr1); step_act1.Wait(thr1); step_act1.Run(thr1); finish1.Wait(thr1); start2.Run(thr2); step_act2.Wait(thr2); step_act2.Run(thr2); finish2.Wait(thr2); finish1.Run(thr1); finish2.Run(thr2); thr1.Join(); thr2.Join(); } Assert.AreEqual(4, log.Count); Assert.AreEqual("after start1", log[0]); Assert.AreEqual("after step act1", log[1]); Assert.AreEqual("after start2", log[2]); Assert.AreEqual("after step act2", log[3]); }
Правда неудобно? Но надо смириться — ведь без тестирования не возможен рефакторинг. Всегда сначала приходится перебарывать себя… бла-бла-бла. Даже я вскоре понял, что пользоваться этим невозможно. Понял в районе второго десятка написанных тестов. Тесты получаются ненаглядные и сложные. Но…
Сложно — это хорошо. Ведь ничего кроме решения сложности я делать не умею. Немного усилий и получилось такое решение:
public class ThreadTestManager { public ThreadTestManager(TimeSpan timeout, params Action[] threads){…} public void Run(params BreakMark[] breaks){…} } public class BreakMark { public string Name { get; private set; } public Action ThreadActor { get; private set; } public bool Timeout { get; set; } public BreakMark(string breakName){…} public BreakMark(Action threadActor, string breakName){…} public static implicit operator BreakMark(string breakName){…} }
При его использовании предыдущий тест выглядеть так:
[TestMethod] public void StopStartThreadsTest_exemple2() { var log = new List<string>(); Action act1 = () => { Breakpoint.Define("before start1"); Breakpoint.Define("start1"); log.Add("after start1"); Breakpoint.Define("step act1"); log.Add("after step act1"); Breakpoint.Define("finish1"); }; Action act2 = () => { Breakpoint.Define("before start2"); Breakpoint.Define("start2"); log.Add("after start2"); Breakpoint.Define("step act2"); log.Add("after step act2"); Breakpoint.Define("finish2"); }; new ThreadTestManager(TimeSpan.FromSeconds(1), act1, act2).Run( "before start1", "before start2", "start1", "step act1", "finish1", "start2", "step act2", "finish2"); Assert.AreEqual(4, log.Count); Assert.AreEqual("after start1", log[0]); Assert.AreEqual("after step act1", log[1]); Assert.AreEqual("after start2", log[2]); Assert.AreEqual("after step act2", log[3]); }
Свойства диспетчера:
- Все делегаты запускаются при старте в своем потоке.
- Маркеры брейкпоинтов определяют порядок возобновления работы. Не входа, а выхода из брейкпоинтов. Возможно это просто издержки реализации абстракции «брейкпоинт». Но свойство есть и о нем приходится иногда вспомнить.
- Все контролеры для соответствующих маркеров брейкпоинтов определены на всем протяжении работы диспетчера.
- С маркером брейкпоинта можно указать потоком (делегат), с которым он будет работать. По умолчанию работает со всеми.
[TestMethod] public void ThreadMarkBreakpointTest_exemple3() { var log = new List<string>(); Action<string> act = name => { Breakpoint.Define("start"); log.Add(name); Breakpoint.Define("finish"); }; Action act0 = () => act("act0"); Action act1 = () => act("act1"); new ThreadTestManager(TimeSpan.FromSeconds(1), act0, act1).Run( new BreakMark(act0, "finish"), new BreakMark(act1, "start"), new BreakMark(act1, "finish")); Assert.AreEqual(2, log.Count); Assert.AreEqual("act0", log[0]); Assert.AreEqual("act1", log[1]); } - Определено время, в течении которого должны выполнится все операции — timeout. При превышении — все потоки останавливаются грубо и беспощадно (abort).
- К маркеру брейкпоинта, можно добавить признак недосягаемости, не добравшись сюда система планово выйдет по timeout-у. Срабатывание брейкпоинта приведет к провалу теста. Этот механизм используется для проверки факта блокировки.
[TestMethod] public void Timeout_exemple4() { var log = new List<string>(); Action act = () => { try { while (true) ; } catch (ThreadAbortException) { log.Add("timeout"); } Breakpoint.Define("don't work"); }; new ThreadTestManager(TimeSpan.FromSeconds(1), act).Run( new BreakMark("don't work") { Timeout = true }); Assert.AreEqual("timeout", log.Single()); } - Если хочется остановить выполнение потока и не продолжать его, надо указать соответствующий маркер брейкпоинта после маркера с timeout-ом.
[TestMethod] public void CatchThread_exemple5() { var log = new List<string>(); Action act0 = () => { bool a = true; while (a) ; Breakpoint.Define("act0"); }; Action act1 = () => { Breakpoint.Define("act1"); log.Add("after act1"); }; new ThreadTestManager(TimeSpan.FromSeconds(1), act0, act1).Run( new BreakMark("act0") { Timeout = true }, "act1"); Assert.IsFalse(log.Any()); }
PS: Solution
