В дебагере можно без проблем поймать поток исполнения в правильной точке, а затем, после проведения анализа, перезапустить его. В автоматических тестах эти операции выглядят безумно сложными.
А зачем вообще это нужно?
Построение параллельных систем — дело не самое простое. Необходимо соблюдать баланс между параллельностью и синхронностью. Недосинхронизируешь — потеряешь в стабильности. Пересинхронизируешь — получишь последовательную систему.
Рефакторинг — это вообще прогулка по минному полю.
Если бы не сложность реализации, автоматическое тестирование могло бы здорово помочь. На мой взгляд, в параллельной системе неплохо было бы автоматически проверять одновременную работу отдельных конфликтных участков кода — факт и порядок выполнения.
Вот я наконец и добрался до приостановления и возобновления работы потоков. Как уже говорилось, такой не автоматический механизм существует. Это брейкпоинты. Не будем изобретать новую терминологию — брейкпоинт, значит брейкпоинт.
Свойства автоматических брейкпоинтов:
Правда неудобно? Но надо смириться — ведь без тестирования не возможен рефакторинг. Всегда сначала приходится перебарывать себя… бла-бла-бла. Даже я вскоре понял, что пользоваться этим невозможно. Понял в районе второго десятка написанных тестов. Тесты получаются ненаглядные и сложные. Но…
Сложно — это хорошо. Ведь ничего кроме решения сложности я делать не умею. Немного усилий и получилось такое решение:
При его использовании предыдущий тест выглядеть так:
Свойства диспетчера:
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