using System; using System.ComponentModel; using System.Linq; using System.Threading; using BepInEx.Logging; using UnityEngine; namespace BepInEx { /// /// Provides methods for running code on other threads and synchronizing with the main thread. /// public sealed class ThreadingHelper : MonoBehaviour, ISynchronizeInvoke { private readonly object _invokeLock = new object(); private Action _invokeList; private Thread _mainThread; /// /// Current instance of the helper. /// public static ThreadingHelper Instance { get; private set; } /// /// Gives methods for invoking delegates on the main unity thread, both synchronously and asynchronously. /// Can be used in many built-in framework types, for example /// and to make their events fire on the main unity thread. /// public static ISynchronizeInvoke SynchronizingObject => Instance; internal static void Initialize() { var go = new GameObject("BepInEx_ThreadingHelper"); DontDestroyOnLoad(go); Instance = go.AddComponent(); } /// /// Queue the delegate to be invoked on the main unity thread. Use to synchronize your threads. /// public void StartSyncInvoke(Action action) { if (action == null) throw new ArgumentNullException(nameof(action)); lock (_invokeLock) _invokeList += action; } private void Update() { if (_mainThread == null) _mainThread = Thread.CurrentThread; // Safe to do outside of lock because nothing can remove callbacks, at worst we execute with 1 frame delay if (_invokeList == null) return; Action toRun; lock (_invokeLock) { toRun = _invokeList; _invokeList = null; } // Need to execute outside of the lock in case the callback itself calls Invoke we could deadlock // The invocation would also block any threads that call Invoke foreach (var action in toRun.GetInvocationList().Cast()) { try { action(); } catch (Exception ex) { LogInvocationException(ex); } } } /// /// Queue the delegate to be invoked on a background thread. Use this to run slow tasks without affecting the game. /// NOTE: Most of Unity API can not be accessed while running on another thread! /// /// /// Task to be executed on another thread. Can optionally return an Action that will be executed on the main thread. /// You can use this action to return results of your work safely. Return null if this is not needed. /// public void StartAsyncInvoke(Func action) { void DoWork(object _) { try { var result = action(); if (result != null) StartSyncInvoke(result); } catch (Exception ex) { LogInvocationException(ex); } } if (!ThreadPool.QueueUserWorkItem(DoWork)) throw new NotSupportedException("Failed to queue the action on ThreadPool"); } private static void LogInvocationException(Exception ex) { Logging.Logger.Log(LogLevel.Error, ex); if (ex.InnerException != null) Logging.Logger.Log(LogLevel.Error, "INNER: " + ex.InnerException); } #region ISynchronizeInvoke IAsyncResult ISynchronizeInvoke.BeginInvoke(Delegate method, object[] args) { object Invoke() { try { return method.DynamicInvoke(args); } catch (Exception ex) { return ex; } } var result = new InvokeResult(); if (!InvokeRequired) result.Finish(Invoke(), true); else StartSyncInvoke(() => result.Finish(Invoke(), false)); return result; } object ISynchronizeInvoke.EndInvoke(IAsyncResult result) { result.AsyncWaitHandle.WaitOne(); if (result.AsyncState is Exception ex) throw ex; return result.AsyncState; } object ISynchronizeInvoke.Invoke(Delegate method, object[] args) { var invokeResult = ((ISynchronizeInvoke)this).BeginInvoke(method, args); return ((ISynchronizeInvoke)this).EndInvoke(invokeResult); } /// /// False if current code is executing on the main unity thread, otherwise True. /// Warning: Will return false before the first frame finishes (i.e. inside plugin Awake and Start methods). /// /// public bool InvokeRequired => _mainThread == null || _mainThread != Thread.CurrentThread; private sealed class InvokeResult : IAsyncResult { public InvokeResult() { AsyncWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset); } public void Finish(object result, bool completedSynchronously) { AsyncState = result; CompletedSynchronously = completedSynchronously; IsCompleted = true; ((EventWaitHandle)AsyncWaitHandle).Set(); } public bool IsCompleted { get; private set; } public WaitHandle AsyncWaitHandle { get; } public object AsyncState { get; private set; } public bool CompletedSynchronously { get; private set; } } #endregion } }