JD2022-TU1/main/tools/framework/JD.Threading/Dispatch/LongOperationDispatchStrategy.cs

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
}
}