Let’s say you want to implement a Producer-Consumer pattern based on System.Threading.Channel to process items asynchronously:

public class AsyncLogProcessor
{
    private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();
    private readonly Task _processingTask;

    public AsyncLogProcessor()
    {
        _processingTask = Task.Factory.StartNew(async () =>
        {
            await foreach (var log in _channel.Reader.ReadAllAsync())
            {
                // Processing a log item.
                Console.WriteLine(log);
            }
        }, TaskCreationOptions.LongRunning);
    }

    public void ProcessLog(string log)
    {
        _channel.Writer.TryWrite(log);
    }
}

And since you know that the processing task should run for the duration of the process, you use TaskCreationOptions.LongRunning flag.

Do you have an issue with this solution?

I actually, do.

The LongRunning flag tells the TPL to have a dedicated thread for a given callback, instead of getting a thread from the thread pool. But even though your task is semantically long running, the dedicated thread won’t be running for a long time.

Let’s simplify the code and add some tracing:

static void WriteLine(string message)
{
    Console.WriteLine($"[{Environment.CurrentManagedThreadId}] [IsThreadPoolTHread: {Thread.CurrentThread.IsThreadPoolThread}] - {message}.");
}

static void Main(string[] args)
{
    var task = Task.Factory.StartNew(async () =>
	{
	    WriteLine("Task started");
	    await Task.Delay(1000);
	    WriteLine("Task completed");
	}, TaskCreationOptions.LongRunning);

    Thread.Sleep(100);
    Console.WriteLine($"task.IsCompleted: " + task.IsCompleted);

    Console.ReadLine();
}

The output:

[4] [IsThreadPoolThread: False] - Task started.
task.IsCompleted: True
[6] [IsThreadPoolThread: True] - Task completed.

Here what’s happening at runtime:

  • The Task started message is printed from a dedicated thread.
  • The Task completed message is printed from a thread pool thread.
  • The task appears to be completed before the callback is done.

This happens because Task.Factory.StartNew is not async-friendly. The actual type of the task variable is Task<Task>, and the parent task completes when the new thread starts executing the callback, not when the callback itself completes.

The LongRunning flag is respected for running the first block of the async method before the first await. The await suspends the execution of the async method and the rest of the method is scheduled into the thread pool thread by the default task scheduler.

It is theoretically possible, that the callback that you provide to StartNew has a long running piece before the first await and you really want to run it in the dedicated thread. If this is the case the LongRunning flag is legit, but this is very uncommon and if you hit such case, please add a lengthy comment explaining the performance benefits of this approach.

The guidelines for Task.Factory.StartNew and async delegates:

  1. Avoid using Task.Factory.StartNew with async delegates. If you must, use the Unwarp extension method to get the actual underlying task.
  2. Do not use LongRunning flag with Task.Factory.StartNew for async callbacks.  The flag is useful for synchronous methods that block the thread, but not for async methods where the continuation will be scheduled on the thread pool.