'Synchronously wait on task in another thread
I need to do some work on a specific thread (for all intents and purposes, we can say this is the UI thread), but the method requesting that work to be done may or may not be executing in a different thread. I am completely new to multithreaded programming, but have arrived at the conclusion that the correct approach to this is to use a TaskScheduler.
After toying around for a while with a custom implementation, I found FromCurrentSynchronizationContext. This appears to do exactly what I need and save me a lot of trouble (famous last words and all that).
My question comes down to whether I am overlooking anything that will get me into trouble, or maybe I'm overcomplicating the issue altogether. Here's what I'm doing now:
TaskScheduler
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Internals
{
internal static class MainThreadTaskScheduler
{
private static readonly object taskSchedulerLock = new();
private static readonly Thread taskSchedulerThread;
private static readonly TaskScheduler taskScheduler;
static MainThreadTaskScheduler()
{
lock (taskSchedulerLock)
{
// despite calling it the "main thread", we don't really care which thread
// this is initialized with, we just need to always use the same one for
// executing the scheduled tasks
taskSchedulerThread = Thread.CurrentThread;
if (SynchronizationContext.Current is null)
{
// in implementation this may be null, a default context works
SynchronizationContext.SetSynchronizationContext(new());
}
taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
}
}
public static Task Schedule(Action action)
{
lock (taskSchedulerLock)
{
if (Thread.CurrentThread == taskSchedulerThread)
{
// if we are already on the main thread, just run the delegate
action();
return Task.CompletedTask;
}
return Task.Factory.StartNew(action, CancellationToken.None,
TaskCreationOptions.None, taskScheduler);
}
}
public static Task<TResult> Schedule<TResult>(Func<TResult> func)
{
lock (taskSchedulerLock)
{
if (Thread.CurrentThread == taskSchedulerThread)
{
// if we are already on the main thread, just run the delegate
return Task.FromResult(func());
}
return Task.Factory.StartNew(func, CancellationToken.None,
TaskCreationOptions.None, taskScheduler);
}
}
}
}
Usage
// ...elsewhere...
public static bool RunTaskInMainThread()
{
// we need to synchronously return the result from the main thread regardless of
// which thread we are currently executing in
return MainThreadTaskScheduler.Schedule(() => SomeMethod()).GetAwaiter().GetResult();
}
I had attempted to make RunTaskInMainThread an async method and use await, but it kept causing my program to hang rather than yielding a result. I'm sure I was just using that incorrectly, but I don't know how to implement it here (bonus question: how can I use await here?).
Am I doing anything wrong here? Is there a better way to get the same results?
Solution 1:[1]
You are not in the right direction. Not because you are not smart, but because in the general area that you are trying to move there are traps all over the place.
The
TaskSchedulers are not compatible with async/await. They were invented before async/await was a thing, for tasks that we now call delegate-based tasks (tasks that represent the completion of a specific delegate), in contrast with the kind of tasks that are created by async methods and are now known as promise-style tasks (tasks that represent just a promise that sometime in the future they'll complete).The
SynchronizationContextclass is useless by itself. It's only useful as a base class for implementing derived classes like theWindowsFormsSynchronizationContextor Stephen Cleary'sAsyncContextSynchronizationContext. It's a pity that this class was not defined asabstract, like theTaskScheduleris, to prevent programmers from trying to use it as is. Implementing a properSynchronizationContext-derived class is not trivial.When a thread is used for scheduling work via a scheduler, either a
TaskScheduleror aSynchronizationContext, the thread is then owned by the scheduler. You can't have a thread that is shared by two schedulers, or by a scheduler and some method that wants to use that thread at any time on demand. That's why when start a message loop on the UI thread with theApplication.Runmethod, this call is blocking. Any code that follows this call will not execute before the loops is completed (before the associated windowsFormis closed). The same is true and with Stephen Cleary'sAsyncContext. TheAsyncContext.Runcall is blocking (example).
Some more links:
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Theodor Zoulias |
