Improve thread safety and performance in core systems

Refactors event, entity, and physics management to use thread-safe and lock-free data structures (Immutable collections, ConcurrentQueue, ConcurrentDictionary, Channel) for improved concurrency and performance. Replaces O(n) queue lookups with O(1) set/dictionary checks, ensures atomic updates for shared state, and optimizes queue draining and deferred action processing. Updates related code to use new APIs and patterns, and adds documentation for thread safety and workflow.
This commit is contained in:
Eero
2025-12-29 16:47:10 +08:00
parent e167a34f32
commit 7b8275100d
17 changed files with 368 additions and 191 deletions
@@ -1,34 +1,51 @@
using System;
using System.Collections.Generic;
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Channels;
namespace Barotrauma
{
/// <summary>
/// Thread-safe queue for deferring physics operations to the main thread.
/// High-performance lock-free thread-safe queue for deferring physics operations to the main thread.
/// This is necessary because Farseer Physics' DynamicTree is not thread-safe,
/// and physics operations cannot be safely performed during parallel updates.
///
/// Uses System.Threading.Channels for optimal throughput with single-reader pattern.
/// Channel&lt;T&gt; provides better performance than ConcurrentQueue in producer-consumer scenarios.
///
/// Supported operations include:
/// - Physics body creation
/// - Physics body transform updates (SetTransform, SetTransformIgnoreContacts)
/// - Any other operation that modifies the Farseer physics world
/// </summary>
/// <start>
/// ├─> PhysicsBodyQueue.IsInParallelContext = true (ThreadStatic)
/// ├─> Item.Update()
/// │ └─> StatusEffect.Apply()
/// │ └─> Character.Kill()
/// │ └─> Item.Drop()
/// │ └─> Check if IsInParallelContext == true
/// │ └─> PhysicsBodyQueue.Enqueue(Physics operation)
/// ├──> PhysicsBodyQueue.IsInParallelContext = false
/// └──> PhysicsBodyQueue.ProcessPendingOperations() ← Main thread executes
/// └─> body.SetTransformIgnoreContacts()
/// <remarks>
/// Workflow:
/// <code>
/// ├─> PhysicsBodyQueue.IsInParallelContext = true (ThreadStatic)
/// ├─> Item.Update()
/// │ └─> StatusEffect.Apply()
/// │ └─> Character.Kill()
/// │ └─> Item.Drop()
/// │ └─> Check if IsInParallelContext == true
/// │ └─> PhysicsBodyQueue.Enqueue(Physics operation)
/// ├──> PhysicsBodyQueue.IsInParallelContext = false
/// └──> PhysicsBodyQueue.ProcessPendingOperations() ← Main thread executes
/// └─> body.SetTransformIgnoreContacts()
/// </code>
/// </remarks>
static class PhysicsBodyQueue
{
private static readonly object _lock = new object();
private static readonly Queue<Action> _pendingOperations = new Queue<Action>();
// High-performance unbounded channel optimized for single-reader scenario
private static readonly Channel<Action> _channel = Channel.CreateUnbounded<Action>(
new UnboundedChannelOptions
{
SingleReader = true, // Only main thread reads - enables optimizations
SingleWriter = false, // Multiple parallel threads may write
AllowSynchronousContinuations = false // Prevent stack dives, improve throughput
});
private static readonly ChannelWriter<Action> _writer = _channel.Writer;
private static readonly ChannelReader<Action> _reader = _channel.Reader;
/// <summary>
/// Thread-local flag indicating whether the current thread is in a parallel physics update context.
@@ -49,21 +66,20 @@ namespace Barotrauma
/// <summary>
/// Enqueues a physics operation to be executed on the main thread.
/// This method is thread-safe and can be called from parallel update loops.
/// This method is lock-free and can be safely called from parallel update loops.
/// Uses Channel's optimized TryWrite which is faster than ConcurrentQueue.Enqueue.
/// </summary>
/// <param name="operation">The physics operation to defer</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Enqueue(Action operation)
{
if (operation == null) { return; }
lock (_lock)
{
_pendingOperations.Enqueue(operation);
}
_writer.TryWrite(operation);
}
/// <summary>
/// Enqueues a physics body creation action to be executed on the main thread.
/// This method is thread-safe and can be called from parallel update loops.
/// This method is lock-free and can be safely called from parallel update loops.
/// </summary>
/// <param name="createAction">The action that creates the physics body</param>
public static void EnqueueCreation(Action createAction)
@@ -75,50 +91,49 @@ namespace Barotrauma
/// Executes a physics operation, either immediately or deferred depending on context.
/// If called from a parallel context, the operation will be queued for later execution.
/// If called from the main thread (outside parallel loops), the operation executes immediately.
///
/// Hot path optimization: Most calls occur outside parallel context, so we check
/// the non-parallel case first to improve branch prediction.
/// </summary>
/// <param name="operation">The physics operation to execute</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ExecuteOrDefer(Action operation)
{
if (operation == null) { return; }
if (_isInParallelContext)
{
Enqueue(operation);
}
else
// Hot path: Most calls are outside parallel context - execute immediately
if (!_isInParallelContext)
{
operation();
return;
}
// Cold path: In parallel context - defer to queue
_writer.TryWrite(operation);
}
/// <summary>
/// Gets the number of pending physics operations.
/// Gets whether there are any pending physics operations.
/// This is an O(1) operation.
/// </summary>
public static int PendingCount
{
get
{
lock (_lock)
{
return _pendingOperations.Count;
}
}
}
public static bool HasPending => _reader.TryPeek(out _);
/// <summary>
/// Gets the approximate number of pending physics operations.
/// Note: This may have some overhead compared to the previous atomic counter.
/// Use HasPending for simple empty checks.
/// </summary>
public static int PendingCount => _reader.Count;
/// <summary>
/// Processes all pending physics operations.
/// Must be called on the main thread, outside of any parallel loops.
/// Uses Channel's optimized TryRead for single-reader scenario.
/// </summary>
public static void ProcessPendingOperations()
{
while (true)
while (_reader.TryRead(out Action action))
{
Action action;
lock (_lock)
{
if (_pendingOperations.Count == 0) { break; }
action = _pendingOperations.Dequeue();
}
try
{
action?.Invoke();
@@ -145,11 +160,7 @@ namespace Barotrauma
/// </summary>
public static void Clear()
{
lock (_lock)
{
_pendingOperations.Clear();
}
while (_reader.TryRead(out _)) { }
}
}
}