ThreadingHelper.cs 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. using System;
  2. using System.ComponentModel;
  3. using System.Linq;
  4. using System.Threading;
  5. using BepInEx.Logging;
  6. using UnityEngine;
  7. namespace BepInEx
  8. {
  9. /// <summary>
  10. /// Provides methods for running code on other threads and synchronizing with the main thread.
  11. /// </summary>
  12. public sealed class ThreadingHelper : MonoBehaviour, ISynchronizeInvoke
  13. {
  14. private readonly object _invokeLock = new object();
  15. private Action _invokeList;
  16. private Thread _mainThread;
  17. /// <summary>
  18. /// Current instance of the helper.
  19. /// </summary>
  20. public static ThreadingHelper Instance { get; private set; }
  21. /// <summary>
  22. /// Gives methods for invoking delegates on the main unity thread, both synchronously and asynchronously.
  23. /// Can be used in many built-in framework types, for example <see cref="System.IO.FileSystemWatcher.SynchronizingObject"/>
  24. /// and <see cref="System.Timers.Timer.SynchronizingObject"/> to make their events fire on the main unity thread.
  25. /// </summary>
  26. public static ISynchronizeInvoke SynchronizingObject => Instance;
  27. internal static void Initialize()
  28. {
  29. var go = new GameObject("BepInEx_ThreadingHelper");
  30. DontDestroyOnLoad(go);
  31. Instance = go.AddComponent<ThreadingHelper>();
  32. }
  33. /// <summary>
  34. /// Queue the delegate to be invoked on the main unity thread. Use to synchronize your threads.
  35. /// </summary>
  36. public void StartSyncInvoke(Action action)
  37. {
  38. if (action == null) throw new ArgumentNullException(nameof(action));
  39. lock (_invokeLock) _invokeList += action;
  40. }
  41. private void Update()
  42. {
  43. // The CurrentThread can change between Awake and later methods, it's safest to get it here.
  44. if (_mainThread == null)
  45. _mainThread = Thread.CurrentThread;
  46. // Safe to do outside of lock because nothing can remove callbacks, at worst we execute with 1 frame delay
  47. if (_invokeList == null) return;
  48. Action toRun;
  49. lock (_invokeLock)
  50. {
  51. toRun = _invokeList;
  52. _invokeList = null;
  53. }
  54. // Need to execute outside of the lock in case the callback itself calls Invoke we could deadlock
  55. // The invocation would also block any threads that call Invoke
  56. foreach (var action in toRun.GetInvocationList().Cast<Action>())
  57. {
  58. try
  59. {
  60. action();
  61. }
  62. catch (Exception ex)
  63. {
  64. LogInvocationException(ex);
  65. }
  66. }
  67. }
  68. /// <summary>
  69. /// Queue the delegate to be invoked on a background thread. Use this to run slow tasks without affecting the game.
  70. /// NOTE: Most of Unity API can not be accessed while running on another thread!
  71. /// </summary>
  72. /// <param name="action">
  73. /// Task to be executed on another thread. Can optionally return an Action that will be executed on the main thread.
  74. /// You can use this action to return results of your work safely. Return null if this is not needed.
  75. /// </param>
  76. public void StartAsyncInvoke(Func<Action> action)
  77. {
  78. void DoWork(object _)
  79. {
  80. try
  81. {
  82. var result = action();
  83. if (result != null)
  84. StartSyncInvoke(result);
  85. }
  86. catch (Exception ex)
  87. {
  88. LogInvocationException(ex);
  89. }
  90. }
  91. if (!ThreadPool.QueueUserWorkItem(DoWork))
  92. throw new NotSupportedException("Failed to queue the action on ThreadPool");
  93. }
  94. private static void LogInvocationException(Exception ex)
  95. {
  96. Logging.Logger.Log(LogLevel.Error, ex);
  97. if (ex.InnerException != null) Logging.Logger.Log(LogLevel.Error, "INNER: " + ex.InnerException);
  98. }
  99. #region ISynchronizeInvoke
  100. IAsyncResult ISynchronizeInvoke.BeginInvoke(Delegate method, object[] args)
  101. {
  102. var result = new InvokeResult();
  103. object Invoke()
  104. {
  105. try
  106. {
  107. return method.DynamicInvoke(args);
  108. }
  109. catch (Exception ex)
  110. {
  111. result.ExceptionThrown = true;
  112. return ex;
  113. }
  114. }
  115. if (!InvokeRequired)
  116. result.Finish(Invoke(), true);
  117. else
  118. StartSyncInvoke(() => result.Finish(Invoke(), false));
  119. return result;
  120. }
  121. object ISynchronizeInvoke.EndInvoke(IAsyncResult result)
  122. {
  123. var invokeResult = (InvokeResult)result;
  124. invokeResult.AsyncWaitHandle.WaitOne();
  125. if (invokeResult.ExceptionThrown)
  126. throw (Exception)invokeResult.AsyncState;
  127. return invokeResult.AsyncState;
  128. }
  129. object ISynchronizeInvoke.Invoke(Delegate method, object[] args)
  130. {
  131. var invokeResult = ((ISynchronizeInvoke)this).BeginInvoke(method, args);
  132. return ((ISynchronizeInvoke)this).EndInvoke(invokeResult);
  133. }
  134. /// <summary>
  135. /// False if current code is executing on the main unity thread, otherwise True.
  136. /// Warning: Will return true before the first frame finishes (i.e. inside plugin Awake and Start methods).
  137. /// </summary>
  138. /// <inheritdoc />
  139. public bool InvokeRequired => _mainThread == null || _mainThread != Thread.CurrentThread;
  140. private sealed class InvokeResult : IAsyncResult
  141. {
  142. public InvokeResult()
  143. {
  144. AsyncWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
  145. }
  146. public void Finish(object result, bool completedSynchronously)
  147. {
  148. AsyncState = result;
  149. CompletedSynchronously = completedSynchronously;
  150. IsCompleted = true;
  151. ((EventWaitHandle)AsyncWaitHandle).Set();
  152. }
  153. public bool IsCompleted { get; private set; }
  154. public WaitHandle AsyncWaitHandle { get; }
  155. public object AsyncState { get; private set; }
  156. public bool CompletedSynchronously { get; private set; }
  157. internal bool ExceptionThrown;
  158. }
  159. #endregion
  160. }
  161. }