Skip to content

Instantly share code, notes, and snippets.

@jnm2
Last active May 7, 2018 19:38
Show Gist options
  • Save jnm2/855392059360819b2903 to your computer and use it in GitHub Desktop.
Save jnm2/855392059360819b2903 to your computer and use it in GitHub Desktop.
Simulates an await by running the message pump for the current thread, but does not return until the task ends. Same pattern as ShowDialog().
// #define ASYNC_SIMULATOR_RAISE_IDLE - Disabled due to conflict with XtraReportEx.CreateDocument(true) and AsyncWaitData, and no known benefit
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// <para>
/// Contains experimental methods to simulate an await when you are forced to wait synchronously.
/// Blocks the current thread but pumps all thread messages so that any await continuations or UI on
/// the current thread can run as usual, preventing deadlocks and frozen UI.
/// </para>
/// <para>
/// Use with caution. This is no more dangerous than ShowDialog() or DoEvents(), but what will happen
/// is that thread messages will be handled recursively inside the current stack. This results in
/// reentry of UI event handlers.
/// For example, a third party library might be in the process of handling a mouse event when you
/// use .Await() to keep the event hander from returning until you have a value. While waiting,
/// .Await() receives the next mouse event which gets handled by the third-party library before the
/// last one even returns. If the third-party library is not hardened against reentry, which would be
/// unusual, you will get bugs that are very difficult to reproduce and diagnose.
/// </para>
/// </summary>
public static class AsyncSimulatorExperimental
{
#region WaitHandle wait and pump
/// <summary>
/// <para>
/// Experimental. Returns the index in the <paramref name="waitHandles"/> array of the first <see cref="WaitHandle"/> to signal, or -1 if the wait timed out.
/// Blocks the current thread but pumps all thread messages so that any await continuations or UI on
/// the current thread can run as usual, preventing deadlocks and frozen UI.
/// </para>
/// <para>
/// Use with caution. This is no more dangerous than ShowDialog() or DoEvents(), but what will happen
/// is that thread messages will be handled recursively inside the current stack. This results in
/// reentry of UI event handlers.
/// For example, a third party library might be in the process of handling a mouse event when you
/// use .Await() to keep the event hander from returning until you have a value. While waiting,
/// .Await() receives the next mouse event which gets handled by the third-party library before the
/// last one even returns. If the third-party library is not hardened against reentry, which would be
/// unusual, you will get bugs that are very difficult to reproduce and diagnose.
/// </para>
/// <para>
/// It is typically expensive to create wait handles, so for managed objects only access the wait handle
/// property and call this method after you have checked the managed properties such as
/// <see cref="ManualResetEventSlim.IsSet"/> or <see cref="CancellationToken.IsCancellationRequested"/>.
/// </para>
/// </summary>
/// <param name="millisecondsTimeout">
/// <see cref="Timeout.Infinite"/> for no timeout, 0 to return immediately without going into wait mode, and any other value to specify how many milliseconds to wait.
/// </param>
/// <param name="waitHandles">
/// It is typically expensive to create wait handles, so for managed objects only access the wait handle
/// property and call this method after you have checked the managed properties such as
/// <see cref="ManualResetEventSlim.IsSet"/> or <see cref="CancellationToken.IsCancellationRequested"/>.
/// </param>
public static int WaitAnyAndPump(int millisecondsTimeout, params WaitHandle[] waitHandles)
{
if (waitHandles == null) throw new ArgumentNullException(nameof(waitHandles));
if (waitHandles.Length == 0) throw new ArgumentException("There must be at least one wait handle.", nameof(waitHandles));
waitHandles = (WaitHandle[])waitHandles.Clone(); // Defend against mutation
var startTick = Environment.TickCount;
using (var handles = new SafeHandleArray(waitHandles.Select(_ => _.SafeWaitHandle)))
{
while (true)
{
var timeout = (uint)(Timeout.Infinite == millisecondsTimeout ?
Timeout.Infinite :
Math.Max(0, millisecondsTimeout + startTick - Environment.TickCount));
var result = MsgWaitForMultipleObjectsEx((uint)handles.Length, handles, timeout, QS.ALLINPUT, MWMO.INPUTAVAILABLE);
if (result == WAIT.FAILED) throw new Win32Exception();
if (result == WAIT.TIMEOUT) return -1;
var signaledHandleIndex = (int)(result - WAIT.OBJECT_0);
if (signaledHandleIndex >= 0 && signaledHandleIndex < handles.Length)
return signaledHandleIndex;
if (signaledHandleIndex == handles.Length) // Message is available
{
// Prefer WaitHandle signal
for (var i = 0; i < waitHandles.Length; i++)
{
try
{
if (waitHandles[i].WaitOne(0))
return i;
}
catch (AbandonedMutexException)
{
throw new AbandonedMutexException(i, waitHandles[i]);
}
}
// Then prefer to time out
if (millisecondsTimeout != Timeout.Infinite && (uint)startTick + (uint)millisecondsTimeout <= (uint)Environment.TickCount)
return -1;
// No signal, abandonment, or timeout- process messages
GetPumpActionForContext(SynchronizationContext.Current)?.Invoke();
continue;
}
var abandonedHandleIndex = (int)(result - WAIT.ABANDONED_0);
if (abandonedHandleIndex >= 0 && abandonedHandleIndex < handles.Length)
throw new AbandonedMutexException(abandonedHandleIndex, waitHandles[abandonedHandleIndex]);
throw new Win32Exception("Unknown MsgWaitForMultipleObjectsEx return " + result);
}
}
}
/// <summary>
/// <para>
/// Experimental. Returns the index in the <paramref name="waitHandles"/> array of the first <see cref="WaitHandle"/> to signal.
/// Blocks the current thread but pumps all thread messages so that any await continuations or UI on
/// the current thread can run as usual, preventing deadlocks and frozen UI.
/// </para>
/// <para>
/// Use with caution. This is no more dangerous than ShowDialog() or DoEvents(), but what will happen
/// is that thread messages will be handled recursively inside the current stack. This results in
/// reentry of UI event handlers.
/// For example, a third party library might be in the process of handling a mouse event when you
/// use .Await() to keep the event hander from returning until you have a value. While waiting,
/// .Await() receives the next mouse event which gets handled by the third-party library before the
/// last one even returns. If the third-party library is not hardened against reentry, which would be
/// unusual, you will get bugs that are very difficult to reproduce and diagnose.
/// </para>
/// <para>
/// It is typically expensive to create wait handles, so for managed objects only access the wait handle
/// property and call this method after you have checked the managed properties such as
/// <see cref="ManualResetEventSlim.IsSet"/> or <see cref="CancellationToken.IsCancellationRequested"/>.
/// </para>
/// </summary>
/// <param name="waitHandles">
/// It is typically expensive to create wait handles, so for managed objects only access the wait handle
/// property and call this method after you have checked the managed properties such as
/// <see cref="ManualResetEventSlim.IsSet"/> or <see cref="CancellationToken.IsCancellationRequested"/>.
/// </param>
public static int WaitAnyAndPump(params WaitHandle[] waitHandles) => WaitAnyAndPump(Timeout.Infinite, waitHandles);
#region Native methods
// ReSharper disable InconsistentNaming
[DllImport("user32.dll", SetLastError = true)]
private static extern WAIT MsgWaitForMultipleObjectsEx(uint nCount, [In] SafeHandleArray pHandles, uint dwMilliseconds, QS dwWakeMask, MWMO dwFlags);
private enum QS : uint
{
ALLINPUT = 0x04FF
}
[Flags]
private enum MWMO : uint
{
INPUTAVAILABLE = 0x4
}
private enum WAIT : uint
{
OBJECT_0 = 0,
ABANDONED_0 = 0x80,
TIMEOUT = 0x102,
FAILED = uint.MaxValue
}
// ReSharper restore InconsistentNaming
#endregion
#endregion
private static readonly Lazy<Type, Action> PumpWinForms = new Lazy<Type, Action>(synchronizationContextType => Reflect.CompileMethod<Action>("Pump available WinForms messages", typeof(AsyncSimulatorExperimental), il =>
{
var applicationType = synchronizationContextType.Assembly.GetType("System.Windows.Forms.Application", true);
il.Emit(OpCodes.Call, applicationType.GetMethod("DoEvents", BindingFlags.Static | BindingFlags.Public, null, Type.EmptyTypes, null));
#if ASYNC_SIMULATOR_RAISE_IDLE
il.Emit(OpCodes.Ldsfld, typeof(EventArgs).GetField(nameof(EventArgs.Empty), BindingFlags.Static | BindingFlags.Public));
il.Emit(OpCodes.Call, applicationType.GetMethod("RaiseIdle", BindingFlags.Static | BindingFlags.Public, null, new[] { typeof(EventArgs) }, null));
#endif
il.Emit(OpCodes.Ret);
}));
private static Delegate pumpWpfExitFrame;
private static readonly Lazy<Type, Action> PumpWpf = new Lazy<Type, Action>(synchronizationContextType =>
{
var dispatcherOperationCallbackType = synchronizationContextType.Assembly.GetType("System.Windows.Threading.DispatcherOperationCallback");
var dispatcherFrameType = synchronizationContextType.Assembly.GetType("System.Windows.Threading.DispatcherFrame", true);
pumpWpfExitFrame = Reflect.CompileMethod(dispatcherOperationCallbackType, "Pump available WPF messages.ExitFrame", typeof(AsyncSimulatorExperimental), il =>
{
// See https://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher.pushframe.aspx
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Castclass, dispatcherFrameType);
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Callvirt, dispatcherFrameType.GetMethod("set_Continue"));
il.Emit(OpCodes.Ldnull);
il.Emit(OpCodes.Ret);
});
return Reflect.CompileMethod<Action>("Pump available WPF messages", typeof(AsyncSimulatorExperimental), il =>
{
// See https://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher.pushframe.aspx
var dispatcherType = synchronizationContextType.Assembly.GetType("System.Windows.Threading.Dispatcher", true);
il.DeclareLocal(dispatcherFrameType);
il.Emit(OpCodes.Newobj, dispatcherFrameType.GetConstructor(Type.EmptyTypes));
il.Emit(OpCodes.Stloc_0);
il.Emit(OpCodes.Call, dispatcherType.GetMethod("get_CurrentDispatcher"));
il.Emit(OpCodes.Ldc_I4_4); // DispatcherPriority.Background
il.Emit(OpCodes.Ldsfld, typeof(AsyncSimulatorExperimental).GetField(nameof(pumpWpfExitFrame), BindingFlags.Static | BindingFlags.NonPublic));
il.Emit(OpCodes.Castclass, dispatcherOperationCallbackType);
il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Callvirt, dispatcherType.GetMethod("BeginInvoke", new[] { synchronizationContextType.Assembly.GetType("System.Windows.Threading.DispatcherPriority", true), dispatcherOperationCallbackType, dispatcherFrameType }));
il.Emit(OpCodes.Pop);
il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Call, dispatcherType.GetMethod("PushFrame", new[] { dispatcherFrameType }));
il.Emit(OpCodes.Ret);
});
});
private static Action GetPumpActionForContext(SynchronizationContext context)
{
if (context == null || context.GetType() == typeof(SynchronizationContext)) return null;
var type = context.GetType();
if (type.FullName == "System.Windows.Forms.WindowsFormsSynchronizationContext" && type.Assembly.GetName().Name == "System.Windows.Forms")
return PumpWinForms.GetValue(type);
if (type.FullName == "System.Windows.Threading.DispatcherSynchronizationContext" && type.Assembly.GetName().Name == "WindowsBase")
return PumpWpf.GetValue(type);
throw new NotImplementedException($"Unknown synchronization context {type.AssemblyQualifiedName}.");
}
/// <summary>
/// <para>
/// Experimental. Do not use if you have an alternative. For catch and finally blocks, use C# 6 and await. For property setters, replace with a method if possible. If not, have the setter call an async void method On{PropertyName}Changed() or TrySet{PropertyName}(value) and use await inside that.
/// Simulates an await by running the message pump for the current thread, but does not return until the task ends.
/// </para>
/// <para>
/// Blocks the current thread but pumps all thread messages so that any await continuations or UI on
/// the current thread can run as usual, preventing deadlocks and frozen UI.
/// </para>
/// <para>
/// Use with caution. This is no more dangerous than ShowDialog() or DoEvents(), but what will happen
/// is that thread messages will be handled recursively inside the current stack. This results in
/// reentry of UI event handlers.
/// For example, a third party library might be in the process of handling a mouse event when you
/// use .Await() to keep the event hander from returning until you have a value. While waiting,
/// .Await() receives the next mouse event which gets handled by the third-party library before the
/// last one even returns. If the third-party library is not hardened against reentry, which would be
/// unusual, you will get bugs that are very difficult to reproduce and diagnose.
/// </para>
/// </summary>
[DebuggerNonUserCode]
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Experimental. Do not use if you have an alternative. For catch and finally blocks, use C# 6 and await. For property setters, replace with a method if possible. If not, have the setter call an async void method On{PropertyName}Changed() or TrySet{PropertyName}(value) and use await inside that.")]
public static void Await(this Task task)
{
var awaiter = task.GetAwaiter();
if (!awaiter.IsCompleted)
{
using (var completeEvent = new ManualResetEvent(false))
{
awaiter.OnCompleted(() => completeEvent.Set());
WaitAnyAndPump(completeEvent);
}
}
awaiter.GetResult();
}
/// <summary>
/// <para>
/// Experimental. Do not use if you have an alternative. For catch and finally blocks, use C# 6 and await. For property setters, replace with a method if possible. If not, have the setter call an async void method On{PropertyName}Changed() or TrySet{PropertyName}(value) and use await inside that.
/// Simulates an await by running the message pump for the current thread, but does not return until the task ends.
/// </para>
/// <para>
/// Blocks the current thread but pumps all thread messages so that any await continuations or UI on
/// the current thread can run as usual, preventing deadlocks and frozen UI.
/// </para>
/// <para>
/// Use with caution. This is no more dangerous than ShowDialog() or DoEvents(), but what will happen
/// is that thread messages will be handled recursively inside the current stack. This results in
/// reentry of UI event handlers.
/// For example, a third party library might be in the process of handling a mouse event when you
/// use .Await() to keep the event hander from returning until you have a value. While waiting,
/// .Await() receives the next mouse event which gets handled by the third-party library before the
/// last one even returns. If the third-party library is not hardened against reentry, which would be
/// unusual, you will get bugs that are very difficult to reproduce and diagnose.
/// </para>
/// </summary>
[DebuggerNonUserCode]
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Experimental. Do not use if you have an alternative. For catch and finally blocks, use C# 6 and await. For property setters, replace with a method if possible. If not, have the setter call an async void method On{PropertyName}Changed() or TrySet{PropertyName}(value) and use await inside that.")]
public static T Await<T>(this Task<T> task)
{
var awaiter = task.GetAwaiter();
if (!awaiter.IsCompleted)
{
using (var completeEvent = new ManualResetEvent(false))
{
awaiter.OnCompleted(() => completeEvent.Set());
WaitAnyAndPump(completeEvent);
}
}
return awaiter.GetResult();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment