Calling _thread.Join()
causes the GetConsumingEnumerable
loop to be stuck on the last element. Why does this behavior occur?
public abstract class ActorBase : IDisposable
{
private readonly BlockingCollection<Task> _queue = new BlockingCollection<Task>(new ConcurrentQueue<Task>());
private readonly Thread _thread;
private bool _isDisposed;
protected ActorBase()
{
_thread = new Thread(ProcessMessages);
_thread.Start();
}
protected void QueueTask(Task task)
{
if (_isDisposed)
{
throw new Exception("Actor was disposed, cannot queue task.");
}
_queue.Add(task);
}
private void ProcessMessages()
{
foreach (var task in _queue.GetConsumingEnumerable())
{
task.RunSynchronously();
}
}
public void Dispose()
{
_isDisposed = true;
_queue.CompleteAdding();
_thread.Join();
}
}
public class SampleActor : ActorBase
{
private string GetThreadStatus()
{
Thread.Sleep(500);
return string.Format("Running on thread {0}", Thread.CurrentThread.ManagedThreadId);
}
public async Task<string> GetThreadStatusAsync()
{
var task = new Task<string>(GetThreadStatus);
QueueTask(task);
return await task;
}
}
class Program
{
public static async Task Run()
{
using (var sa = new SampleActor())
{
for (int i = 0; i < 3; i++)
{
Console.WriteLine(await sa.GetThreadStatusAsync());
}
}
}
public static void Main(string[] args)
{
Console.WriteLine("Main thread id {0}", Thread.CurrentThread.ManagedThreadId);
var task = Task.Run(async ()=> { await Run(); });
task.Wait();
}
}
The context for this approach is that I need to make sure that all operations are executed on one OS thread, which would allow a part of the app to use different credentials than the main thread.
async-await
works with continuations. To be efficient and reduce scheduling these continuations usually run on the same thread that completed the previous task.That means in your case that your special thread is not only running the tasks, it's also running all the continuations after these tasks (the
for
loop itself). You can see that by printing the thread id:When the
for
loop completes and theSampleActor
is being disposed you callThread.Join
from the same thread your are trying to join so you get a deadlock. Your situation boils down to this:In .Net 4.6 you can solve this with
TaskCreationOptions.RunContinuationsAsynchronously
but in the current version you can specify the defaultTaskScheduler
: