📜 ⬆️ ⬇️

Testing parallel threads

In debugger, you can easily catch the flow of performance at the right point, and then, after analyzing, restart it. In automatic tests, these operations look incredibly complex.

Why do we need it at all?

Building parallel systems is not the easiest thing. A balance must be maintained between parallelism and synchronism. Unsynchronized - you lose in stability. Resynchronize - you get a consistent system.
')
Refactoring is generally a walk in a minefield.

If it were not for the complexity of the implementation, automatic testing could be a great help. In my opinion, in a parallel system, it would be nice to automatically check the simultaneous operation of individual conflicting parts of the code - a fact and order of execution.

So I finally got to the suspension and resumption of work flows. As already mentioned, this non-automatic mechanism exists. These are breakpoints. We will not invent a new terminology - breakpoint, which means breakpoint.

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(){…} } 


Properties of automatic breakpoint:
  1. Only work in debug mode (with a certain DEBUG macro). We should not think that the additional code will affect the work of the system at the end user.
  2. Breakpoint works only if its controller is defined. Unnecessary in a specific test breakpoint should not direct the system (and complicate the tests).
  3. The controller knows what state the breakpoint is in - whether it keeps the flow.
  4. The controller is able to force a breakpoint to release the stream.
  5. And optional binding to a specific thread. We want to control a specific stream, we want all at once.


 [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]); } 


Really uncomfortable? But you have to accept it, because without testing, refactoring is not possible. Always first you have to go over yourself ... blah blah blah. Even I soon realized that it was impossible to use it. Understood in the second dozen written tests. Tests turn out beloved and complex. But…

Difficult is good. After all, nothing but a solution to the complexity I can not do. A little effort came up with this solution:

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


When using it, the previous test looks like this:

 [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]); } 


Manager properties:
  1. All delegates are started at the start in their thread.
  2. Breakpoint markers determine how to resume work. Not an entry, but an exit from breakpoint. Perhaps this is just the cost of implementing a breakpoint abstraction. But the property is, and sometimes you have to remember about it.
  3. All controllers for the corresponding breakpoint markers are defined throughout the work of the dispatcher.
  4. With a breakpoint marker, you can specify a thread (delegate) with which it will work. By default it works with everyone.

     [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]); } 
  5. The time is determined during which all operations must be performed - timeout. When exceeded - all threads stop roughly and mercilessly (abort).
  6. To breakpoint marker, you can add a sign of inaccessibility, not having reached here, the system will be released according to a timeout plan. Breaking a breakpoint will cause the test to fail. This mechanism is used to check for blocking.

     [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()); } 
  7. If you want to stop the execution of the stream and not to continue it, you must specify the corresponding breakpoint marker after the marker with 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

Source: https://habr.com/ru/post/135815/


All Articles