Make entity lists thread-safe with copy-on-write wrappers
Replaced static entity lists (e.g., HullList, GapList, MapEntityList, etc.) with thread-safe copy-on-write wrappers to improve concurrency and prevent race conditions. Updated usages and related methods to support the new thread-safe collections, ensuring atomic operations and lock-free reads throughout the codebase.
This commit is contained in:
@@ -6,14 +6,111 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// Thread-safe wrapper for MapEntity list operations.
|
||||
/// Uses copy-on-write pattern for lock-free reads.
|
||||
/// </summary>
|
||||
internal class ThreadSafeMapEntityList : IEnumerable<MapEntity>
|
||||
{
|
||||
private volatile List<MapEntity> _list = new List<MapEntity>();
|
||||
private readonly object _writeLock = new object();
|
||||
|
||||
public int Count => _list.Count;
|
||||
|
||||
public void Add(MapEntity entity)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<MapEntity>(_list) { entity };
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
}
|
||||
|
||||
public void Insert(int index, MapEntity entity)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<MapEntity>(_list);
|
||||
newList.Insert(index, entity);
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Atomically inserts an entity at a position determined by the insertAction.
|
||||
/// The insertAction is executed within the lock to ensure thread-safety.
|
||||
/// </summary>
|
||||
public void InsertWithAction(MapEntity entity, Action<List<MapEntity>, MapEntity> insertAction)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<MapEntity>(_list);
|
||||
insertAction(newList, entity);
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(MapEntity entity)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<MapEntity>(_list);
|
||||
bool removed = newList.Remove(entity);
|
||||
if (removed)
|
||||
{
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
public int RemoveAll(Predicate<MapEntity> match)
|
||||
{
|
||||
lock (_writeLock)
|
||||
{
|
||||
var newList = new List<MapEntity>(_list);
|
||||
int count = newList.RemoveAll(match);
|
||||
if (count > 0)
|
||||
{
|
||||
Interlocked.Exchange(ref _list, newList);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Interlocked.Exchange(ref _list, new List<MapEntity>());
|
||||
}
|
||||
|
||||
public bool Contains(MapEntity entity) => _list.Contains(entity);
|
||||
|
||||
public MapEntity this[int index] => _list[index];
|
||||
|
||||
public IEnumerator<MapEntity> GetEnumerator() => _list.GetEnumerator();
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
// LINQ-friendly methods that work on a snapshot
|
||||
public List<MapEntity> ToList() => new List<MapEntity>(_list);
|
||||
public MapEntity FirstOrDefault(Func<MapEntity, bool> predicate) => _list.FirstOrDefault(predicate);
|
||||
public MapEntity Find(Predicate<MapEntity> predicate) => _list.Find(predicate);
|
||||
public List<MapEntity> FindAll(Predicate<MapEntity> predicate) => _list.FindAll(predicate);
|
||||
public IEnumerable<MapEntity> Where(Func<MapEntity, bool> predicate) => _list.Where(predicate);
|
||||
public bool Any(Func<MapEntity, bool> predicate) => _list.Any(predicate);
|
||||
public bool Exists(Predicate<MapEntity> predicate) => _list.Exists(predicate);
|
||||
public IOrderedEnumerable<MapEntity> OrderBy<TKey>(Func<MapEntity, TKey> keySelector) => _list.OrderBy(keySelector);
|
||||
public void ForEach(Action<MapEntity> action) => _list.ForEach(action);
|
||||
}
|
||||
|
||||
abstract partial class MapEntity : Entity, ISpatialEntity
|
||||
{
|
||||
public readonly static List<MapEntity> MapEntityList = new List<MapEntity>();
|
||||
public readonly static ThreadSafeMapEntityList MapEntityList = new ThreadSafeMapEntityList();
|
||||
|
||||
public readonly MapEntityPrefab Prefab;
|
||||
|
||||
@@ -559,45 +656,51 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
//sort damageable walls by sprite depth:
|
||||
//necessary because rendering the damage effect starts a new sprite batch and breaks the order otherwise
|
||||
int i = 0;
|
||||
if (this is Structure { DrawDamageEffect: true } structure)
|
||||
// Use atomic insertion to ensure thread-safety
|
||||
MapEntityList.InsertWithAction(this, (list, entity) =>
|
||||
{
|
||||
//insertion sort according to draw depth
|
||||
float drawDepth = structure.SpriteDepth;
|
||||
while (i < MapEntityList.Count)
|
||||
int i = 0;
|
||||
|
||||
//sort damageable walls by sprite depth:
|
||||
//necessary because rendering the damage effect starts a new sprite batch and breaks the order otherwise
|
||||
if (entity is Structure { DrawDamageEffect: true } structure)
|
||||
{
|
||||
float otherDrawDepth = (MapEntityList[i] as Structure)?.SpriteDepth ?? 1.0f;
|
||||
if (otherDrawDepth < drawDepth) { break; }
|
||||
i++;
|
||||
}
|
||||
MapEntityList.Insert(i, this);
|
||||
return;
|
||||
}
|
||||
|
||||
i = 0;
|
||||
while (i < MapEntityList.Count)
|
||||
{
|
||||
i++;
|
||||
if (MapEntityList[i - 1]?.Prefab == Prefab)
|
||||
{
|
||||
MapEntityList.Insert(i, this);
|
||||
//insertion sort according to draw depth
|
||||
float drawDepth = structure.SpriteDepth;
|
||||
while (i < list.Count)
|
||||
{
|
||||
float otherDrawDepth = (list[i] as Structure)?.SpriteDepth ?? 1.0f;
|
||||
if (otherDrawDepth < drawDepth) { break; }
|
||||
i++;
|
||||
}
|
||||
list.Insert(i, entity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
i = 0;
|
||||
var mapEntity = (MapEntity)entity;
|
||||
while (i < list.Count)
|
||||
{
|
||||
i++;
|
||||
if (list[i - 1]?.Prefab == mapEntity.Prefab)
|
||||
{
|
||||
list.Insert(i, entity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
i = 0;
|
||||
while (i < MapEntityList.Count)
|
||||
{
|
||||
i++;
|
||||
Sprite existingSprite = MapEntityList[i - 1].Sprite;
|
||||
if (existingSprite == null) { continue; }
|
||||
if (existingSprite.Texture == this.Sprite.Texture) { break; }
|
||||
}
|
||||
i = 0;
|
||||
while (i < list.Count)
|
||||
{
|
||||
i++;
|
||||
Sprite existingSprite = list[i - 1].Sprite;
|
||||
if (existingSprite == null) { continue; }
|
||||
if (existingSprite.Texture == mapEntity.Sprite?.Texture) { break; }
|
||||
}
|
||||
#endif
|
||||
MapEntityList.Insert(i, this);
|
||||
list.Insert(i, entity);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user