alap
This commit is contained in:
45
Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs
Normal file
45
Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs
Normal file
@ -0,0 +1,45 @@
|
||||
// API consistent with Microsoft's ObjectPool<T>.
|
||||
// thread safe.
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public static class ConcurrentNetworkWriterPool
|
||||
{
|
||||
// initial capacity to avoid allocations in the first few frames
|
||||
// 1000 * 1200 bytes = around 1 MB.
|
||||
public const int InitialCapacity = 1000;
|
||||
|
||||
|
||||
// reuse ConcurrentPool<T>
|
||||
// we still wrap it in NetworkWriterPool.Get/Recycle so we can reset the
|
||||
// position before reusing.
|
||||
// this is also more consistent with NetworkReaderPool where we need to
|
||||
// assign the internal buffer before reusing.
|
||||
static readonly ConcurrentPool<ConcurrentNetworkWriterPooled> pool =
|
||||
new ConcurrentPool<ConcurrentNetworkWriterPooled>(
|
||||
// new object function
|
||||
() => new ConcurrentNetworkWriterPooled(),
|
||||
// initial capacity to avoid allocations in the first few frames
|
||||
// 1000 * 1200 bytes = around 1 MB.
|
||||
InitialCapacity
|
||||
);
|
||||
|
||||
// pool size access for debugging & tests
|
||||
public static int Count => pool.Count;
|
||||
|
||||
public static ConcurrentNetworkWriterPooled Get()
|
||||
{
|
||||
// grab from pool & reset position
|
||||
ConcurrentNetworkWriterPooled writer = pool.Get();
|
||||
writer.Position = 0;
|
||||
return writer;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Return(ConcurrentNetworkWriterPooled writer)
|
||||
{
|
||||
pool.Return(writer);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fdf46e334f52400c854c9732f6fcf005
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>Pooled (not threadsafe) NetworkWriter used from Concurrent pool (thread safe). Automatically returned to concurrent pool when using 'using'</summary>
|
||||
// TODO make sealed again after removing obsolete NetworkWriterPooled!
|
||||
public class ConcurrentNetworkWriterPooled : NetworkWriter, IDisposable
|
||||
{
|
||||
public void Dispose() => ConcurrentNetworkWriterPool.Return(this);
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9163d963b36b4e389318f312bfd8e488
|
||||
timeCreated: 1691485295
|
44
Assets/Mirror/Core/Threading/ConcurrentPool.cs
Normal file
44
Assets/Mirror/Core/Threading/ConcurrentPool.cs
Normal file
@ -0,0 +1,44 @@
|
||||
// Pool to avoid allocations (from libuv2k)
|
||||
// API consistent with Microsoft's ObjectPool<T>.
|
||||
// concurrent for thread safe access.
|
||||
//
|
||||
// currently not in use. keep it in case we need it again.
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public class ConcurrentPool<T>
|
||||
{
|
||||
// Mirror is single threaded, no need for concurrent collections
|
||||
// concurrent bag is for items who's order doesn't matter.
|
||||
// just about right for our use case here.
|
||||
readonly ConcurrentBag<T> objects = new ConcurrentBag<T>();
|
||||
|
||||
// some types might need additional parameters in their constructor, so
|
||||
// we use a Func<T> generator
|
||||
readonly Func<T> objectGenerator;
|
||||
|
||||
public ConcurrentPool(Func<T> objectGenerator, int initialCapacity)
|
||||
{
|
||||
this.objectGenerator = objectGenerator;
|
||||
|
||||
// allocate an initial pool so we have fewer (if any)
|
||||
// allocations in the first few frames (or seconds).
|
||||
for (int i = 0; i < initialCapacity; ++i)
|
||||
objects.Add(objectGenerator());
|
||||
}
|
||||
|
||||
// take an element from the pool, or create a new one if empty
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public T Get() => objects.TryTake(out T obj) ? obj : objectGenerator();
|
||||
|
||||
// return an element to the pool
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Return(T item) => objects.Add(item);
|
||||
|
||||
// count to see how many objects are in the pool. useful for tests.
|
||||
public int Count => objects.Count;
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/Threading/ConcurrentPool.cs.meta
Normal file
11
Assets/Mirror/Core/Threading/ConcurrentPool.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed304bd790ff478ca37233f66d04d1c6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
112
Assets/Mirror/Core/Threading/ThreadLog.cs
Normal file
112
Assets/Mirror/Core/Threading/ThreadLog.cs
Normal file
@ -0,0 +1,112 @@
|
||||
// threaded Debug.Log support (mischa 2022)
|
||||
//
|
||||
// Editor shows Debug.Logs from different threads.
|
||||
// Builds don't show Debug.Logs from different threads.
|
||||
//
|
||||
// need to hook into logMessageReceivedThreaded to receive them in builds too.
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public static class ThreadLog
|
||||
{
|
||||
// queue log messages from threads
|
||||
struct LogEntry
|
||||
{
|
||||
public int threadId;
|
||||
public LogType type;
|
||||
public string message;
|
||||
public string stackTrace;
|
||||
|
||||
public LogEntry(int threadId, LogType type, string message, string stackTrace)
|
||||
{
|
||||
this.threadId = threadId;
|
||||
this.type = type;
|
||||
this.message = message;
|
||||
this.stackTrace = stackTrace;
|
||||
}
|
||||
}
|
||||
|
||||
// ConcurrentQueue allocations are fine here.
|
||||
// logs allocate anywway.
|
||||
static readonly ConcurrentQueue<LogEntry> logs =
|
||||
new ConcurrentQueue<LogEntry>();
|
||||
|
||||
// main thread id
|
||||
static int mainThreadId;
|
||||
|
||||
#if !UNITY_EDITOR
|
||||
// Editor as of Unity 2021 does log threaded messages.
|
||||
// only builds don't.
|
||||
// do nothing in editor, otherwise we would log twice.
|
||||
// before scene load ensures thread logs are all caught.
|
||||
// otherwise some component's Awake may be called before we hooked it up.
|
||||
// for example, ThreadedTransport's early logs wouldn't be caught.
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
static void Initialize()
|
||||
{
|
||||
|
||||
// set main thread id
|
||||
mainThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
// receive threaded log calls
|
||||
Application.logMessageReceivedThreaded -= OnLog; // remove old first. TODO unnecessary?
|
||||
Application.logMessageReceivedThreaded += OnLog;
|
||||
|
||||
// process logs on main thread Update
|
||||
NetworkLoop.OnLateUpdate -= OnLateUpdate; // remove old first. TODO unnecessary?
|
||||
NetworkLoop.OnLateUpdate += OnLateUpdate;
|
||||
|
||||
// log for debugging
|
||||
Debug.Log("ThreadLog initialized.");
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool IsMainThread() =>
|
||||
Thread.CurrentThread.ManagedThreadId == mainThreadId;
|
||||
|
||||
// callback runs on the same thread where the Debug.Log is called.
|
||||
// we can use this to buffer messages for main thread here.
|
||||
static void OnLog(string message, string stackTrace, LogType type)
|
||||
{
|
||||
// only enqueue messages from other threads.
|
||||
// otherwise OnLateUpdate main thread logging would be enqueued
|
||||
// as well, causing deadlock.
|
||||
if (IsMainThread()) return;
|
||||
|
||||
// queue for logging from main thread later
|
||||
logs.Enqueue(new LogEntry(Thread.CurrentThread.ManagedThreadId, type, message, stackTrace));
|
||||
}
|
||||
|
||||
static void OnLateUpdate()
|
||||
{
|
||||
// process queued logs on main thread
|
||||
while (logs.TryDequeue(out LogEntry entry))
|
||||
{
|
||||
switch (entry.type)
|
||||
{
|
||||
// add [Thread#] prefix to make it super obvious where this log message comes from.
|
||||
// some projects may see unexpected messages that were previously hidden,
|
||||
// since Unity wouldn't log them without ThreadLog.cs.
|
||||
case LogType.Log:
|
||||
Debug.Log($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}");
|
||||
break;
|
||||
case LogType.Warning:
|
||||
Debug.LogWarning($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}");
|
||||
break;
|
||||
case LogType.Error:
|
||||
Debug.LogError($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}");
|
||||
break;
|
||||
case LogType.Exception:
|
||||
Debug.LogError($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}");
|
||||
break;
|
||||
case LogType.Assert:
|
||||
Debug.LogAssertion($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/Threading/ThreadLog.cs.meta
Normal file
11
Assets/Mirror/Core/Threading/ThreadLog.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22360406b3844808b0a305486758a703
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
165
Assets/Mirror/Core/Threading/WorkerThread.cs
Normal file
165
Assets/Mirror/Core/Threading/WorkerThread.cs
Normal file
@ -0,0 +1,165 @@
|
||||
// worker thread for Unity (mischa 2022)
|
||||
// thread with proper exception handling, profling, init, cleanup, etc. for Unity.
|
||||
// use this from main thread.
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using UnityEngine.Profiling;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public class WorkerThread
|
||||
{
|
||||
readonly Thread thread;
|
||||
|
||||
protected volatile bool active;
|
||||
|
||||
// stopwatch so we don't need to use Unity's Time (engine independent)
|
||||
readonly Stopwatch watch = new Stopwatch();
|
||||
|
||||
// callbacks need to be set after constructor.
|
||||
// inheriting classes can't pass their member funcs to base ctor.
|
||||
// don't set them while the thread is running!
|
||||
public Action Init;
|
||||
public Action Tick;
|
||||
public Action Cleanup;
|
||||
|
||||
public WorkerThread(string identifier)
|
||||
{
|
||||
// start the thread wrapped in safety guard
|
||||
// if main application terminates, this thread needs to terminate too
|
||||
thread = new Thread(
|
||||
() => Guard(identifier)
|
||||
);
|
||||
thread.IsBackground = true;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
// only if thread isn't already running
|
||||
if (thread.IsAlive)
|
||||
{
|
||||
Debug.LogWarning("WorkerThread is still active, can't start it again.");
|
||||
return;
|
||||
}
|
||||
|
||||
active = true;
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
// signal the thread to stop gracefully.
|
||||
// returns immediately, but the thread may take a while to stop.
|
||||
// may be overwritten to clear more flags like 'computing' etc.
|
||||
public virtual void SignalStop() => active = false;
|
||||
|
||||
// wait for the thread to fully stop
|
||||
public bool StopBlocking(float timeout)
|
||||
{
|
||||
// only if alive
|
||||
if (!thread.IsAlive) return true;
|
||||
|
||||
// double precision for long running servers.
|
||||
watch.Restart();
|
||||
|
||||
// signal to stop
|
||||
SignalStop();
|
||||
|
||||
// wait while thread is still alive
|
||||
while (IsAlive)
|
||||
{
|
||||
// simply wait..
|
||||
Thread.Sleep(0);
|
||||
|
||||
// deadlock detection
|
||||
if (watch.Elapsed.TotalSeconds >= timeout)
|
||||
{
|
||||
// force kill all threads as last resort to stop them.
|
||||
// return false to indicate deadlock.
|
||||
Interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsAlive => thread.IsAlive;
|
||||
|
||||
// signal an interrupt in the thread.
|
||||
// this function is very safe to use.
|
||||
// https://stackoverflow.com/questions/5950994/thread-abort-vs-thread-interrupt
|
||||
//
|
||||
// note this does not always kill the thread:
|
||||
// "If this thread is not currently blocked in a wait, sleep, or join
|
||||
// state, it will be interrupted when it next begins to block."
|
||||
// https://docs.microsoft.com/en-us/dotnet/api/system.threading.thread.interrupt?view=net-6.0
|
||||
//
|
||||
// in other words, "while (true) {}" wouldn't throw an interrupt exception.
|
||||
// and that's _okay_. using interrupt is safe & best practice.
|
||||
// => Unity still aborts deadlocked threads on script reload.
|
||||
// => and we catch + warn on AbortException.
|
||||
public void Interrupt() => thread.Interrupt();
|
||||
|
||||
// thread constructor needs callbacks.
|
||||
// always define them, and make them call actions.
|
||||
// those can be set at any time.
|
||||
void OnInit() => Init?.Invoke();
|
||||
void OnTick() => Tick?.Invoke();
|
||||
void OnCleanup() => Cleanup?.Invoke();
|
||||
|
||||
// guarded wrapper for thread code.
|
||||
// catches exceptions which would otherwise be silent.
|
||||
// shows in Unity profiler.
|
||||
// etc.
|
||||
public void Guard(string identifier)
|
||||
{
|
||||
try
|
||||
{
|
||||
// log when work begins = thread starts.
|
||||
// very important for debugging threads.
|
||||
Debug.Log($"{identifier}: started.");
|
||||
|
||||
// show this thread in Unity profiler
|
||||
Profiler.BeginThreadProfiling("Mirror Worker Threads", $"{identifier}");
|
||||
|
||||
// run init once
|
||||
OnInit();
|
||||
|
||||
// run thread func while active
|
||||
while (active)
|
||||
{
|
||||
OnTick();
|
||||
}
|
||||
}
|
||||
// Thread.Interrupt() will gracefully raise a InterruptedException.
|
||||
catch (ThreadInterruptedException)
|
||||
{
|
||||
Debug.Log($"{identifier}: interrupted. That's okay.");
|
||||
}
|
||||
// Unity domain reload will cause a ThreadAbortException.
|
||||
// for example, when saving a changed script while in play mode.
|
||||
catch (ThreadAbortException)
|
||||
{
|
||||
Debug.LogWarning($"{identifier}: aborted. This may happen after domain reload. That's okay.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogException(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// run cleanup (if any)
|
||||
active = false;
|
||||
OnCleanup();
|
||||
|
||||
// remove this thread from Unity profiler
|
||||
Profiler.EndThreadProfiling();
|
||||
|
||||
// log when work ends = thread terminates.
|
||||
// very important for debugging threads.
|
||||
// 'finally' to log no matter what (even if exceptions)
|
||||
Debug.Log($"{identifier}: ended.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/Threading/WorkerThread.cs.meta
Normal file
11
Assets/Mirror/Core/Threading/WorkerThread.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 605fa1d7e32f40a08e5549bb43fc5c07
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Reference in New Issue
Block a user