1

I have a threading module in my app which manages initiating parallel operations. It adds various bits of timing and logging for reasons, and is complicated enough that I recently discovered I'd coded in a bug whereby it was initiating some of the tasks on doubly nested threads.

i.e. it was calling the equivalent of:

Task.Run(
    async () => await Task.Run(
        () => DoStuff();
    );
).Wait()

Now on the one hand, that code works ... the target code gets run, and the waiting code doesn't continue until the target code has completed.

On the other hand, it's using 2 threads to do this rather than 1 and since we're having issues with thread-starvation, that's something of an issue.

I know how to fix the code, but I'd like to write a unit test to ensure that A) I've fixed all the bugs like this / fixed it in all the scenarios. and B) no-one recreates this bug in the future.

But I can't see how to get hold of "all the threads that I've created". CurrentProcess.Threads gives me LOADS of threads, and no obvious way to identify which ones are the ones I care about.

Any thoughts?

Brondahl
  • 7,402
  • 5
  • 45
  • 74

3 Answers3

2

how to get hold of "all the threads that I've created"

Task.Run does not create any threads; it schedules jobs to run on the currently configured thread pool. See https://msdn.microsoft.com/library/system.threading.tasks.taskscheduler.aspx

If you mean "how to count the number of Tasks I've enqueued", I think you will need to create a custom implementation of TaskScheduler which counts incoming tasks and configure your test code to use it. There is an example of a custom TaskScheduler shown on the page linked above.

Rich
  • 15,048
  • 2
  • 66
  • 119
  • Yes, you're correct ... that's exactly what I mean. I don't suppose you know how to do to that do you? /me goes off with a new phrase to google – Brondahl May 16 '16 at 14:34
2

As is often the solution with unit testing something that involves a static method (Task.Run in this case), you will likely need to pass something in as a dependency to your class that wraps this up and that you can then add behaviour to in the tests.

As @Rich suggests in his answer, you could do this by passing in a TaskScheduler. Your test version of this can then maintain a count of the tasks as they are enqueued.

Making a test TaskScheduler is actually a little ugly because of protection levels, but at the bottom of this post I have included one that wraps an existing TaskScheduler (e.g. you could use TaskScheduler.Default).

Unfortunately, you would also need to change your calls like

Task.Run(() => DoSomething);

to something like

Task.Factory.StartNew(
    () => DoSomething(),
    CancellationToken.None,
    TaskCreationOptions.DenyChildAttach,
    myTaskScheduler);

which is basically what Task.Run does under the hood, except with the TaskScheduler.Default. You could of course wrap that up in a helper method somewhere.

Alternatively, if you are not squeamish about some riskier reflection in your test code you could hijack the TaskScheduler.Default property, so that you can still just use Task.Run:

var defaultSchedulerField = typeof(TaskScheduler).GetField("s_defaultTaskScheduler", BindingFlags.Static | BindingFlags.NonPublic);
var scheduler = new TestTaskScheduler(TaskScheduler.Default);
defaultSchedulerField.SetValue(null, scheduler);

(Private field name is from TaskScheduler.cs line 285.)

So for example, this test would pass using my TestTaskScheduler below and the reflection trick:

[Test]
public void Can_count_tasks()
{
    // Given
    var originalScheduler = TaskScheduler.Default;
    var defaultSchedulerField = typeof(TaskScheduler).GetField("s_defaultTaskScheduler", BindingFlags.Static | BindingFlags.NonPublic);
    var testScheduler = new TestTaskScheduler(originalScheduler);
    defaultSchedulerField.SetValue(null, testScheduler);

    // When
    Task.Run(() => {});
    Task.Run(() => {});
    Task.Run(() => {});

    // Then
    testScheduler.TaskCount.Should().Be(3);

    // Clean up
    defaultSchedulerField.SetValue(null, originalScheduler);
}

Here is the test task scheduler:

using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;

