ThreadingHelper.cs 5.1 KB

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