WIP Make static collections thread-safe using ThreadStatic and ThreadLocal

Refactored various static and instance collections to use [ThreadStatic], ThreadLocal, or local variables to prevent concurrent modification issues during parallel updates. This affects status effect targets, affliction lists, damage modifiers, and cached data in Character, CharacterHealth, Limb, Explosion, Hull, Submarine, and ToolBox classes. Also replaced Dictionary caches with ConcurrentDictionary where appropriate for thread safety.
This commit is contained in:
Eero
2025-12-28 14:14:53 +08:00
parent c5fa49405f
commit 45312af297
7 changed files with 135 additions and 75 deletions

View File

@@ -4880,7 +4880,11 @@ namespace Barotrauma
HealthUpdateInterval = 0.0f;
}
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
// Thread-static to avoid concurrent modification in parallel item updates
[ThreadStatic]
private static List<ISerializableEntity> t_statusEffectTargets;
private static List<ISerializableEntity> StatusEffectTargets => t_statusEffectTargets ??= new List<ISerializableEntity>();
public void ApplyStatusEffects(ActionType actionType, float deltaTime)
{
if (actionType == ActionType.OnEating)
@@ -4909,6 +4913,7 @@ namespace Barotrauma
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = StatusEffectTargets;
targets.Clear();
statusEffect.AddNearbyTargets(WorldPosition, targets);
statusEffect.Apply(actionType, deltaTime, this, targets);

View File

@@ -537,20 +537,25 @@ namespace Barotrauma
return false;
}
private readonly List<Affliction> matchingAfflictions = new List<Affliction>();
// Thread-static to avoid concurrent modification in parallel item updates
[ThreadStatic]
private static List<Affliction> t_matchingAfflictions;
private static List<Affliction> MatchingAfflictions => t_matchingAfflictions ??= new List<Affliction>();
public void ReduceAllAfflictionsOnAllLimbs(float amount, ActionType? treatmentAction = null)
{
var matchingAfflictions = MatchingAfflictions;
matchingAfflictions.Clear();
matchingAfflictions.AddRange(afflictions.Keys);
ReduceMatchingAfflictions(amount, treatmentAction);
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction);
}
public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
{
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
var matchingAfflictions = MatchingAfflictions;
matchingAfflictions.Clear();
foreach (var affliction in afflictions)
{
@@ -560,7 +565,7 @@ namespace Barotrauma
}
}
ReduceMatchingAfflictions(amount, treatmentAction, attacker);
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction, attacker);
}
private IEnumerable<Affliction> GetAfflictionsForLimb(Limb targetLimb)
@@ -570,10 +575,11 @@ namespace Barotrauma
{
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
var matchingAfflictions = MatchingAfflictions;
matchingAfflictions.Clear();
matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb));
ReduceMatchingAfflictions(amount, treatmentAction);
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction);
}
public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
@@ -581,6 +587,7 @@ namespace Barotrauma
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
var matchingAfflictions = MatchingAfflictions;
matchingAfflictions.Clear();
var targetLimbHealth = limbHealths[targetLimb.HealthIndex];
foreach (var affliction in afflictions)
@@ -591,10 +598,10 @@ namespace Barotrauma
matchingAfflictions.Add(affliction.Key);
}
}
ReduceMatchingAfflictions(amount, treatmentAction, attacker);
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction, attacker);
}
private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction, Character attacker = null)
private void ReduceMatchingAfflictions(List<Affliction> matchingAfflictions, float amount, ActionType? treatmentAction, Character attacker = null)
{
if (matchingAfflictions.Count == 0) { return; }
@@ -681,12 +688,19 @@ namespace Barotrauma
}
}
private readonly static List<Affliction> afflictionsToRemove = new List<Affliction>();
private readonly static List<KeyValuePair<Affliction, LimbHealth>> afflictionsToUpdate = new List<KeyValuePair<Affliction, LimbHealth>>();
// Thread-static to avoid concurrent modification when multiple characters are updated in parallel
[ThreadStatic]
private static List<Affliction> t_afflictionsToRemove;
[ThreadStatic]
private static List<KeyValuePair<Affliction, LimbHealth>> t_afflictionsToUpdate;
private static List<Affliction> AfflictionsToRemove => t_afflictionsToRemove ??= new List<Affliction>();
private static List<KeyValuePair<Affliction, LimbHealth>> AfflictionsToUpdate => t_afflictionsToUpdate ??= new List<KeyValuePair<Affliction, LimbHealth>>();
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
{
if (Unkillable || Character.GodMode) { return; }
var afflictionsToRemove = AfflictionsToRemove;
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
a.Prefab.AfflictionType == AfflictionPrefab.InternalDamage.AfflictionType ||
@@ -737,6 +751,7 @@ namespace Barotrauma
public void RemoveAfflictions(Func<Affliction, bool> predicate)
{
var afflictionsToRemove = AfflictionsToRemove;
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction)));
foreach (var affliction in afflictionsToRemove)
@@ -748,6 +763,7 @@ namespace Barotrauma
public void RemoveAllAfflictions()
{
var afflictionsToRemove = AfflictionsToRemove;
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.ContainsKey(a)));
foreach (var affliction in afflictionsToRemove)
@@ -765,6 +781,7 @@ namespace Barotrauma
public void RemoveNegativeAfflictions()
{
var afflictionsToRemove = AfflictionsToRemove;
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
!irremovableAfflictions.ContainsKey(a) &&
@@ -904,6 +921,8 @@ namespace Barotrauma
if (!Character.GodMode)
{
var afflictionsToRemove = AfflictionsToRemove;
var afflictionsToUpdate = AfflictionsToUpdate;
afflictionsToRemove.Clear();
afflictionsToUpdate.Clear();
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
@@ -1198,9 +1217,14 @@ namespace Barotrauma
return (causeOfDeath, strongestAffliction);
}
private readonly List<Affliction> allAfflictions = new List<Affliction>();
// Thread-static to avoid concurrent modification in parallel item updates
[ThreadStatic]
private static List<Affliction> t_allAfflictions;
private static List<Affliction> AllAfflictionsList => t_allAfflictions ??= new List<Affliction>();
private IEnumerable<Affliction> GetAllAfflictions(bool mergeSameAfflictions, Func<Affliction, bool> predicate = null)
{
var allAfflictions = AllAfflictionsList;
allAfflictions.Clear();
if (!mergeSameAfflictions)
{
@@ -1383,10 +1407,17 @@ namespace Barotrauma
return MathHelper.Clamp(strength, 0.0f, affliction.Prefab.MaxStrength);
}
private readonly List<Affliction> activeAfflictions = new List<Affliction>();
private readonly List<(LimbHealth limbHealth, Affliction affliction)> limbAfflictions = new List<(LimbHealth limbHealth, Affliction affliction)>();
// Thread-static to avoid concurrent modification in parallel updates
[ThreadStatic]
private static List<Affliction> t_activeAfflictions;
[ThreadStatic]
private static List<(LimbHealth limbHealth, Affliction affliction)> t_limbAfflictions;
private static List<Affliction> ActiveAfflictionsList => t_activeAfflictions ??= new List<Affliction>();
private static List<(LimbHealth limbHealth, Affliction affliction)> LimbAfflictionsList => t_limbAfflictions ??= new List<(LimbHealth limbHealth, Affliction affliction)>();
public void ServerWrite(IWriteMessage msg)
{
var activeAfflictions = ActiveAfflictionsList;
activeAfflictions.Clear();
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
@@ -1412,6 +1443,7 @@ namespace Barotrauma
}
}
var limbAfflictions = LimbAfflictionsList;
limbAfflictions.Clear();
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
@@ -1441,8 +1473,9 @@ namespace Barotrauma
public void Remove()
{
RemoveProjSpecific();
afflictionsToRemove.Clear();
afflictionsToUpdate.Clear();
// Clear thread-static lists to help with garbage collection
AfflictionsToRemove.Clear();
AfflictionsToUpdate.Clear();
}
partial void RemoveProjSpecific();

View File

@@ -797,16 +797,14 @@ namespace Barotrauma
return AddDamage(simPosition, afflictions, playSound);
}
private readonly List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
private readonly List<DamageModifier> tempModifiers = new List<DamageModifier>();
private readonly List<Affliction> afflictionsCopy = new List<Affliction>();
// Thread-safe: using local variables instead of instance fields to avoid concurrent modification
public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound, float damageMultiplier = 1, float penetration = 0f, Character attacker = null)
{
appliedDamageModifiers.Clear();
afflictionsCopy.Clear();
var appliedDamageModifiers = new List<DamageModifier>();
var afflictionsCopy = new List<Affliction>();
foreach (var affliction in afflictions)
{
tempModifiers.Clear();
var tempModifiers = new List<DamageModifier>();
var newAffliction = affliction;
float random = Rand.Value(Rand.RandSync.Unsynced);
bool foundMatchingModifier = false;
@@ -1022,13 +1020,18 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime);
private readonly List<Body> contactBodies = new List<Body>();
// Thread-static to avoid concurrent modification in parallel item updates
[ThreadStatic]
private static List<Body> t_contactBodies;
private static List<Body> ContactBodies => t_contactBodies ??= new List<Body>();
/// <summary>
/// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated.
/// </summary>
public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null)
{
attackResult = default;
var contactBodies = ContactBodies;
Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition;
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos));
bool wasRunning = attack.IsRunning;
@@ -1287,7 +1290,11 @@ namespace Barotrauma
}
}
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
// Thread-static to avoid concurrent modification in parallel item updates
[ThreadStatic]
private static List<ISerializableEntity> t_statusEffectTargets;
private static List<ISerializableEntity> StatusEffectTargets => t_statusEffectTargets ??= new List<ISerializableEntity>();
public void ApplyStatusEffects(ActionType actionType, float deltaTime)
{
if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; }
@@ -1310,6 +1317,7 @@ namespace Barotrauma
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = StatusEffectTargets;
targets.Clear();
statusEffect.AddNearbyTargets(WorldPosition, targets);
statusEffect.Apply(actionType, deltaTime, character, targets);

