From 494c341c084c5a07b865eedf05330a9dc1e4ddee Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 11 Mar 2026 21:57:04 +0800 Subject: [PATCH 1/3] =?UTF-8?q?refactor(storage):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=AD=98=E5=82=A8=E5=AE=9E=E7=8E=B0=E4=BB=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=BC=82=E6=AD=A5=E5=AE=89=E5=85=A8=E9=94=81?= =?UTF-8?q?=E5=92=8C=E5=8E=9F=E5=AD=90=E5=86=99=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 替换同步锁机制为异步键锁管理器,提升并发性能 - 实现原子写入功能,通过临时文件防止写入过程中的数据损坏 - 添加资源释放接口(IDisposable)和对象销毁检查 - 集成异步IO操作,包括缓冲区大小配置选项 - 更新Godot文件存储适配器以匹配新的异步安全机制 - 优化文件读取操作,支持异步文本读取避免阻塞 - 移除旧的并发字典锁实现,统一使用新的锁管理器 --- GFramework.Game/Storage/FileStorage.cs | 176 ++++++++------- GFramework.Godot/Storage/GodotFileStorage.cs | 222 ++++++++----------- 2 files changed, 193 insertions(+), 205 deletions(-) diff --git a/GFramework.Game/Storage/FileStorage.cs b/GFramework.Game/Storage/FileStorage.cs index c593697..2f08dce 100644 --- a/GFramework.Game/Storage/FileStorage.cs +++ b/GFramework.Game/Storage/FileStorage.cs @@ -1,21 +1,24 @@ using System.IO; using System.Text; +using GFramework.Core.Abstractions.Concurrency; using GFramework.Core.Abstractions.Serializer; +using GFramework.Core.Concurrency; using GFramework.Game.Abstractions.Storage; namespace GFramework.Game.Storage; /// /// 基于文件系统的存储实现,实现了IFileStorage接口,支持按key细粒度锁保证线程安全 +/// 使用异步安全的锁机制、原子写入和自动清理 /// -public sealed class FileStorage : IFileStorage +public sealed class FileStorage : IFileStorage, IDisposable { + private readonly int _bufferSize; private readonly string _extension; - - // 每个key对应的锁对象 - private readonly ConcurrentDictionary _keyLocks = new(); + private readonly IAsyncKeyLockManager _lockManager; private readonly string _rootPath; private readonly ISerializer _serializer; + private bool _disposed; /// /// 初始化FileStorage实例 @@ -23,15 +26,30 @@ public sealed class FileStorage : IFileStorage /// 存储根目录路径 /// 序列化器实例 /// 存储文件的扩展名 - public FileStorage(string rootPath, ISerializer serializer, string extension = ".dat") + /// IO 缓冲区大小,默认 8KB + /// 可选的锁管理器,用于依赖注入 + public FileStorage(string rootPath, ISerializer serializer, string extension = ".dat", int bufferSize = 8192, + IAsyncKeyLockManager? lockManager = null) { _rootPath = rootPath; _serializer = serializer; _extension = extension; + _bufferSize = bufferSize; + _lockManager = lockManager ?? new AsyncKeyLockManager(); Directory.CreateDirectory(_rootPath); } + /// + /// 释放资源 + /// + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _lockManager.Dispose(); + } + /// /// 清理文件段字符串,将其中的无效文件名字符替换为下划线 /// @@ -92,19 +110,7 @@ public sealed class FileStorage : IFileStorage /// 存储键,用于标识要删除的存储项 public void Delete(string key) { - // 将键转换为文件路径 - var path = ToPath(key); - - // 获取或创建与路径关联的锁对象,确保线程安全 - var keyLock = _keyLocks.GetOrAdd(path, _ => new object()); - - // 使用锁确保同一时间只有一个线程操作该路径的文件 - lock (keyLock) - { - // 如果文件存在,则删除该文件 - if (File.Exists(path)) - File.Delete(path); - } + DeleteAsync(key).GetAwaiter().GetResult(); } /// @@ -112,10 +118,16 @@ public sealed class FileStorage : IFileStorage /// /// 存储键,用于标识要删除的存储项 /// 表示异步操作的任务 - public Task DeleteAsync(string key) + public async Task DeleteAsync(string key) { - // 在线程池中运行同步删除方法以实现异步操作 - return Task.Run(() => Delete(key)); + ObjectDisposedException.ThrowIf(_disposed, this); + var path = ToPath(key); + + await using (await _lockManager.AcquireLockAsync(path)) + { + if (File.Exists(path)) + File.Delete(path); + } } #endregion @@ -129,13 +141,7 @@ public sealed class FileStorage : IFileStorage /// 如果存储项存在则返回true,否则返回false public bool Exists(string key) { - var path = ToPath(key); - var keyLock = _keyLocks.GetOrAdd(path, _ => new object()); - - lock (keyLock) - { - return File.Exists(path); - } + return ExistsAsync(key).GetAwaiter().GetResult(); } /// @@ -143,9 +149,15 @@ public sealed class FileStorage : IFileStorage /// /// 存储键 /// 如果存储项存在则返回true,否则返回false - public Task ExistsAsync(string key) + public async Task ExistsAsync(string key) { - return Task.FromResult(Exists(key)); + ObjectDisposedException.ThrowIf(_disposed, this); + var path = ToPath(key); + + await using (await _lockManager.AcquireLockAsync(path)) + { + return File.Exists(path); + } } #endregion @@ -161,17 +173,7 @@ public sealed class FileStorage : IFileStorage /// 当存储键不存在时抛出 public T Read(string key) { - var path = ToPath(key); - var keyLock = _keyLocks.GetOrAdd(path, _ => new object()); - - lock (keyLock) - { - if (!File.Exists(path)) - throw new FileNotFoundException($"Storage key not found: {key}", path); - - var content = File.ReadAllText(path, Encoding.UTF8); - return _serializer.Deserialize(content); - } + return ReadAsync(key).GetAwaiter().GetResult(); } /// @@ -183,16 +185,14 @@ public sealed class FileStorage : IFileStorage /// 反序列化后的对象或默认值 public T Read(string key, T defaultValue) { - var path = ToPath(key); - var keyLock = _keyLocks.GetOrAdd(path, _ => new object()); - - lock (keyLock) + ObjectDisposedException.ThrowIf(_disposed, this); + try { - if (!File.Exists(path)) - return defaultValue; - - var content = File.ReadAllText(path, Encoding.UTF8); - return _serializer.Deserialize(content); + return Read(key); + } + catch (FileNotFoundException) + { + return defaultValue; } } @@ -205,25 +205,26 @@ public sealed class FileStorage : IFileStorage /// 当存储键不存在时抛出 public async Task ReadAsync(string key) { + ObjectDisposedException.ThrowIf(_disposed, this); var path = ToPath(key); - var keyLock = _keyLocks.GetOrAdd(path, _ => new object()); - // 异步操作依然使用lock保护文件读写 - lock (keyLock) + await using (await _lockManager.AcquireLockAsync(path)) { if (!File.Exists(path)) throw new FileNotFoundException($"Storage key not found: {key}", path); - } - // 读取文件内容可以使用异步IO,但要注意锁范围 - string content; - await using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) - using (var sr = new StreamReader(fs, Encoding.UTF8)) - { - content = await sr.ReadToEndAsync(); - } + await using var fs = new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + _bufferSize, + useAsync: true); - return _serializer.Deserialize(content); + using var sr = new StreamReader(fs, Encoding.UTF8); + var content = await sr.ReadToEndAsync(); + return _serializer.Deserialize(content); + } } #endregion @@ -305,18 +306,11 @@ public sealed class FileStorage : IFileStorage /// 要存储的对象 public void Write(string key, T value) { - var path = ToPath(key); - var keyLock = _keyLocks.GetOrAdd(path, _ => new object()); - var content = _serializer.Serialize(value); - - lock (keyLock) - { - File.WriteAllText(path, content, Encoding.UTF8); - } + WriteAsync(key, value).GetAwaiter().GetResult(); } /// - /// 异步写入指定键的存储项 + /// 异步写入指定键的存储项,使用原子写入防止文件损坏 /// /// 要序列化的对象类型 /// 存储键 @@ -324,19 +318,41 @@ public sealed class FileStorage : IFileStorage /// 表示异步操作的任务 public async Task WriteAsync(string key, T value) { + ObjectDisposedException.ThrowIf(_disposed, this); var path = ToPath(key); - var keyLock = _keyLocks.GetOrAdd(path, _ => new object()); - var content = _serializer.Serialize(value); + var tempPath = path + ".tmp"; - // 异步写也需要锁 - lock (keyLock) + await using (await _lockManager.AcquireLockAsync(path)) { - using var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); - using var sw = new StreamWriter(fs, Encoding.UTF8); - sw.WriteAsync(content); - } + try + { + var content = _serializer.Serialize(value); - await Task.CompletedTask; + // 先写入临时文件 + await using (var fs = new FileStream( + tempPath, + FileMode.Create, + FileAccess.Write, + FileShare.None, + _bufferSize, + useAsync: true)) + { + await using var sw = new StreamWriter(fs, Encoding.UTF8); + await sw.WriteAsync(content); + await sw.FlushAsync(); + } + + // 原子性替换目标文件 + File.Move(tempPath, path, overwrite: true); + } + catch + { + // 清理临时文件 + if (File.Exists(tempPath)) + File.Delete(tempPath); + throw; + } + } } #endregion diff --git a/GFramework.Godot/Storage/GodotFileStorage.cs b/GFramework.Godot/Storage/GodotFileStorage.cs index 4ef608e..3fc22e3 100644 --- a/GFramework.Godot/Storage/GodotFileStorage.cs +++ b/GFramework.Godot/Storage/GodotFileStorage.cs @@ -1,8 +1,9 @@ -using System.Collections.Concurrent; 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; @@ -12,24 +13,33 @@ namespace GFramework.Godot.Storage; /// /// Godot 特化的文件存储实现,支持 res://、user:// 和普通文件路径 -/// 支持按 key 细粒度锁保证线程安全 +/// 支持按 key 细粒度锁保证线程安全,使用异步安全的锁机制 /// -public sealed class GodotFileStorage : IStorage +public sealed class GodotFileStorage : IStorage, IDisposable { - /// - /// 每个 key 对应的锁对象 - /// - private readonly ConcurrentDictionary _keyLocks = new(); - + private readonly IAsyncKeyLockManager _lockManager; private readonly ISerializer _serializer; + private bool _disposed; /// /// 初始化 Godot 文件存储 /// /// 序列化器实例 - public GodotFileStorage(ISerializer serializer) + /// 可选的锁管理器,用于依赖注入 + public GodotFileStorage(ISerializer serializer, IAsyncKeyLockManager? lockManager = null) { _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + _lockManager = lockManager ?? new AsyncKeyLockManager(); + } + + /// + /// 释放资源 + /// + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _lockManager.Dispose(); } #region Delete @@ -40,10 +50,20 @@ public sealed class GodotFileStorage : IStorage /// 存储键 public void Delete(string key) { - var path = ToAbsolutePath(key); - var keyLock = GetLock(path); + DeleteAsync(key).GetAwaiter().GetResult(); + } - lock (keyLock) + /// + /// 异步删除指定键对应的文件 + /// + /// 存储键 + /// 异步任务 + public async Task DeleteAsync(string key) + { + ObjectDisposedException.ThrowIf(_disposed, this); + var path = ToAbsolutePath(key); + + await using (await _lockManager.AcquireLockAsync(path)) { // 处理Godot文件系统路径的删除操作 if (path.IsGodotPath()) @@ -61,19 +81,6 @@ public sealed class GodotFileStorage : IStorage if (File.Exists(path)) File.Delete(path); } } - - // 删除完成后尝试移除锁,防止锁字典无限增长 - _keyLocks.TryRemove(path, out _); - } - - /// - /// 异步删除指定键对应的文件 - /// - /// 存储键 - /// 异步任务 - public async Task DeleteAsync(string key) - { - await Task.Run(() => Delete(key)); } #endregion @@ -126,16 +133,6 @@ public sealed class GodotFileStorage : IStorage return Path.Combine(dir, fileName); } - /// - /// 获取指定路径对应的锁对象,如果不存在则创建新的锁对象 - /// - /// 文件路径 - /// 对应路径的锁对象 - private object GetLock(string path) - { - return _keyLocks.GetOrAdd(path, _ => new object()); - } - #endregion #region Exists @@ -147,15 +144,7 @@ public sealed class GodotFileStorage : IStorage /// 文件存在返回 true,否则返回 false public bool Exists(string key) { - var path = ToAbsolutePath(key); - var keyLock = GetLock(path); - - lock (keyLock) - { - if (!path.IsGodotPath()) return File.Exists(path); - using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); - return file != null; - } + return ExistsAsync(key).GetAwaiter().GetResult(); } /// @@ -163,9 +152,17 @@ public sealed class GodotFileStorage : IStorage /// /// 存储键 /// 表示异步操作的任务,结果为布尔值表示文件是否存在 - public Task ExistsAsync(string key) + public async Task ExistsAsync(string key) { - return Task.FromResult(Exists(key)); + ObjectDisposedException.ThrowIf(_disposed, this); + var path = ToAbsolutePath(key); + + await using (await _lockManager.AcquireLockAsync(path)) + { + if (!path.IsGodotPath()) return File.Exists(path); + using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); + return file != null; + } } #endregion @@ -181,10 +178,41 @@ public sealed class GodotFileStorage : IStorage /// 当指定键对应的文件不存在时抛出 public T Read(string key) { - var path = ToAbsolutePath(key); - var keyLock = GetLock(path); + return ReadAsync(key).GetAwaiter().GetResult(); + } - lock (keyLock) + /// + /// 读取指定键对应的序列化数据,如果文件不存在则返回默认值 + /// + /// 要反序列化的类型 + /// 存储键 + /// 当文件不存在时返回的默认值 + /// 反序列化后的对象实例或默认值 + public T Read(string key, T defaultValue) + { + ObjectDisposedException.ThrowIf(_disposed, this); + try + { + return Read(key); + } + catch (FileNotFoundException) + { + return defaultValue; + } + } + + /// + /// 异步读取指定键对应的序列化数据并反序列化为指定类型 + /// + /// 要反序列化的类型 + /// 存储键 + /// 表示异步操作的任务,结果为反序列化后的对象实例 + public async Task ReadAsync(string key) + { + ObjectDisposedException.ThrowIf(_disposed, this); + var path = ToAbsolutePath(key); + + await using (await _lockManager.AcquireLockAsync(path)) { string content; @@ -198,69 +226,13 @@ public sealed class GodotFileStorage : IStorage { if (!File.Exists(path)) throw new FileNotFoundException($"Storage key not found: {key}", path); - content = File.ReadAllText(path, Encoding.UTF8); + content = await File.ReadAllTextAsync(path, Encoding.UTF8); } return _serializer.Deserialize(content); } } - /// - /// 读取指定键对应的序列化数据,如果文件不存在则返回默认值 - /// - /// 要反序列化的类型 - /// 存储键 - /// 当文件不存在时返回的默认值 - /// 反序列化后的对象实例或默认值 - public T Read(string key, T defaultValue) - { - var path = ToAbsolutePath(key); - var keyLock = GetLock(path); - - lock (keyLock) - { - if ((path.IsGodotPath() && !FileAccess.FileExists(path)) || (!path.IsGodotPath() && !File.Exists(path))) - return defaultValue; - - return Read(key); - } - } - - /// - /// 异步读取指定键对应的序列化数据并反序列化为指定类型 - /// - /// 要反序列化的类型 - /// 存储键 - /// 表示异步操作的任务,结果为反序列化后的对象实例 - public async Task ReadAsync(string key) - { - var path = ToAbsolutePath(key); - var keyLock = GetLock(path); - - return await Task.Run(() => - { - lock (keyLock) - { - string content; - - if (path.IsGodotPath()) - { - using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); - if (file == null) throw new FileNotFoundException($"Storage key not found: {key}", path); - content = file.GetAsText(); - } - else - { - if (!File.Exists(path)) - throw new FileNotFoundException($"Storage key not found: {key}", path); - content = File.ReadAllText(path, Encoding.UTF8); - } - - return _serializer.Deserialize(content); - } - }); - } - #endregion #region Directory Operations @@ -361,24 +333,7 @@ public sealed class GodotFileStorage : IStorage /// 要写入的对象实例 public void Write(string key, T value) { - var path = ToAbsolutePath(key); - var keyLock = GetLock(path); - - lock (keyLock) - { - var content = _serializer.Serialize(value); - if (path.IsGodotPath()) - { - using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write); - if (file == null) throw new IOException($"Cannot write file: {path}"); - file.StoreString(content); - } - else - { - Directory.CreateDirectory(Path.GetDirectoryName(path)!); - File.WriteAllText(path, content, Encoding.UTF8); - } - } + WriteAsync(key, value).GetAwaiter().GetResult(); } /// @@ -390,7 +345,24 @@ public sealed class GodotFileStorage : IStorage /// 表示异步操作的任务 public async Task WriteAsync(string key, T value) { - await Task.Run(() => Write(key, value)); + ObjectDisposedException.ThrowIf(_disposed, this); + var path = ToAbsolutePath(key); + + await using (await _lockManager.AcquireLockAsync(path)) + { + var content = _serializer.Serialize(value); + if (path.IsGodotPath()) + { + using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write); + if (file == null) throw new IOException($"Cannot write file: {path}"); + file.StoreString(content); + } + else + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + await File.WriteAllTextAsync(path, content, Encoding.UTF8); + } + } } #endregion From b01867b231540681ec5197773a4047bd2deedf6f Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 11 Mar 2026 22:26:01 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(storage):=20=E4=BF=AE=E5=A4=8D=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=AD=98=E5=82=A8=E7=BB=84=E4=BB=B6=E7=9A=84=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E7=AE=A1=E7=90=86=E5=92=8C=E6=AD=BB=E9=94=81=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了内部锁管理器所有权标识,防止外部传入的锁管理器被错误释放 - 在构造函数中正确初始化锁管理器的所有权状态 - 在Dispose方法中只释放内部创建的锁管理器,避免重复释放异常 - 为所有同步包装方法添加了ConfigureAwait(false)以避免死锁 - 更新了读取、写入、删除和检查存在的同步方法实现 - 为所有异步操作添加了适当的配置避免上下文切换问题 - 改进了Godot文件存储类的相同资源管理逻辑 - 为所有阻塞式同步方法添加了详细的XML注释警告说明 --- GFramework.Game/Storage/FileStorage.cs | 64 ++++++++++++++------ GFramework.Godot/Storage/GodotFileStorage.cs | 63 +++++++++++++------ 2 files changed, 90 insertions(+), 37 deletions(-) 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); } } } From 820cdcf0fae05b08c745744429ec82fae804ea20 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 11 Mar 2026 22:34:30 +0800 Subject: [PATCH 3/3] =?UTF-8?q?refactor(storage):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=AD=98=E5=82=A8=E6=A8=A1=E5=9D=97=E7=9A=84?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E6=B3=A8=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 FileStorage 中添加 System.IO 和 System.Text 引用 - 在 GodotFileStorage 中整合所有必要的命名空间引用 - 统一并发和序列化接口的依赖注入方式 - 添加 Godot 特定的 FileAccess 类型别名 - 优化 Godot 扩展功能的引用结构 --- GFramework.Game/Storage/FileStorage.cs | 6 +++++- GFramework.Godot/Storage/GodotFileStorage.cs | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/GFramework.Game/Storage/FileStorage.cs b/GFramework.Game/Storage/FileStorage.cs index e1140c6..383c029 100644 --- a/GFramework.Game/Storage/FileStorage.cs +++ b/GFramework.Game/Storage/FileStorage.cs @@ -1,4 +1,8 @@ -using GFramework.Core.Concurrency; +using System.IO; +using System.Text; +using GFramework.Core.Abstractions.Concurrency; +using GFramework.Core.Abstractions.Serializer; +using GFramework.Core.Concurrency; using GFramework.Game.Abstractions.Storage; namespace GFramework.Game.Storage; diff --git a/GFramework.Godot/Storage/GodotFileStorage.cs b/GFramework.Godot/Storage/GodotFileStorage.cs index ca46db7..1256520 100644 --- a/GFramework.Godot/Storage/GodotFileStorage.cs +++ b/GFramework.Godot/Storage/GodotFileStorage.cs @@ -1,6 +1,12 @@ +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 FileAccess = Godot.FileAccess; namespace GFramework.Godot.Storage;