diff --git a/GFramework.Game/Storage/FileStorage.cs b/GFramework.Game/Storage/FileStorage.cs index 2f08dce..e1140c6 100644 --- a/GFramework.Game/Storage/FileStorage.cs +++ b/GFramework.Game/Storage/FileStorage.cs @@ -1,8 +1,4 @@ -using System.IO; -using System.Text; -using GFramework.Core.Abstractions.Concurrency; -using GFramework.Core.Abstractions.Serializer; -using GFramework.Core.Concurrency; +using GFramework.Core.Concurrency; using GFramework.Game.Abstractions.Storage; namespace GFramework.Game.Storage; @@ -16,6 +12,7 @@ public sealed class FileStorage : IFileStorage, IDisposable private readonly int _bufferSize; private readonly string _extension; private readonly IAsyncKeyLockManager _lockManager; + private readonly bool _ownsLockManager; private readonly string _rootPath; private readonly ISerializer _serializer; private bool _disposed; @@ -35,7 +32,17 @@ public sealed class FileStorage : IFileStorage, IDisposable _serializer = serializer; _extension = extension; _bufferSize = bufferSize; - _lockManager = lockManager ?? new AsyncKeyLockManager(); + + if (lockManager == null) + { + _lockManager = new AsyncKeyLockManager(); + _ownsLockManager = true; + } + else + { + _lockManager = lockManager; + _ownsLockManager = false; + } Directory.CreateDirectory(_rootPath); } @@ -47,7 +54,12 @@ public sealed class FileStorage : IFileStorage, IDisposable { if (_disposed) return; _disposed = true; - _lockManager.Dispose(); + + // 只释放内部创建的锁管理器 + if (_ownsLockManager) + { + _lockManager.Dispose(); + } } /// @@ -108,9 +120,13 @@ public sealed class FileStorage : IFileStorage, IDisposable /// 删除指定键的存储项 /// /// 存储键,用于标识要删除的存储项 + /// + /// 此方法通过同步等待异步操作完成,可能在具有同步上下文的环境(例如 UI 线程、经典 ASP.NET)中导致死锁。 + /// 仅在无法使用异步 API 时使用。如果可能,请优先使用 。 + /// public void Delete(string key) { - DeleteAsync(key).GetAwaiter().GetResult(); + DeleteAsync(key).ConfigureAwait(false).GetAwaiter().GetResult(); } /// @@ -123,7 +139,7 @@ public sealed class FileStorage : IFileStorage, IDisposable ObjectDisposedException.ThrowIf(_disposed, this); var path = ToPath(key); - await using (await _lockManager.AcquireLockAsync(path)) + await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false)) { if (File.Exists(path)) File.Delete(path); @@ -139,9 +155,13 @@ public sealed class FileStorage : IFileStorage, IDisposable /// /// 存储键 /// 如果存储项存在则返回true,否则返回false + /// + /// 此方法通过同步等待异步操作完成,可能在具有同步上下文的环境(例如 UI 线程、经典 ASP.NET)中导致死锁。 + /// 仅在无法使用异步 API 时使用。如果可能,请优先使用 。 + /// public bool Exists(string key) { - return ExistsAsync(key).GetAwaiter().GetResult(); + return ExistsAsync(key).ConfigureAwait(false).GetAwaiter().GetResult(); } /// @@ -154,7 +174,7 @@ public sealed class FileStorage : IFileStorage, IDisposable ObjectDisposedException.ThrowIf(_disposed, this); var path = ToPath(key); - await using (await _lockManager.AcquireLockAsync(path)) + await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false)) { return File.Exists(path); } @@ -171,9 +191,13 @@ public sealed class FileStorage : IFileStorage, IDisposable /// 存储键 /// 反序列化后的对象 /// 当存储键不存在时抛出 + /// + /// 此方法通过同步等待异步操作完成,可能在具有同步上下文的环境(例如 UI 线程、经典 ASP.NET)中导致死锁。 + /// 仅在无法使用异步 API 时使用。如果可能,请优先使用 。 + /// public T Read(string key) { - return ReadAsync(key).GetAwaiter().GetResult(); + return ReadAsync(key).ConfigureAwait(false).GetAwaiter().GetResult(); } /// @@ -208,7 +232,7 @@ public sealed class FileStorage : IFileStorage, IDisposable ObjectDisposedException.ThrowIf(_disposed, this); var path = ToPath(key); - await using (await _lockManager.AcquireLockAsync(path)) + await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false)) { if (!File.Exists(path)) throw new FileNotFoundException($"Storage key not found: {key}", path); @@ -222,7 +246,7 @@ public sealed class FileStorage : IFileStorage, IDisposable useAsync: true); using var sr = new StreamReader(fs, Encoding.UTF8); - var content = await sr.ReadToEndAsync(); + var content = await sr.ReadToEndAsync().ConfigureAwait(false); return _serializer.Deserialize(content); } } @@ -304,9 +328,13 @@ public sealed class FileStorage : IFileStorage, IDisposable /// 要序列化的对象类型 /// 存储键 /// 要存储的对象 + /// + /// 此方法通过同步等待异步操作完成,可能在具有同步上下文的环境(例如 UI 线程、经典 ASP.NET)中导致死锁。 + /// 仅在无法使用异步 API 时使用。如果可能,请优先使用 。 + /// public void Write(string key, T value) { - WriteAsync(key, value).GetAwaiter().GetResult(); + WriteAsync(key, value).ConfigureAwait(false).GetAwaiter().GetResult(); } /// @@ -322,7 +350,7 @@ public sealed class FileStorage : IFileStorage, IDisposable var path = ToPath(key); var tempPath = path + ".tmp"; - await using (await _lockManager.AcquireLockAsync(path)) + await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false)) { try { @@ -338,8 +366,8 @@ public sealed class FileStorage : IFileStorage, IDisposable useAsync: true)) { await using var sw = new StreamWriter(fs, Encoding.UTF8); - await sw.WriteAsync(content); - await sw.FlushAsync(); + await sw.WriteAsync(content).ConfigureAwait(false); + await sw.FlushAsync().ConfigureAwait(false); } // 原子性替换目标文件 diff --git a/GFramework.Godot/Storage/GodotFileStorage.cs b/GFramework.Godot/Storage/GodotFileStorage.cs index 3fc22e3..ca46db7 100644 --- a/GFramework.Godot/Storage/GodotFileStorage.cs +++ b/GFramework.Godot/Storage/GodotFileStorage.cs @@ -1,13 +1,6 @@ -using System.IO; -using System.Text; using GFramework.Core.Abstractions.Concurrency; using GFramework.Core.Abstractions.Serializer; using GFramework.Core.Abstractions.Storage; -using GFramework.Core.Concurrency; -using GFramework.Godot.Extensions; -using Godot; -using Error = Godot.Error; -using FileAccess = Godot.FileAccess; namespace GFramework.Godot.Storage; @@ -18,6 +11,7 @@ namespace GFramework.Godot.Storage; public sealed class GodotFileStorage : IStorage, IDisposable { private readonly IAsyncKeyLockManager _lockManager; + private readonly bool _ownsLockManager; private readonly ISerializer _serializer; private bool _disposed; @@ -29,7 +23,17 @@ public sealed class GodotFileStorage : IStorage, IDisposable public GodotFileStorage(ISerializer serializer, IAsyncKeyLockManager? lockManager = null) { _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); - _lockManager = lockManager ?? new AsyncKeyLockManager(); + + if (lockManager == null) + { + _lockManager = new AsyncKeyLockManager(); + _ownsLockManager = true; + } + else + { + _lockManager = lockManager; + _ownsLockManager = false; + } } /// @@ -39,7 +43,12 @@ public sealed class GodotFileStorage : IStorage, IDisposable { if (_disposed) return; _disposed = true; - _lockManager.Dispose(); + + // 只释放内部创建的锁管理器 + if (_ownsLockManager) + { + _lockManager.Dispose(); + } } #region Delete @@ -48,9 +57,13 @@ public sealed class GodotFileStorage : IStorage, IDisposable /// 删除指定键对应的文件 /// /// 存储键 + /// + /// 此方法通过同步等待异步操作完成,可能在具有同步上下文的环境(例如 UI 线程、经典 ASP.NET)中导致死锁。 + /// 仅在无法使用异步 API 时使用。如果可能,请优先使用 。 + /// public void Delete(string key) { - DeleteAsync(key).GetAwaiter().GetResult(); + DeleteAsync(key).ConfigureAwait(false).GetAwaiter().GetResult(); } /// @@ -63,7 +76,7 @@ public sealed class GodotFileStorage : IStorage, IDisposable ObjectDisposedException.ThrowIf(_disposed, this); var path = ToAbsolutePath(key); - await using (await _lockManager.AcquireLockAsync(path)) + await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false)) { // 处理Godot文件系统路径的删除操作 if (path.IsGodotPath()) @@ -142,9 +155,13 @@ public sealed class GodotFileStorage : IStorage, IDisposable /// /// 存储键 /// 文件存在返回 true,否则返回 false + /// + /// 此方法通过同步等待异步操作完成,可能在具有同步上下文的环境(例如 UI 线程、经典 ASP.NET)中导致死锁。 + /// 仅在无法使用异步 API 时使用。如果可能,请优先使用 。 + /// public bool Exists(string key) { - return ExistsAsync(key).GetAwaiter().GetResult(); + return ExistsAsync(key).ConfigureAwait(false).GetAwaiter().GetResult(); } /// @@ -157,7 +174,7 @@ public sealed class GodotFileStorage : IStorage, IDisposable ObjectDisposedException.ThrowIf(_disposed, this); var path = ToAbsolutePath(key); - await using (await _lockManager.AcquireLockAsync(path)) + await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false)) { if (!path.IsGodotPath()) return File.Exists(path); using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); @@ -176,9 +193,13 @@ public sealed class GodotFileStorage : IStorage, IDisposable /// 存储键 /// 反序列化后的对象实例 /// 当指定键对应的文件不存在时抛出 + /// + /// 此方法通过同步等待异步操作完成,可能在具有同步上下文的环境(例如 UI 线程、经典 ASP.NET)中导致死锁。 + /// 仅在无法使用异步 API 时使用。如果可能,请优先使用 。 + /// public T Read(string key) { - return ReadAsync(key).GetAwaiter().GetResult(); + return ReadAsync(key).ConfigureAwait(false).GetAwaiter().GetResult(); } /// @@ -212,7 +233,7 @@ public sealed class GodotFileStorage : IStorage, IDisposable ObjectDisposedException.ThrowIf(_disposed, this); var path = ToAbsolutePath(key); - await using (await _lockManager.AcquireLockAsync(path)) + await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false)) { string content; @@ -226,7 +247,7 @@ public sealed class GodotFileStorage : IStorage, IDisposable { if (!File.Exists(path)) throw new FileNotFoundException($"Storage key not found: {key}", path); - content = await File.ReadAllTextAsync(path, Encoding.UTF8); + content = await File.ReadAllTextAsync(path, Encoding.UTF8).ConfigureAwait(false); } return _serializer.Deserialize(content); @@ -331,9 +352,13 @@ public sealed class GodotFileStorage : IStorage, IDisposable /// 要序列化的对象类型 /// 存储键 /// 要写入的对象实例 + /// + /// 此方法通过同步等待异步操作完成,可能在具有同步上下文的环境(例如 UI 线程、经典 ASP.NET)中导致死锁。 + /// 仅在无法使用异步 API 时使用。如果可能,请优先使用 。 + /// public void Write(string key, T value) { - WriteAsync(key, value).GetAwaiter().GetResult(); + WriteAsync(key, value).ConfigureAwait(false).GetAwaiter().GetResult(); } /// @@ -348,7 +373,7 @@ public sealed class GodotFileStorage : IStorage, IDisposable ObjectDisposedException.ThrowIf(_disposed, this); var path = ToAbsolutePath(key); - await using (await _lockManager.AcquireLockAsync(path)) + await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false)) { var content = _serializer.Serialize(value); if (path.IsGodotPath()) @@ -360,7 +385,7 @@ public sealed class GodotFileStorage : IStorage, IDisposable else { Directory.CreateDirectory(Path.GetDirectoryName(path)!); - await File.WriteAllTextAsync(path, content, Encoding.UTF8); + await File.WriteAllTextAsync(path, content, Encoding.UTF8).ConfigureAwait(false); } } }