View File

@@ -7,6 +7,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace Barotrauma
{
@@ -648,7 +649,11 @@ namespace Barotrauma
}
}
private static readonly Dictionary<Structure, float> damagedStructures = new Dictionary<Structure, float>();
// ThreadLocal for thread-safe structure damage tracking
private static readonly ThreadLocal<Dictionary<Structure, float>> damagedStructuresLocal =
new ThreadLocal<Dictionary<Structure, float>>(() => new Dictionary<Structure, float>());
private static Dictionary<Structure, float> damagedStructures => damagedStructuresLocal.Value;
/// <summary>
/// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken
/// </summary>

View File

@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using Barotrauma.MapCreatures.Behavior;
using Barotrauma.Items.Components;
@@ -1133,13 +1134,18 @@ namespace Barotrauma
}
/// <summary>
/// Used in <see cref="GetApproximateDistance"/>
/// Used in <see cref="GetApproximateDistance"/> - ThreadLocal for thread safety
/// </summary>
private static readonly Dictionary<Hull, float> cachedDistances = [];
private static readonly ThreadLocal<Dictionary<Hull, float>> cachedDistancesLocal =
new ThreadLocal<Dictionary<Hull, float>>(() => new Dictionary<Hull, float>());
/// <summary>
/// Used in <see cref="GetApproximateDistance"/>
/// Used in <see cref="GetApproximateDistance"/> - ThreadLocal for thread safety
/// </summary>
private static readonly PriorityQueue<(Hull hull, Vector2 pos), float> priorityQueue = new PriorityQueue<(Hull hull, Vector2 pos), float>();
private static readonly ThreadLocal<PriorityQueue<(Hull hull, Vector2 pos), float>> priorityQueueLocal =
new ThreadLocal<PriorityQueue<(Hull hull, Vector2 pos), float>>(() => new PriorityQueue<(Hull hull, Vector2 pos), float>());
private static Dictionary<Hull, float> cachedDistances => cachedDistancesLocal.Value;
private static PriorityQueue<(Hull hull, Vector2 pos), float> priorityQueue => priorityQueueLocal.Value;
/// <summary>
/// Approximate distance from this hull to the target hull, moving through open gaps without passing through walls.

