2
votes

I have a method called WaitForAction, which takes an Action delegate and executes it in a new Task. The method blocks until the task completes or until a timeout expires. It uses ManualResetEvent to wait for timeout/completion.

The following code shows an attempt to test the method in a multi-threaded environment.

class Program
{
    public static void Main()
    {
        List<Foo> list = new List<Foo>();

        for (int i = 0; i < 10; i++)
        {
            Foo foo = new Foo();
            list.Add(foo);
            foo.Bar();
        }

        SpinWait.SpinUntil(() => list.Count(f => f.finished || f.failed) == 10, 2000);
        Debug.WriteLine(list.Count(f => f.finished));
    }
}

public class Foo
{
    public volatile bool finished = false;
    public volatile bool failed = false;

    public void Bar()
    {
        Task.Factory.StartNew(() =>
        {
            try
            {
                WaitForAction(1000, () => { });
                finished = true;
            }
            catch
            {
                failed = true;
            }
        });
    }

    private void WaitForAction(int iMsToWait, Action action)
    {
        using (ManualResetEvent waitHandle = new ManualResetEvent(false))
        {
            Task.Factory.StartNew(() =>
            {
                action();
                waitHandle.SafeSet();
            });

            if (waitHandle.SafeWaitOne(iMsToWait) == false)
            {
                throw new Exception("Timeout");
            }
        }
    }
}

As the Action is doing nothing I would expect the 10 tasks started by calling Foo.Bar 10 times to complete well within the timeout. Sometimes this happens, but usually the program takes 2 seconds to execute and reports that only 2 instances of Foo 'finished' without error. In other words, 8 calls to WaitForAction have timed out.

I'm assuming that WaitForAction is thread safe, as each call on a Task-provided thread has its own stack. I have more or less proved this by logging the thread ID and wait handle ID for each call.

I realise that this code presented is a daft example, but I am interested in the principle. Is it possible for the task scheduler to be scheduling a task running the action delegate to the same threadpool thread that is already waiting for another action to complete? Or is there something else going on that I've missed?

1

1 Answers

1
votes

Task.Factory utilizes the ThreadPool by default. With every call to WaitHandle.WaitOne, you block a worker thread. The .Net 4/4.5 thread pool starts with a small number of worker threads depending on your hardware platform (e.g., 4 on my machine) and it re-evaluates the pool size periodically (I believe it is every 1 second), creating new workers if necessary.

Since your program blocks all worker threads, and the thread pool doesn't grow fast enough, your waithandles timeout as you saw.

To confirm this, you can either 1) increase the timeouts or 2) increase the beginning thread pool size by adding the following line to the beginning of your program:

ThreadPool.SetMinThreads(32, 4);

then you should see the timeouts don't occur.


I believe your question was more academic than anything else, but you can read about a better implementation of a task timeout mechanism here, e.g.

var task = Task.Run(someAction);
if (task == await Task.WhenAny(task, Task.Delay(millisecondsTimeout))) 
    await task;
else
    throw new TimeoutException();