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

View File

@@ -1397,12 +1397,12 @@ namespace Barotrauma
if (me.SimPosition.Length() > 2000.0f)
{
NewMessage("Removed " + me.Name + " (simposition " + me.SimPosition + ")", Color.Orange);
MapEntity.MapEntityList.RemoveAt(i);
MapEntity.MapEntityList.Remove(me);
}
else if (!me.ShouldBeSaved)
{
NewMessage("Removed " + me.Name + " (!ShouldBeSaved)", Color.Orange);
MapEntity.MapEntityList.RemoveAt(i);
MapEntity.MapEntityList.Remove(me);
}
else if (me is Item)
{

View File

@@ -28,7 +28,7 @@ namespace Barotrauma
public void DebugDraw(SpriteBatch spriteBatch)
{
foreach (Event ev in activeEvents)
foreach (Event ev in _activeEvents)
{
Vector2 drawPos = ev.DebugDrawPos;
drawPos.Y = -drawPos.Y;
@@ -41,7 +41,7 @@ namespace Barotrauma
public void DebugDrawHUD(SpriteBatch spriteBatch, float y)
{
foreach (ScriptedEvent scriptedEvent in activeEvents.Where(ev => !ev.IsFinished && ev is ScriptedEvent).Cast<ScriptedEvent>())
foreach (ScriptedEvent scriptedEvent in _activeEvents.Where(ev => !ev.IsFinished && ev is ScriptedEvent).Cast<ScriptedEvent>())
{
DrawEventTargetTags(spriteBatch, scriptedEvent);
}
@@ -156,7 +156,7 @@ namespace Barotrauma
{
if (isGraphHovered || isGraphSelected)
{
foreach (var timeStamp in timeStamps)
foreach (var timeStamp in _timeStamps)
{
int t = (int)Math.Abs(Math.Round((timeStamp.Time - lastIntensityUpdate) / intensityGraphUpdateInterval));
if (t == order)
@@ -205,7 +205,7 @@ namespace Barotrauma
}
adjustedYStep = GUI.AdjustForTextScale(12);
foreach (EventSet eventSet in pendingEventSets)
foreach (EventSet eventSet in _pendingEventSets)
{
if (Submarine.MainSub == null) { break; }
@@ -263,7 +263,7 @@ namespace Barotrauma
y += yStep;
adjustedYStep = GUI.AdjustForTextScale(18);
foreach (Event ev in activeEvents.Where(ev => !ev.IsFinished || PlayerInput.IsShiftDown()))
foreach (Event ev in _activeEvents.Where(ev => !ev.IsFinished || PlayerInput.IsShiftDown()))
{
GUI.DrawString(spriteBatch, new Vector2(x + 5, y), ev.ToString(), (!ev.IsFinished ? Color.White : Color.Red) * 0.8f, null, 0, GUIStyle.SmallFont);
@@ -752,4 +752,4 @@ namespace Barotrauma
entry.CanBeCompleted);
}
}
}
}

View File

@@ -1913,7 +1913,7 @@ namespace Barotrauma.Items.Components
void CalculateDistance()
{
pathFinder ??= new PathFinder(WayPoint.WayPointList, false);
pathFinder ??= new PathFinder(WayPoint.WayPointList.ToList(), false);
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(transducerPosition), ConvertUnits.ToSimUnits(worldPosition));
if (!path.Unreachable)
{

View File

@@ -105,7 +105,7 @@ namespace Barotrauma
public static void Draw(SpriteBatch spriteBatch, bool editing = false)
{
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
foreach (MapEntity e in entitiesToRender)
{
@@ -115,7 +115,7 @@ namespace Barotrauma
public static void DrawFront(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
{
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
foreach (MapEntity e in entitiesToRender)
{
@@ -164,7 +164,7 @@ namespace Barotrauma
public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing = false, Predicate<MapEntity> predicate = null)
{
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
depthSortedDamageable.Clear();
@@ -197,7 +197,7 @@ namespace Barotrauma
public static void DrawPaintedColors(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
{
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
foreach (MapEntity e in entitiesToRender)
{
@@ -217,7 +217,7 @@ namespace Barotrauma
public static void DrawBack(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
{
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList.ToList();
foreach (MapEntity e in entitiesToRender)
{

View File

@@ -1,12 +1,32 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace Barotrauma
{
partial class EntitySpawner : Entity, IServerSerializable
{
public readonly List<(Entity entity, bool isRemoval)> receivedEvents = new List<(Entity entity, bool isRemoval)>();
/// <summary>
/// Thread-safe queue for received entity spawn/remove events from the server.
/// </summary>
private readonly ConcurrentQueue<(Entity entity, bool isRemoval)> receivedEventsQueue = new ConcurrentQueue<(Entity entity, bool isRemoval)>();
/// <summary>
/// Gets a thread-safe snapshot of received events.
/// </summary>
public IEnumerable<(Entity entity, bool isRemoval)> GetReceivedEventsSnapshot()
{
return receivedEventsQueue.ToArray();
}
/// <summary>
/// Clears all received events from the queue.
/// </summary>
partial void ResetReceivedEvents()
{
while (receivedEventsQueue.TryDequeue(out _)) { }
}
public void ClientEventRead(IReadMessage message, float sendingTime)
{
@@ -34,7 +54,7 @@ namespace Barotrauma
{
DebugConsole.Log("Received entity removal message for ID " + entityId + ". Entity with a matching ID not found.");
}
receivedEvents.Add((entity, true));
receivedEventsQueue.Enqueue((entity, true));
}
else
{
@@ -57,7 +77,7 @@ namespace Barotrauma
GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + newItem.Prefab.Identifier);
}
}
receivedEvents.Add((newItem, false));
receivedEventsQueue.Enqueue((newItem, false));
}
break;
case (byte)SpawnableType.Character:
@@ -68,7 +88,7 @@ namespace Barotrauma
}
else
{
receivedEvents.Add((character, false));
receivedEventsQueue.Enqueue((character, false));
}
break;
default:

View File

@@ -3918,7 +3918,7 @@ namespace Barotrauma.Networking
{
errorLines.Add("");
errorLines.Add("EntitySpawner events:");
foreach ((Entity entity, bool isRemoval) in Entity.Spawner.receivedEvents)
foreach ((Entity entity, bool isRemoval) in Entity.Spawner.GetReceivedEventsSnapshot())
{
errorLines.Add(
(isRemoval ? "Remove " : "Create ") +

View File

@@ -75,7 +75,7 @@ namespace Barotrauma
{
if (client.InGame)
{
client.PendingPositionUpdates.Enqueue(this);
client.TryEnqueuePositionUpdate(this);
}
}
}

View File

@@ -33,7 +33,7 @@ namespace Barotrauma
byte selectedOption = inc.ReadByte();
bool isIgnore = selectedOption == byte.MaxValue;
foreach (Event ev in activeEvents)
foreach (Event ev in _activeEvents)
{
if (ev is not ScriptedEvent scriptedEvent) { continue; }

View File

@@ -202,9 +202,9 @@ namespace Barotrauma
#if DEBUG || UNSTABLE
DebugConsole.NewMessage($"Client {sender.Name} failed to put \"{item}\" in the inventory of {Owner} (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow);
#endif
if (item.body != null && !sender.PendingPositionUpdates.Contains(item))
if (item.body != null)
{
sender.PendingPositionUpdates.Enqueue(item);
sender.TryEnqueuePositionUpdate(item);
}
item.PositionUpdateInterval = 0.0f;
continue;

View File

@@ -64,6 +64,32 @@ namespace Barotrauma.Networking
// key = entity, value = NetTime.Now when sending
public readonly Dictionary<Entity, float> PositionUpdateLastSent = new Dictionary<Entity, float>();
public readonly Queue<Entity> PendingPositionUpdates = new Queue<Entity>();
private readonly HashSet<Entity> pendingPositionUpdatesSet = new HashSet<Entity>();
/// <summary>
/// Attempts to enqueue a position update for the given entity. Returns true if the entity was added, false if it was already in the queue.
/// Uses HashSet for O(1) lookup instead of Queue.Contains() which is O(n).
/// </summary>
public bool TryEnqueuePositionUpdate(Entity entity)
{
if (pendingPositionUpdatesSet.Add(entity))
{
PendingPositionUpdates.Enqueue(entity);
return true;
}
return false;
}
/// <summary>
/// Dequeues a position update and removes it from the HashSet tracking.
/// </summary>
public Entity DequeuePositionUpdate()
{
if (PendingPositionUpdates.Count == 0) { return null; }
var entity = PendingPositionUpdates.Dequeue();
pendingPositionUpdatesSet.Remove(entity);
return entity;
}
public bool ReadyToStart;
@@ -353,6 +379,7 @@ namespace Barotrauma.Networking
{
NeedsMidRoundSync = false;
PendingPositionUpdates.Clear();
pendingPositionUpdatesSet.Clear();
EntityEventLastSent.Clear();
LastSentEntityEventID = 0;
LastRecvEntityEventID = 0;

View File

@@ -744,11 +744,6 @@ namespace Barotrauma.Networking
{
errorMsg += "\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace();
}
GameAnalyticsManager.AddErrorEventOnce(
"GameServer.Update:ClientWriteFailed" + e.StackTrace.CleanupStackTrace(),
GameAnalyticsManager.ErrorSeverity.Error,
errorMsg);
}
}
@@ -2147,7 +2142,7 @@ namespace Barotrauma.Networking
{
if (lastSent > NetTime.Now - updateInterval) { continue; }
}
if (!c.PendingPositionUpdates.Contains(otherCharacter)) { c.PendingPositionUpdates.Enqueue(otherCharacter); }
c.TryEnqueuePositionUpdate(otherCharacter);
}
foreach (Submarine sub in Submarine.Loaded)
@@ -2156,7 +2151,7 @@ namespace Barotrauma.Networking
// (= update is only sent for the docked sub that has the smallest ID, doesn't matter if it's the main sub or a shuttle)
if (sub.Info.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) { continue; }
if (sub.PhysicsBody == null || sub.PhysicsBody.BodyType == FarseerPhysics.BodyType.Static) { continue; }
if (!c.PendingPositionUpdates.Contains(sub)) { c.PendingPositionUpdates.Enqueue(sub); }
c.TryEnqueuePositionUpdate(sub);
}
foreach (Item item in Item.ItemList)
@@ -2173,7 +2168,7 @@ namespace Barotrauma.Networking
{
if (lastSent > NetTime.Now - updateInterval) { continue; }
}
if (!c.PendingPositionUpdates.Contains(item)) { c.PendingPositionUpdates.Enqueue(item); }
c.TryEnqueuePositionUpdate(item);
}
}
@@ -2217,7 +2212,7 @@ namespace Barotrauma.Networking
entity.Removed ||
(entity is Item item && float.IsInfinity(item.PositionUpdateInterval)))
{
c.PendingPositionUpdates.Dequeue();
c.DequeuePositionUpdate();
continue;
}
@@ -2239,7 +2234,7 @@ namespace Barotrauma.Networking
outmsg.WritePadBits();
c.PositionUpdateLastSent[entity] = (float)NetTime.Now;
c.PendingPositionUpdates.Dequeue();
c.DequeuePositionUpdate();
}
positionUpdateBytes = outmsg.LengthBytes - positionUpdateBytes;

View File

@@ -1,7 +1,8 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
namespace Barotrauma
{
@@ -10,11 +11,22 @@ namespace Barotrauma
/// </summary>
class UnlockPathAction : EventAction
{
private static readonly HashSet<LocationConnection> pathsUnlockedThisRound = new HashSet<LocationConnection>();
private static volatile ImmutableHashSet<LocationConnection> _pathsUnlockedThisRound =
ImmutableHashSet<LocationConnection>.Empty;
public static void ResetPathsUnlockedThisRound()
{
pathsUnlockedThisRound.Clear();
_pathsUnlockedThisRound = ImmutableHashSet<LocationConnection>.Empty;
}
private static void AddUnlockedPath(LocationConnection connection)
{
ImmutableHashSet<LocationConnection> original, updated;
do
{
original = _pathsUnlockedThisRound;
updated = original.Add(connection);
} while (Interlocked.CompareExchange(ref _pathsUnlockedThisRound, updated, original) != original);
}
public UnlockPathAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -40,7 +52,7 @@ namespace Barotrauma
{
if (!connection.Locked) { continue; }
connection.Locked = false;
pathsUnlockedThisRound.Add(connection);
AddUnlockedPath(connection);
#if SERVER
NotifyUnlock(connection);
#else
@@ -61,7 +73,7 @@ namespace Barotrauma
#if SERVER
public static void NotifyPathsUnlockedThisRound(Client client)
{
foreach (LocationConnection connection in pathsUnlockedThisRound)
foreach (LocationConnection connection in _pathsUnlockedThisRound)
{
NotifyUnlock(connection, client);
}
@@ -85,4 +97,4 @@ namespace Barotrauma
}
#endif
}
}
}

View File

@@ -3,8 +3,11 @@ using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
namespace Barotrauma
@@ -43,7 +46,7 @@ namespace Barotrauma
private Level level;
private readonly List<Sprite> preloadedSprites = new List<Sprite>();
private volatile ImmutableList<Sprite> _preloadedSprites = ImmutableList<Sprite>.Empty;
//The "intensity" of the current situation (a value between 0.0 - 1.0).
//High when a disaster has struck, low when nothing special is going on.
@@ -83,14 +86,18 @@ namespace Barotrauma
private float crewAwayResetTimer;
private float crewAwayDuration;
private readonly List<EventSet> pendingEventSets = new List<EventSet>();
// volatile + ImmutableCollections
private volatile ImmutableList<EventSet> _pendingEventSets = ImmutableList<EventSet>.Empty;
private readonly Dictionary<EventSet, List<Event>> selectedEvents = new Dictionary<EventSet, List<Event>>();
private volatile ImmutableDictionary<EventSet, ImmutableList<Event>> _selectedEvents =
ImmutableDictionary<EventSet, ImmutableList<Event>>.Empty;
private readonly List<Event> activeEvents = new List<Event>();
private volatile ImmutableList<Event> _activeEvents = ImmutableList<Event>.Empty;
private readonly HashSet<Event> finishedEvents = new HashSet<Event>();
private readonly HashSet<Identifier> nonRepeatableEvents = new HashSet<Identifier>();
private volatile ImmutableHashSet<Event> _finishedEvents = ImmutableHashSet<Event>.Empty;
private volatile ImmutableHashSet<Identifier> _nonRepeatableEvents = ImmutableHashSet<Identifier>.Empty;
private volatile ImmutableQueue<Action> _deferredActions = ImmutableQueue<Action>.Empty;
#if DEBUG && SERVER
@@ -112,10 +119,10 @@ namespace Barotrauma
public IEnumerable<Event> ActiveEvents
{
get { return activeEvents; }
get { return _activeEvents; }
}
public readonly Queue<Event> QueuedEvents = new Queue<Event>();
public readonly ConcurrentQueue<Event> QueuedEvents = new ConcurrentQueue<Event>();
public readonly Queue<Identifier> QueuedEventsForNextRound = new Queue<Identifier>();
@@ -131,8 +138,8 @@ namespace Barotrauma
}
}
private readonly List<TimeStamp> timeStamps = new List<TimeStamp>();
public void AddTimeStamp(Event e) => timeStamps.Add(new TimeStamp(e));
private volatile ImmutableList<TimeStamp> _timeStamps = ImmutableList<TimeStamp>.Empty;
public void AddTimeStamp(Event e) => AtomicUpdate(ref _timeStamps, list => list.Add(new TimeStamp(e)));
public readonly EventLog EventLog = new EventLog();
@@ -143,6 +150,72 @@ namespace Barotrauma
public bool Enabled = true;
private static T AtomicUpdate<T>(ref T location, Func<T, T> updateFunc) where T : class
{
T original, updated;
do
{
original = Volatile.Read(ref location);
updated = updateFunc(original);
} while (Interlocked.CompareExchange(ref location, updated, original) != original);
return updated;
}
// activeEvents
private void AddActiveEvent(Event ev) => AtomicUpdate(ref _activeEvents, list => list.Add(ev));
private void ClearActiveEvents() => _activeEvents = ImmutableList<Event>.Empty;
// pendingEventSets
private void AddPendingEventSet(EventSet eventSet) =>
AtomicUpdate(ref _pendingEventSets, list => list.Contains(eventSet) ? list : list.Add(eventSet));
private void RemovePendingEventSetAt(int index) =>
AtomicUpdate(ref _pendingEventSets, list => index < list.Count ? list.RemoveAt(index) : list);
private void ClearPendingEventSets() => _pendingEventSets = ImmutableList<EventSet>.Empty;
// selectedEvents
private void AddSelectedEvent(EventSet eventSet, Event ev) =>
AtomicUpdate(ref _selectedEvents, dict =>
{
var currentList = dict.GetValueOrDefault(eventSet, ImmutableList<Event>.Empty);
return dict.SetItem(eventSet, currentList.Add(ev));
});
private void RemoveSelectedEventSet(EventSet eventSet) =>
AtomicUpdate(ref _selectedEvents, dict => dict.Remove(eventSet));
private void ClearSelectedEvents() =>
_selectedEvents = ImmutableDictionary<EventSet, ImmutableList<Event>>.Empty;
private ImmutableList<Event> GetSelectedEvents(EventSet eventSet) =>
_selectedEvents.GetValueOrDefault(eventSet, ImmutableList<Event>.Empty);
private bool HasSelectedEvents(EventSet eventSet) => _selectedEvents.ContainsKey(eventSet);
// finishedEvents
private void AddFinishedEvent(Event ev) => AtomicUpdate(ref _finishedEvents, set => set.Add(ev));
private void ClearFinishedEvents() => _finishedEvents = ImmutableHashSet<Event>.Empty;
private bool IsEventFinished(Event ev) => _finishedEvents.Contains(ev);
// nonRepeatableEvents
private void AddNonRepeatableEvent(Identifier id) => AtomicUpdate(ref _nonRepeatableEvents, set => set.Add(id));
private void ClearNonRepeatableEvents() => _nonRepeatableEvents = ImmutableHashSet<Identifier>.Empty;
// preloadedSprites
private void AddPreloadedSprite(Sprite sprite) => AtomicUpdate(ref _preloadedSprites, list => list.Add(sprite));
private void ClearPreloadedSprites()
{
var sprites = Interlocked.Exchange(ref _preloadedSprites, ImmutableList<Sprite>.Empty);
foreach (var s in sprites) { s.Remove(); }
}
// timeStamps
private void ClearTimeStamps() => _timeStamps = ImmutableList<TimeStamp>.Empty;
private void EnqueueDeferredAction(Action action) =>
AtomicUpdate(ref _deferredActions, queue => queue.Enqueue(action));
private void ProcessDeferredActions()
{
var actions = Interlocked.Exchange(ref _deferredActions, ImmutableQueue<Action>.Empty);
foreach (var action in actions) { action(); }
}
private MTRandom random;
public int RandomSeed { get; private set; }
@@ -152,10 +225,10 @@ namespace Barotrauma
if (isClient) { return; }
timeStamps.Clear();
pendingEventSets.Clear();
selectedEvents.Clear();
activeEvents.Clear();
ClearTimeStamps();
ClearPendingEventSets();
ClearSelectedEvents();
ClearActiveEvents();
#if SERVER
MissionAction.ResetMissionsUnlockedThisRound();
UnlockPathAction.ResetPathsUnlockedThisRound();
@@ -235,8 +308,8 @@ namespace Barotrauma
void AddSet(EventSet eventSet)
{
if (pendingEventSets.Contains(eventSet)) { return; }
pendingEventSets.Add(eventSet);
if (_pendingEventSets.Contains(eventSet)) { return; }
AddPendingEventSet(eventSet);
CreateEvents(eventSet);
}
@@ -251,7 +324,7 @@ namespace Barotrauma
if (unlockPathEventPrefab != null)
{
var newEvent = unlockPathEventPrefab.CreateInstance(RandomSeed);
activeEvents.Add(newEvent);
AddActiveEvent(newEvent);
}
else
{
@@ -284,7 +357,7 @@ namespace Barotrauma
{
foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs))
{
nonRepeatableEvents.Add(ep.Identifier);
AddNonRepeatableEvent(ep.Identifier);
}
}
foreach (EventSet childSet in eventSet.ChildSets)
@@ -329,13 +402,13 @@ namespace Barotrauma
public void ActivateEvent(Event newEvent)
{
activeEvents.Add(newEvent);
AddActiveEvent(newEvent);
newEvent.Init();
}
public void ClearEvents()
{
activeEvents.Clear();
ClearActiveEvents();
}
private void SelectSettings()
@@ -388,7 +461,8 @@ namespace Barotrauma
public IEnumerable<ContentFile> GetFilesToPreload()
{
foreach (List<Event> eventList in selectedEvents.Values)
var snapshot = _selectedEvents;
foreach (ImmutableList<Event> eventList in snapshot.Values)
{
foreach (Event ev in eventList)
{
@@ -441,13 +515,13 @@ namespace Barotrauma
foreach (ContentFile file in filesToPreload)
{
file.Preload(preloadedSprites.Add);
file.Preload(AddPreloadedSprite);
}
}
public void TriggerOnEndRoundActions()
{
foreach (var ev in activeEvents)
foreach (var ev in _activeEvents)
{
(ev as ScriptedEvent)?.OnRoundEndAction?.Update(1.0f);
}
@@ -455,17 +529,16 @@ namespace Barotrauma
public void EndRound()
{
pendingEventSets.Clear();
selectedEvents.Clear();
activeEvents.Clear();
QueuedEvents.Clear();
finishedEvents.Clear();
nonRepeatableEvents.Clear();
ClearPendingEventSets();
ClearSelectedEvents();
ClearActiveEvents();
while (QueuedEvents.TryDequeue(out _)) { } // 清空 ConcurrentQueue
ClearFinishedEvents();
ClearNonRepeatableEvents();
preloadedSprites.ForEach(s => s.Remove());
preloadedSprites.Clear();
ClearPreloadedSprites();
timeStamps.Clear();
ClearTimeStamps();
pathFinder = null;
}
@@ -481,7 +554,7 @@ namespace Barotrauma
{
if (registerFinishedOnly)
{
foreach (var finishedEvent in finishedEvents)
foreach (var finishedEvent in _finishedEvents)
{
EventSet parentSet = finishedEvent.ParentSet;
if (parentSet == null) { continue; }
@@ -496,7 +569,7 @@ namespace Barotrauma
}
}
level.LevelData.EventHistory.AddRange(selectedEvents.Values
level.LevelData.EventHistory.AddRange(_selectedEvents.Values
.SelectMany(v => v)
.Select(e => e.Prefab.Identifier)
.Where(eventId => Register(eventId) && !level.LevelData.EventHistory.Contains(eventId)));
@@ -506,14 +579,14 @@ namespace Barotrauma
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
}
}
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId)));
level.LevelData.NonRepeatableEvents.AddRange(_nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId)));
if (!registerFinishedOnly)
{
level.LevelData.FinishedEvents.Clear();
}
bool Register(Identifier eventId) => !registerFinishedOnly || finishedEvents.Any(fe => fe.Prefab.Identifier == eventId);
bool Register(Identifier eventId) => !registerFinishedOnly || _finishedEvents.Any(fe => fe.Prefab.Identifier == eventId);
}
public void SkipEventCooldown()
@@ -531,7 +604,7 @@ namespace Barotrauma
private void CreateEvents(EventSet eventSet)
{
selectedEvents.Remove(eventSet);
RemoveSelectedEventSet(eventSet);
if (level == null) { return; }
if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; }
if (eventSet.Exhaustible && level.LevelData.IsEventSetExhausted(eventSet)) { return; }
@@ -598,11 +671,7 @@ namespace Barotrauma
if (newEvent == null) { continue; }
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<Event>());
}
selectedEvents[eventSet].Add(newEvent);
AddSelectedEvent(eventSet, newEvent);
unusedEvents.Remove(subEventPrefab);
}
}
@@ -641,11 +710,7 @@ namespace Barotrauma
var newEvent = eventPrefab.CreateInstance(RandomSeed);
if (newEvent == null) { continue; }
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<Event>());
}
selectedEvents[eventSet].Add(newEvent);
AddSelectedEvent(eventSet, newEvent);
}
var location = GetEventLocation();
@@ -837,9 +902,10 @@ namespace Barotrauma
if (!eventsInitialized)
{
foreach (var eventSet in selectedEvents.Keys)
var selectedSnapshot = _selectedEvents;
foreach (var eventSet in selectedSnapshot.Keys)
{
foreach (var ev in selectedEvents[eventSet])
foreach (var ev in selectedSnapshot[eventSet])
{
ev.Init(eventSet);
}
@@ -910,23 +976,25 @@ namespace Barotrauma
{
recheck = false;
//activate pending event sets that can be activated
for (int i = pendingEventSets.Count - 1; i >= 0; i--)
var pendingSnapshot = _pendingEventSets;
for (int i = pendingSnapshot.Count - 1; i >= 0; i--)
{
var eventSet = pendingEventSets[i];
var eventSet = pendingSnapshot[i];
if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; }
if (currentIntensity > eventThreshold && !eventSet.IgnoreIntensity) { continue; }
if (!CanStartEventSet(eventSet)) { continue; }
pendingEventSets.RemoveAt(i);
RemovePendingEventSetAt(i);
if (selectedEvents.ContainsKey(eventSet))
var selectedEventsList = GetSelectedEvents(eventSet);
if (selectedEventsList.Count > 0)
{
//start events in this set
foreach (Event ev in selectedEvents[eventSet])
foreach (Event ev in selectedEventsList)
{
activeEvents.Add(ev);
AddActiveEvent(ev);
eventThreshold = settings.DefaultEventThreshold;
if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown))
if (eventSet.TriggerEventCooldown && selectedEventsList.Any(e => e.Prefab.TriggerEventCooldown))
{
eventCoolDown = settings.EventCooldown;
}
@@ -934,12 +1002,15 @@ namespace Barotrauma
{
ev.Finished += () =>
{
pendingEventSets.Add(eventSet);
CreateEvents(eventSet);
foreach (Event newEvent in selectedEvents[eventSet])
EnqueueDeferredAction(() =>
{
if (!newEvent.Initialized) { newEvent.Init(eventSet); }
}
AddPendingEventSet(eventSet);
CreateEvents(eventSet);
foreach (Event newEvent in GetSelectedEvents(eventSet))
{
if (!newEvent.Initialized) { newEvent.Init(eventSet); }
}
});
};
}
}
@@ -948,37 +1019,40 @@ namespace Barotrauma
//add child event sets to pending
foreach (EventSet childEventSet in eventSet.ChildSets)
{
pendingEventSets.Add(childEventSet);
AddPendingEventSet(childEventSet);
recheck = true;
}
}
} while (recheck);
foreach (Event ev in activeEvents)
var activeSnapshot = _activeEvents;
foreach (Event ev in activeSnapshot)
{
if (!ev.IsFinished)
{
ev.Update(deltaTime);
}
else if (ev.Prefab != null && !finishedEvents.Any(e => e.Prefab == ev.Prefab))
else if (ev.Prefab != null && !IsEventFinished(ev))
{
if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost)
{
if (!level.LevelData.EventHistory.Contains(ev.Prefab.Identifier)) { level.LevelData.EventHistory.Add(ev.Prefab.Identifier); }
}
finishedEvents.Add(ev);
AddFinishedEvent(ev);
}
}
if (QueuedEvents.Count > 0)
if (QueuedEvents.TryDequeue(out var queuedEvent))
{
activeEvents.Add(QueuedEvents.Dequeue());
AddActiveEvent(queuedEvent);
}
ProcessDeferredActions();
}
public void EntitySpawned(Entity entity)
{
foreach (var ev in activeEvents)
foreach (var ev in _activeEvents)
{
if (ev is ScriptedEvent scriptedEvent)
{

View File

@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
namespace Barotrauma
@@ -60,47 +61,48 @@ namespace Barotrauma
return null;
}
#endif
private static readonly Dictionary<Identifier, EventPrefab> AllEventPrefabs = new Dictionary<Identifier, EventPrefab>();
private static volatile ImmutableDictionary<Identifier, EventPrefab> _allEventPrefabs =
ImmutableDictionary<Identifier, EventPrefab>.Empty;
public static IEnumerable<EventPrefab> GetAllEventPrefabs()
{
return AllEventPrefabs.Values;
return _allEventPrefabs.Values;
}
/// <summary>
/// Finds all the event prefabs (both "normal prefabs" that exists by themselves, present in <see cref="EventPrefab.Prefabs"/>, and the ones that exists only inside child event sets),
/// and adds them to <see cref="AllEventPrefabs"/>.
/// and adds them to <see cref="_allEventPrefabs"/>.
/// </summary>
public static void RefreshAllEventPrefabs()
{
AllEventPrefabs.Clear();
var builder = ImmutableDictionary.CreateBuilder<Identifier, EventPrefab>();
foreach (var eventPrefab in EventPrefab.Prefabs)
{
AllEventPrefabs.TryAdd(eventPrefab.Identifier, eventPrefab);
builder.TryAdd(eventPrefab.Identifier, eventPrefab);
}
foreach (var eventSet in Prefabs)
{
AddChildEventPrefabs(eventSet);
AddChildEventPrefabs(eventSet, builder);
}
Interlocked.Exchange(ref _allEventPrefabs, builder.ToImmutable());
}
private static void AddChildEventPrefabs(EventSet set)
private static void AddChildEventPrefabs(EventSet set, ImmutableDictionary<Identifier, EventPrefab>.Builder builder)
{
foreach (var subEventPrefabs in set.EventPrefabs)
{
foreach (var eventPrefab in subEventPrefabs.EventPrefabs)
{
AllEventPrefabs.TryAdd(eventPrefab.Identifier, eventPrefab);
builder.TryAdd(eventPrefab.Identifier, eventPrefab);
}
}
foreach (var childSet in set.ChildSets) { AddChildEventPrefabs(childSet); }
foreach (var childSet in set.ChildSets) { AddChildEventPrefabs(childSet, builder); }
}
public static EventPrefab GetEventPrefab(Identifier identifier)
{
return AllEventPrefabs.GetValueOrDefault(identifier);
return _allEventPrefabs.GetValueOrDefault(identifier);
}
/// <summary>

View File

@@ -2555,7 +2555,11 @@ namespace Barotrauma
/// </summary>
public bool IsActive = true;
public bool IsInRemoveQueue;
/// <summary>
/// Thread-safe flag indicating whether this item is queued for removal.
/// Uses volatile to ensure memory visibility across threads.
/// </summary>
public volatile bool IsInRemoveQueue;
public override void Update(float deltaTime, Camera cam)
{
@@ -4903,12 +4907,11 @@ namespace Barotrauma
StaticFixtures.Clear();
}
foreach (Item it in ItemList)
// Optimized: Remove() returns false if not found, no need for Contains() check
// Using _itemDictionary.Values directly avoids property access overhead
foreach (Item it in _itemDictionary.Values)
{
if (it.linkedTo.Contains(this))
{
it.linkedTo.Remove(this);
}
it.linkedTo.Remove(this);
}
RemoveProjSpecific();

View File

@@ -3,6 +3,7 @@ using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -204,7 +205,17 @@ namespace Barotrauma
}
}
private readonly Queue<Either<IEntitySpawnInfo, Entity>> spawnOrRemoveQueue;
/// <summary>
/// Thread-safe queue for spawn/remove operations.
/// Uses ConcurrentQueue for lock-free concurrent access.
/// </summary>
private readonly ConcurrentQueue<Either<IEntitySpawnInfo, Entity>> spawnOrRemoveQueue;
/// <summary>
/// Thread-safe set for O(1) removal queue lookup.
/// Entities are added when queued for removal and removed after actual removal.
/// </summary>
private readonly ConcurrentDictionary<Entity, byte> removeQueueLookup;
public abstract class SpawnOrRemove : NetEntityEvent.IData
{
@@ -264,7 +275,8 @@ namespace Barotrauma
public EntitySpawner()
: base(null, Entity.EntitySpawnerID)
{
spawnOrRemoveQueue = new Queue<Either<IEntitySpawnInfo, Entity>>();
spawnOrRemoveQueue = new ConcurrentQueue<Either<IEntitySpawnInfo, Entity>>();
removeQueueLookup = new ConcurrentDictionary<Entity, byte>();
}
public override string ToString()
@@ -358,8 +370,12 @@ namespace Barotrauma
public void AddEntityToRemoveQueue(Entity entity)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (entity == null || IsInRemoveQueue(entity) || entity.Removed || entity.IdFreed) { return; }
if (entity == null || entity.Removed || entity.IdFreed) { return; }
if (entity is Item item) { AddItemToRemoveQueue(item); return; }
// Thread-safe check-and-add using ConcurrentDictionary
if (!removeQueueLookup.TryAdd(entity, 0)) { return; }
if (entity is Character)
{
Character character = entity as Character;
@@ -381,7 +397,10 @@ namespace Barotrauma
public void AddItemToRemoveQueue(Item item)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (IsInRemoveQueue(item) || item.Removed) { return; }
if (item.Removed) { return; }
// Thread-safe check-and-add using ConcurrentDictionary
if (!removeQueueLookup.TryAdd(item, 0)) { return; }
spawnOrRemoveQueue.Enqueue(item);
item.IsInRemoveQueue = true;
@@ -396,11 +415,13 @@ namespace Barotrauma
}
/// <summary>
/// Are there any entities in the spawn queue that match the given predicate
/// Thread-safe check if any entities in the spawn queue match the given predicate.
/// Uses a snapshot of the queue for iteration.
/// </summary>
public bool IsInSpawnQueue(Predicate<IEntitySpawnInfo> predicate)
{
foreach (var spawnOrRemove in spawnOrRemoveQueue)
// ConcurrentQueue.ToArray() provides a thread-safe snapshot
foreach (var spawnOrRemove in spawnOrRemoveQueue.ToArray())
{
if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { return true; }
}
@@ -408,35 +429,45 @@ namespace Barotrauma
}
/// <summary>
/// How many entities in the spawn queue match the given predicate
/// Thread-safe count of entities in the spawn queue that match the given predicate.
/// Uses a snapshot of the queue for iteration.
/// </summary>
public int CountSpawnQueue(Predicate<IEntitySpawnInfo> predicate)
{
int count = 0;
foreach (var spawnOrRemove in spawnOrRemoveQueue)
// ConcurrentQueue.ToArray() provides a thread-safe snapshot
foreach (var spawnOrRemove in spawnOrRemoveQueue.ToArray())
{
if (spawnOrRemove.TryGet(out IEntitySpawnInfo spawnInfo) && predicate(spawnInfo)) { count++; }
}
return count;
}
/// <summary>
/// Thread-safe O(1) check if entity is in the remove queue.
/// </summary>
public bool IsInRemoveQueue(Entity entity)
{
foreach (var spawnOrRemove in spawnOrRemoveQueue)
{
if (spawnOrRemove.TryGet(out Entity entityToRemove) && entityToRemove == entity) { return true; }
}
return false;
return removeQueueLookup.ContainsKey(entity);
}
public void Update(bool createNetworkEvents = true)
{
if (GameMain.NetworkMember is { IsClient: true }) { return; }
while (spawnOrRemoveQueue.Count > 0)
// IMPORTANT: Entity creation and removal MUST be sequential!
// - Entity ID allocation is NOT thread-safe (causes ID conflicts)
// - Inventory operations are NOT thread-safe (causes stack overflow/slot conflicts)
// - Entity.Remove() has cascading effects on global state
//
// Optimization: batch dequeue for better cache locality
while (spawnOrRemoveQueue.TryDequeue(out var spawnOrRemove))
{
if (!spawnOrRemoveQueue.TryDequeue(out var spawnOrRemove)) { break; }
if (spawnOrRemove.TryGet(out Entity entityToRemove))
{
// Remove from lookup after processing
removeQueueLookup.TryRemove(entityToRemove, out _);
if (entityToRemove is Item item)
{
item.SendPendingNetworkUpdates();
@@ -465,9 +496,11 @@ namespace Barotrauma
public void Reset()
{
spawnOrRemoveQueue.Clear();
// Clear the concurrent queue by draining it
while (spawnOrRemoveQueue.TryDequeue(out _)) { }
removeQueueLookup.Clear();
#if CLIENT
receivedEvents.Clear();
ResetReceivedEvents();
#endif
}
}

View File

@@ -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 _)) { }
}
}
}