public class TestTaskScheduler : TaskScheduler
{
    private static readonly MethodInfo queueTask = GetProtectedMethodInfo("QueueTask");
    private static readonly MethodInfo tryExecuteTaskInline = GetProtectedMethodInfo("TryExecuteTaskInline");
    private static readonly MethodInfo getScheduledTasks = GetProtectedMethodInfo("GetScheduledTasks");

    private readonly TaskScheduler taskScheduler;

    public TestTaskScheduler(TaskScheduler taskScheduler)
    {
        this.taskScheduler = taskScheduler;
    }

    public int TaskCount { get; private set; }

    protected override void QueueTask(Task task)
    {
        TaskCount++;
        CallProtectedMethod(queueTask, task);
    }

    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        return (bool)CallProtectedMethod(tryExecuteTaskInline, task, taskWasPreviouslyQueued);
    }

    protected override IEnumerable<Task> GetScheduledTasks()
    {
        return (IEnumerable<Task>)CallProtectedMethod(getScheduledTasks);
    }

    private object CallProtectedMethod(MethodInfo methodInfo, params object[] args)
    {
        return methodInfo.Invoke(taskScheduler, args);
    }

    private static MethodInfo GetProtectedMethodInfo(string methodName)
    {
        return typeof(TaskScheduler).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic);
    }
}

or tidied up using RelflectionMagic as suggested by @hgcummings in the comments:

var scheduler = new TestTaskScheduler(TaskScheduler.Default);
typeof(TaskScheduler).AsDynamicType().s_defaultTaskScheduler = scheduler;
using System.Collections.Generic;
using System.Threading.Tasks;
using ReflectionMagic;

public class TestTaskScheduler : TaskScheduler
{
    private readonly dynamic taskScheduler;

    public TestTaskScheduler(TaskScheduler taskScheduler)
    {
        this.taskScheduler = taskScheduler.AsDynamic();
    }

    public int TaskCount { get; private set; }

    protected override void QueueTask(Task task)
    {
        TaskCount++;
        taskScheduler.QueueTask(task);
    }

    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        return taskScheduler.TryExecuteTaskInline(task, taskWasPreviouslyQueued);
    }

    protected override IEnumerable<Task> GetScheduledTasks()
    {
        return taskScheduler.GetScheduledTasks();
    }
}
Community
  • 1
  • 1
Jamie Humphries
  • 3,368
  • 2
  • 18
  • 21
  • Thats ... stunning? horrifying? I don't really understand why "use this as the default scheduler" isn't a thing that you can JustDo, but glad to know how to do now.. – Brondahl May 16 '16 at 16:55
  • 1
    Nice (or possibly evil)! You could simplify the TestTaskRunner a bit using ReflectionMagic's AsDynamic (there may be a performance hit though). See https://blogs.msdn.microsoft.com/davidebb/2010/01/18/use-c-4-0-dynamic-to-drastically-simplify-your-private-reflection-code/ – hgcummings May 16 '16 at 17:02
  • 1
    Nice. I'd add some cleanup code to put the TaskScheduler.Default back again at the end of the test, or you may get unwanted interference with other later tests. – Rich May 16 '16 at 18:53
  • It is a bit disappointing that the CLR doesn't let you configure the default TaskScheduler. Java's equivalent has this as a pluggable dependency, which would make this test much easier and nicer to write. – Rich May 16 '16 at 18:55
  • Yes, some cleanup code would be sensible. Nothing should go terribly wrong at the moment because the test scheduler just wraps the original deafult one, but restoring it fully would be preferable. – Jamie Humphries May 16 '16 at 19:41
  • Edited to incorporate ideas from comments. – Jamie Humphries May 17 '16 at 09:45
0

The thread class does have a Name property you could use to help identify all the threads you have created. Meaning a simple linq or for loop that will enable you to track which threads are yours.

https://msdn.microsoft.com/en-us/library/system.threading.thread.name(v=vs.110).aspx

Ben Steele
  • 414
  • 2
  • 10