View File

@@ -12,6 +12,7 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using Voronoi2;
@@ -97,10 +98,11 @@ namespace Barotrauma
}
}
private static Vector2 lastPickedPosition;
private static float lastPickedFraction;
private static Fixture lastPickedFixture;
private static Vector2 lastPickedNormal;
// ThreadLocal for thread-safe ray casting results
private static readonly ThreadLocal<Vector2> lastPickedPositionLocal = new ThreadLocal<Vector2>();
private static readonly ThreadLocal<float> lastPickedFractionLocal = new ThreadLocal<float>();
private static readonly ThreadLocal<Fixture> lastPickedFixtureLocal = new ThreadLocal<Fixture>();
private static readonly ThreadLocal<Vector2> lastPickedNormalLocal = new ThreadLocal<Vector2>();
private Vector2 prevPosition;
@@ -114,22 +116,22 @@ namespace Barotrauma
public static Vector2 LastPickedPosition
{
get { return lastPickedPosition; }
get { return lastPickedPositionLocal.Value; }
}
public static float LastPickedFraction
{
get { return lastPickedFraction; }
get { return lastPickedFractionLocal.Value; }
}
public static Fixture LastPickedFixture
{
get { return lastPickedFixture; }
get { return lastPickedFixtureLocal.Value; }
}
public static Vector2 LastPickedNormal
{
get { return lastPickedNormal; }
get { return lastPickedNormalLocal.Value; }
}
public bool Loading
@@ -854,10 +856,10 @@ namespace Barotrauma
}, ref aabb);
if (closestFraction <= 0.0f)
{
lastPickedPosition = rayStart;
lastPickedFraction = closestFraction;
lastPickedFixture = closestFixture;
lastPickedNormal = closestNormal;
lastPickedPositionLocal.Value = rayStart;
lastPickedFractionLocal.Value = closestFraction;
lastPickedFixtureLocal.Value = closestFixture;
lastPickedNormalLocal.Value = closestNormal;
return closestBody;
}
}
@@ -876,16 +878,22 @@ namespace Barotrauma
return fraction;
}, rayStart, rayEnd, collisionCategory ?? Category.All);
lastPickedPosition = rayStart + (rayEnd - rayStart) * closestFraction;
lastPickedFraction = closestFraction;
lastPickedFixture = closestFixture;
lastPickedNormal = closestNormal;
lastPickedPositionLocal.Value = rayStart + (rayEnd - rayStart) * closestFraction;
lastPickedFractionLocal.Value = closestFraction;
lastPickedFixtureLocal.Value = closestFixture;
lastPickedNormalLocal.Value = closestNormal;
return closestBody;
}
private static readonly Dictionary<Body, float> bodyDist = new Dictionary<Body, float>();
private static readonly List<Body> bodies = new List<Body>();
// ThreadLocal for thread-safe body picking
private static readonly ThreadLocal<Dictionary<Body, float>> bodyDistLocal =
new ThreadLocal<Dictionary<Body, float>>(() => new Dictionary<Body, float>());
private static readonly ThreadLocal<List<Body>> bodiesLocal =
new ThreadLocal<List<Body>>(() => new List<Body>());
private static Dictionary<Body, float> bodyDist => bodyDistLocal.Value;
private static List<Body> bodies => bodiesLocal.Value;
public static float LastPickedBodyDist(Body body)
{
@@ -919,10 +927,10 @@ namespace Barotrauma
}
if (fraction < closestFraction)
{
lastPickedPosition = rayStart + (rayEnd - rayStart) * fraction;
lastPickedFraction = fraction;
lastPickedNormal = normal;
lastPickedFixture = fixture;
lastPickedPositionLocal.Value = rayStart + (rayEnd - rayStart) * fraction;
lastPickedFractionLocal.Value = fraction;
lastPickedNormalLocal.Value = normal;
lastPickedFixtureLocal.Value = fixture;
}
//continue
return -1;
@@ -940,10 +948,10 @@ namespace Barotrauma
if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; }
closestFraction = 0.0f;
lastPickedPosition = rayStart;
lastPickedFraction = 0.0f;
lastPickedNormal = Vector2.Normalize(rayEnd - rayStart);
lastPickedFixture = fixture;
lastPickedPositionLocal.Value = rayStart;
lastPickedFractionLocal.Value = 0.0f;
lastPickedNormalLocal.Value = Vector2.Normalize(rayEnd - rayStart);
lastPickedFixtureLocal.Value = fixture;
bodies.Add(fixture.Body);
bodyDist[fixture.Body] = 0.0f;
return false;
@@ -1011,7 +1019,7 @@ namespace Barotrauma
if (Vector2.DistanceSquared(rayStart, rayEnd) < 0.01f)
{
lastPickedPosition = rayEnd;
lastPickedPositionLocal.Value = rayEnd;
return null;
}
@@ -1053,10 +1061,10 @@ namespace Barotrauma
, rayStart, rayEnd);
lastPickedPosition = rayStart + (rayEnd - rayStart) * closestFraction;
lastPickedFraction = closestFraction;
lastPickedFixture = closestFixture;
lastPickedNormal = closestNormal;
lastPickedPositionLocal.Value = rayStart + (rayEnd - rayStart) * closestFraction;
lastPickedFractionLocal.Value = closestFraction;
lastPickedFixtureLocal.Value = closestFixture;
lastPickedNormalLocal.Value = closestNormal;
return closestBody;
}

