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] =?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);
}
}
}