1

I have a test and I want to make sure that I will get isolated result per Thread from an async method. My test is look like the following:

public async Task MyMethod_Concurrency_ReturnsIsolatedResultPerThread()
{
    int expectedResult = 20;

    var theMock = new Mock<IService>();
        theMock.Setup(m => m.GetResult(It.IsAny<int>()))
            .Callback(() => Thread.Sleep(10))
            .Returns<int>(t => Task.FromResult(expectedResult));

        var sut = new MyClass(30, theMock.Object);

        var rs1 = new ManualResetEventSlim();
        var rs2 = new ManualResetEventSlim();

        var task1 = Task.Run(async () =>
        {
            expectedResult = 40;
            await sut.MyMethod();
            rs2.Set();
            rs1.Wait();
            Assert.AreEqual(expectedResult, sut.Result);
        });

        var task2 = Task.Run(async () =>
        {
            rs2.Wait();
            expectedResult = 45;
            await sut.MyMethod();
            Assert.AreEqual(expectedResult, sut.Result);
            rs1.Set();
        });

        var task3 = Task.Run(() => Assert.AreEqual(0, sut.Amount));

        await Task.WhenAll(task1, task2, task3);
    }

The test works fine and passed successfully. However without using ManualResetEventSlim it also works as expected. So my question is what is the usage of ManualResetEventSlim in this example? I'm really confused with that? Can anybody please explain what is difference between using ManualResetEventSlim or not using it in my test? What can I do, so that my test won't be passed without using ManualResetEvents??

  • hrm lets see what the documentation says https://msdn.microsoft.com/en-us/library/system.threading.manualreseteventslim(v=vs.110).aspx *Provides a slimmed down version of ManualResetEvent*. hmm lets see what that says https://msdn.microsoft.com/en-us/library/system.threading.manualresetevent(v=vs.110).aspx *Notifies one or more waiting threads that an event has occurred*. hmmm if its still not clear lets see how the code works by using breakpoints https://msdn.microsoft.com/en-us/library/5557y8b4.aspx – TheGeneral May 19 '18 at 09:32
  • @TheGeneral As I said my question is: why my test is passed, even without using `ManualResetEventSlim`. I have read all of the linked that you mentioned already, but I'm confused whit `ManualResetEventSlim` in this test? Also I don't know why the breakpoint doesn't hit for my test! –  May 19 '18 at 09:42
  • Those ManualResetEvents are there to force the order of the methods to run - although - I have no idea why the order is not simply enforced by writing something like: "await sut.MyMethod(); Assert.AreEqual(40, sut.Result); await sut.MyMethod(); Assert.AreEqual(45, sut.Result);" The reason why your test passes without the ManualResetEvents is a "coincidence" – johannes.colmsee May 19 '18 at 09:47
  • @johannes.colmsee Can you show me an example of how can I do, so that my test won't be passed without `ManualResetEvents`?? –  May 19 '18 at 09:50
  • https://stackoverflow.com/questions/34510/what-is-a-race-condition – Hans Passant May 19 '18 at 10:46
  • To make your test fail without ManualResetEvent, just make execution time until you invoke MyMethod() different for each task e.g. by doing some sleep. – BionicCode May 19 '18 at 10:46
  • When your task execution time is big enough, the OS can even switch context between single instructions to run them concurrently on one core. You don't know when and how often this will happen. So although both tasks are equally time consuming by implementation they might therefore terminate at different times. Since you or your OS are running other processes in background, which also need to get served by the CPU, your results will change for each execution. – BionicCode May 19 '18 at 10:59
  • But your tasks are only executing a mocked method which returns a predefined stored integer. So there will be most likely no optimization and your code executes always in order. – BionicCode May 19 '18 at 11:06
  • I think the key here is that thread creation is not parallel but their execution. – BionicCode May 19 '18 at 11:14

1 Answers1

0

Task.WhenAll() does only wait for all tasks until run to completion. It will order the results to match the order the task objects were passed, but it does not enforce any order of execution although all Tasks are always internally started in their occurring order (see source code). If you need a fixed execution order than you must take care of it yourself e.g. by using a WaitHandle like ManualRestEvent or a Semaphore or using the task continuation methods.

This means if your tasks are all equally short running (like in your example) than they are started in the same order and complete in the same order. But if your tasks execute in different times, e.g. task1 -> 2000 ms and task2 -> 20 ms, than task2 would complete long before task1.

Or to use your example when task1 takes longer than task2 to reach the invocation of sut.MyMethod() the result won't be the same. Now if you need task1 to complete before task2 you need to control the flow.

BionicCode
  • 1
  • 4
  • 28
  • 44