View File

@@ -2,6 +2,7 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
@@ -75,7 +76,7 @@ namespace Barotrauma
return !corrected;
}
private static readonly Dictionary<string, string> cachedFileNames = new Dictionary<string, string>();
private static readonly ConcurrentDictionary<string, string> cachedFileNames = new ConcurrentDictionary<string, string>();
public static string CorrectFilenameCase(string filename, out bool corrected, string directory = "")
{
@@ -153,7 +154,7 @@ namespace Barotrauma
if (i < subDirs.Length - 1) { filename += "/"; }
}
cachedFileNames.Add(originalFilename, filename);
cachedFileNames.TryAdd(originalFilename, filename);
return filename;
}
@@ -355,32 +356,26 @@ namespace Barotrauma
return text;
}
private static Dictionary<string, List<string>> cachedLines = new Dictionary<string, List<string>>();
private static readonly ConcurrentDictionary<string, List<string>> cachedLines = new ConcurrentDictionary<string, List<string>>();
public static string GetRandomLine(string filePath, Rand.RandSync randSync = Rand.RandSync.ServerAndClient)
{
List<string> lines;
if (cachedLines.ContainsKey(filePath))
{
lines = cachedLines[filePath];
}
else
List<string> lines = cachedLines.GetOrAdd(filePath, path =>
{
try
{
lines = File.ReadAllLines(filePath, catchUnauthorizedAccessExceptions: false).ToList();
cachedLines.Add(filePath, lines);
if (lines.Count == 0)
var fileLines = File.ReadAllLines(path, catchUnauthorizedAccessExceptions: false).ToList();
if (fileLines.Count == 0)
{
DebugConsole.ThrowError("File \"" + filePath + "\" is empty!");
return "";
DebugConsole.ThrowError("File \"" + path + "\" is empty!");
}
return fileLines;
}
catch (Exception e)
{
DebugConsole.ThrowError("Couldn't open file \"" + filePath + "\"!", e);
return "";
DebugConsole.ThrowError("Couldn't open file \"" + path + "\"!", e);
return new List<string>();
}
}
});
if (lines.Count == 0) return "";
return lines[Rand.Range(0, lines.Count, randSync)];