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] =?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