513 lines
13 KiB
C#
513 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Reflection;
|
|
using System.Threading;
|
|
using JD.Kernel.Classes;
|
|
using JD.Kernel.StaticHelpers;
|
|
|
|
namespace JD.Threading.Dispatch
|
|
{
|
|
public class LongOperationDispatchStrategy : IThreadDispatchStrategy
|
|
{
|
|
#region Variables
|
|
|
|
private readonly IMessageLoop m_messageLoop;
|
|
private readonly InvocationQueue m_invocationQueue = new InvocationQueue();
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
public LongOperationDispatchStrategy()
|
|
: this(new DefaultMessageLoop())
|
|
{
|
|
}
|
|
|
|
internal LongOperationDispatchStrategy(IMessageLoop messageLoop)
|
|
{
|
|
m_messageLoop = messageLoop;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
#region Interface
|
|
|
|
public bool InvokeRequired
|
|
{
|
|
get { return m_messageLoop.InvokeRequired; }
|
|
}
|
|
|
|
public bool LongOperationInvokeRequired
|
|
{
|
|
get { return !LongOperationThread.IsLongOperationThread && InvokeRequired; }
|
|
}
|
|
|
|
public void Dispatch(System.Action action)
|
|
{
|
|
Throw.IfNull(action, "action");
|
|
|
|
if (LongOperationThread.IsLongOperationThread || !m_messageLoop.InvokeRequired)
|
|
{
|
|
InvokeIfNeeded(action);
|
|
return;
|
|
}
|
|
|
|
CheckedInvocation invocation = new CheckedInvocation(action);
|
|
|
|
// If a long operation is in progress, this will queue it
|
|
if (!m_invocationQueue.TryEnqueue(invocation))
|
|
{
|
|
InvokeIfNeeded(() =>
|
|
{
|
|
// If a long operation is in progress, this will queue it
|
|
if (!m_invocationQueue.TryEnqueue(invocation))
|
|
{
|
|
// No long operation in progress, run it right away since we're already on the UI thread
|
|
action();
|
|
invocation = null; // Set to null to avoid waiting
|
|
}
|
|
});
|
|
}
|
|
|
|
if (invocation != null)
|
|
{
|
|
invocation.WaitHandle.WaitOne();
|
|
invocation.ThrowExceptionIfNeeded();
|
|
}
|
|
}
|
|
|
|
public void DispatchAsync(System.Action action, bool delayEvenIfAlreadyOnGuiThread)
|
|
{
|
|
Throw.IfNull(action, "action");
|
|
|
|
if (!m_messageLoop.InvokeRequired && !delayEvenIfAlreadyOnGuiThread)
|
|
{
|
|
// Can execute right away if already on the UI thread
|
|
action();
|
|
return;
|
|
}
|
|
|
|
Invocation invocation = new Invocation(action);
|
|
if (LongOperationThread.IsLongOperationThread)
|
|
{
|
|
m_invocationQueue.Enqueue(invocation);
|
|
return;
|
|
}
|
|
|
|
if (!m_invocationQueue.TryEnqueue(invocation))
|
|
{
|
|
BeginInvokeIfNeeded(() =>
|
|
{
|
|
// If a long operation is in progress, this will queue it
|
|
if (!m_invocationQueue.TryEnqueue(invocation))
|
|
{
|
|
// No long operation in progress, run it right away since we're already on the UI thread
|
|
action();
|
|
}
|
|
}, delayEvenIfAlreadyOnGuiThread);
|
|
}
|
|
}
|
|
|
|
public void DoLongOperation(System.Action action)
|
|
{
|
|
Throw.IfNull(action, "action");
|
|
|
|
if (LongOperationThread.IsLongOperationThread)
|
|
{
|
|
action();
|
|
}
|
|
else
|
|
{
|
|
InvokeIfNeeded(() => StartLongOperation(action));
|
|
}
|
|
}
|
|
|
|
public void DoLongOperationAsync(System.Action action)
|
|
{
|
|
Throw.IfNull(action, "action");
|
|
DispatchAsync(() => StartLongOperation(action), true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This method should block all windows messages that can cause re-entrancy on the UI thread.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Should block: WM_PAINT, WM_MOUSEXYZ
|
|
/// Should not block: Invoke/BeginInvoke messages
|
|
/// </remarks>
|
|
/// <returns></returns>
|
|
protected virtual IDisposable LockUI()
|
|
{
|
|
return null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Implementation
|
|
|
|
private void StartLongOperation(System.Action action)
|
|
{
|
|
Debug.Assert(!m_messageLoop.InvokeRequired, "This method should always be called from the UI thread");
|
|
|
|
using (var longOperationThread = new LongOperationThread(action))
|
|
{
|
|
using (DeclareLongOperation())
|
|
{
|
|
longOperationThread.Start();
|
|
m_messageLoop.WaitAndPump(longOperationThread.WaitHandle);
|
|
}
|
|
|
|
// Throw AFTER all pending invocations have been processed
|
|
longOperationThread.ThrowExceptionIfNeeded();
|
|
}
|
|
}
|
|
|
|
private IDisposable DeclareLongOperation()
|
|
{
|
|
Debug.Assert(!m_messageLoop.InvokeRequired, "This method should always be called from the UI thread");
|
|
|
|
var uiLockScope = LockUI();
|
|
|
|
bool rootOperation = m_invocationQueue.StartLongOperation();
|
|
|
|
return new DisposableHelper(() =>
|
|
{
|
|
DisposableHelper.SafeDispose(uiLockScope);
|
|
|
|
if (rootOperation)
|
|
{
|
|
ProcessInvocationQueue();
|
|
}
|
|
});
|
|
}
|
|
|
|
private void ProcessInvocationQueue()
|
|
{
|
|
Debug.Assert(!m_messageLoop.InvokeRequired, "This method should always be called from the UI thread");
|
|
|
|
Invocation invocation;
|
|
while ((invocation = m_invocationQueue.Dequeue()) != null)
|
|
{
|
|
invocation.Run(); // Can cause re-entrancy in StartLongOperation
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Utilities
|
|
|
|
private void InvokeIfNeeded(System.Action action)
|
|
{
|
|
Debug.Assert(action != null);
|
|
|
|
if (m_messageLoop.InvokeRequired)
|
|
{
|
|
Exception ex = null;
|
|
|
|
m_messageLoop.Invoke(() =>
|
|
{
|
|
try
|
|
{
|
|
action();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
ex = e;
|
|
}
|
|
});
|
|
|
|
if (ex != null)
|
|
{
|
|
throw new TargetInvocationException("An exception occurred while invoking an action on the UI thread", ex);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
action();
|
|
}
|
|
}
|
|
|
|
private void BeginInvokeIfNeeded(System.Action action, bool forceDelay)
|
|
{
|
|
Debug.Assert(action != null);
|
|
|
|
if (m_messageLoop.InvokeRequired || forceDelay)
|
|
{
|
|
m_messageLoop.BeginInvoke(action);
|
|
}
|
|
else
|
|
{
|
|
action();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#endregion
|
|
|
|
#region Events
|
|
|
|
public static event ThreadExceptionEventHandler ThreadException;
|
|
|
|
private static void OnThreadException(Exception e)
|
|
{
|
|
var handler = ThreadException;
|
|
if (handler != null)
|
|
{
|
|
handler(null, new ThreadExceptionEventArgs(e));
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Inner Types
|
|
|
|
private class Invocation : IDisposable
|
|
{
|
|
#region Variables
|
|
|
|
private readonly object m_completedLock = new object();
|
|
private readonly System.Action m_action;
|
|
|
|
private bool m_isCompleted;
|
|
private ManualResetEvent m_completedEvent;
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
public Invocation(System.Action action)
|
|
{
|
|
m_action = action;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
public WaitHandle WaitHandle
|
|
{
|
|
get
|
|
{
|
|
if (m_completedEvent == null)
|
|
{
|
|
lock (m_completedLock)
|
|
{
|
|
if (m_completedEvent == null)
|
|
{
|
|
m_completedEvent = new ManualResetEvent(false);
|
|
if (m_isCompleted)
|
|
{
|
|
m_completedEvent.Set();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return m_completedEvent;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
/// <summary>
|
|
/// Must not throw
|
|
/// </summary>
|
|
public void Run()
|
|
{
|
|
try
|
|
{
|
|
RunImpl();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
ReportException(e);
|
|
}
|
|
finally
|
|
{
|
|
Complete();
|
|
}
|
|
}
|
|
|
|
protected virtual void ReportException(Exception exception)
|
|
{
|
|
OnThreadException(exception);
|
|
}
|
|
|
|
protected virtual void RunImpl()
|
|
{
|
|
m_action();
|
|
}
|
|
|
|
private void Complete()
|
|
{
|
|
lock (m_completedLock)
|
|
{
|
|
m_isCompleted = true;
|
|
if (m_completedEvent != null)
|
|
{
|
|
m_completedEvent.Set();
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
private class CheckedInvocation : Invocation
|
|
{
|
|
#region Variables
|
|
|
|
private Exception m_exception;
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
public CheckedInvocation(System.Action action)
|
|
: base(action)
|
|
{
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
protected override void ReportException(Exception exception)
|
|
{
|
|
m_exception = exception;
|
|
}
|
|
|
|
public void ThrowExceptionIfNeeded()
|
|
{
|
|
if (m_exception != null)
|
|
{
|
|
throw new TargetInvocationException("An exception occurred while invoking an action on the UI thread", m_exception);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
private class LongOperationThread : CheckedInvocation
|
|
{
|
|
#region Variables
|
|
|
|
[ThreadStatic]
|
|
private static bool ms_isLongOperationThread;
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
public LongOperationThread(System.Action action)
|
|
: base(action)
|
|
{
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
public static bool IsLongOperationThread
|
|
{
|
|
get { return ms_isLongOperationThread; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
protected override void RunImpl()
|
|
{
|
|
ms_isLongOperationThread = true;
|
|
base.RunImpl();
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
Thread thread = new Thread(Run)
|
|
{
|
|
Name = "Long Operation Thread"
|
|
};
|
|
ThreadHelper.SetDefaultThreadCulture(thread);
|
|
thread.Start();
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
private class InvocationQueue
|
|
{
|
|
#region Variables
|
|
|
|
private readonly Queue<Invocation> m_queue = new Queue<Invocation>();
|
|
private bool m_inLongOperation;
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
public Invocation Dequeue()
|
|
{
|
|
lock (m_queue)
|
|
{
|
|
if (m_queue.Count > 0)
|
|
{
|
|
return m_queue.Dequeue();
|
|
}
|
|
|
|
m_inLongOperation = false;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public bool StartLongOperation()
|
|
{
|
|
lock (m_queue)
|
|
{
|
|
if (m_inLongOperation)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
m_inLongOperation = true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public bool TryEnqueue(Invocation invocation)
|
|
{
|
|
lock (m_queue)
|
|
{
|
|
if (m_inLongOperation)
|
|
{
|
|
m_queue.Enqueue(invocation);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void Enqueue(Invocation invocation)
|
|
{
|
|
Throw.Assert(TryEnqueue(invocation));
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|