From f984f4a600912be58aa775e75cf7cf2c2aa26373 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Wed, 4 Mar 2026 09:42:17 +0800
Subject: [PATCH] =?UTF-8?q?refactor(core):=20=E4=BC=98=E5=8C=96=E6=A0=B8?=
=?UTF-8?q?=E5=BF=83=E7=BB=84=E4=BB=B6=E7=9A=84=E7=BA=BF=E7=A8=8B=E5=AE=89?=
=?UTF-8?q?=E5=85=A8=E6=80=A7=E5=92=8C=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 重构 AsyncLogAppender 的 Flush 方法,添加超时控制和 SpinWait 优化
- 为 BindableProperty 添加线程安全锁保护,确保并发访问的安全性
- 在 BindableProperty 中实现回调外部调用以避免死锁问题
- 为 EasyEvents 使用 ConcurrentDictionary 替代 Dictionary 提高并发性能
- 添加协程调度器异常处理回调机制,防止异常传播导致调度器崩溃
- 为 FileAppender 添加初始化异常处理和资源清理逻辑
- 补充完整的单元测试覆盖并发场景下的线程安全性验证
---
.../events/EasyEventsTests.cs | 112 +++++++++++-
.../property/BindablePropertyTests.cs | 162 ++++++++++++++++++
.../coroutine/CoroutineScheduler.cs | 25 ++-
GFramework.Core/events/EasyEvents.cs | 21 ++-
.../logging/appenders/AsyncLogAppender.cs | 28 ++-
.../logging/appenders/FileAppender.cs | 16 +-
GFramework.Core/property/BindableProperty.cs | 53 ++++--
7 files changed, 384 insertions(+), 33 deletions(-)
diff --git a/GFramework.Core.Tests/events/EasyEventsTests.cs b/GFramework.Core.Tests/events/EasyEventsTests.cs
index fda6199..ae03023 100644
--- a/GFramework.Core.Tests/events/EasyEventsTests.cs
+++ b/GFramework.Core.Tests/events/EasyEventsTests.cs
@@ -21,7 +21,7 @@ public class EasyEventsTests
private EasyEvents _easyEvents = null!;
///
- /// 测试单参数事件的功能,验证事件能够正确接收并传递int类型参数
+ /// 测试单参数事件的功能,验证事件能够正确接收并传递int类型参数
///
[Test]
public void Get_EventT_Should_Trigger_With_Parameter()
@@ -38,7 +38,7 @@ public class EasyEventsTests
}
///
- /// 测试双参数事件的功能,验证事件能够正确接收并传递int和string类型的参数
+ /// 测试双参数事件的功能,验证事件能够正确接收并传递int和string类型的参数
///
[Test]
public void Get_EventTTK_Should_Trigger_With_Two_Parameters()
@@ -59,4 +59,112 @@ public class EasyEventsTests
Assert.That(receivedInt, Is.EqualTo(100));
Assert.That(receivedString, Is.EqualTo("hello"));
}
+
+ ///
+ /// 测试并发场景下GetOrAdd的线程安全性
+ ///
+ [Test]
+ public void GetOrAdd_Should_Be_Thread_Safe()
+ {
+ const int threadCount = 10;
+ const int iterationsPerThread = 100;
+ var tasks = new Task[threadCount];
+ var exceptions = new List();
+
+ for (var i = 0; i < threadCount; i++)
+ {
+ tasks[i] = Task.Run(() =>
+ {
+ try
+ {
+ for (var j = 0; j < iterationsPerThread; j++)
+ {
+ var @event = _easyEvents.GetOrAddEvent>();
+ Assert.That(@event, Is.Not.Null);
+ }
+ }
+ catch (Exception ex)
+ {
+ lock (exceptions)
+ {
+ exceptions.Add(ex);
+ }
+ }
+ });
+ }
+
+ Task.WaitAll(tasks);
+
+ Assert.That(exceptions, Is.Empty, $"并发测试中发生异常: {string.Join(", ", exceptions.Select(e => e.Message))}");
+ }
+
+ ///
+ /// 测试并发场景下AddEvent的行为
+ ///
+ [Test]
+ public void AddEvent_Should_Throw_When_Already_Registered()
+ {
+ _easyEvents.AddEvent>();
+
+ Assert.Throws(() => _easyEvents.AddEvent>());
+ }
+
+ ///
+ /// 测试并发场景下多个不同事件类型的注册
+ ///
+ [Test]
+ public void Concurrent_Registration_Of_Different_Event_Types_Should_Work()
+ {
+ const int threadCount = 5;
+ var tasks = new Task[threadCount];
+ var exceptions = new List();
+
+ // 每个线程注册不同类型的事件
+ for (var i = 0; i < threadCount; i++)
+ {
+ var index = i;
+ tasks[i] = Task.Run(() =>
+ {
+ try
+ {
+ switch (index)
+ {
+ case 0:
+ _easyEvents.GetOrAddEvent>();
+ break;
+ case 1:
+ _easyEvents.GetOrAddEvent>();
+ break;
+ case 2:
+ _easyEvents.GetOrAddEvent>();
+ break;
+ case 3:
+ _easyEvents.GetOrAddEvent>();
+ break;
+ case 4:
+ _easyEvents.GetOrAddEvent>();
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ lock (exceptions)
+ {
+ exceptions.Add(ex);
+ }
+ }
+ });
+ }
+
+ Task.WaitAll(tasks);
+
+ Assert.That(exceptions, Is.Empty);
+
+ // 验证所有事件都已注册
+ Assert.That(_easyEvents.GetEvent>(), Is.Not.Null);
+ Assert.That(_easyEvents.GetEvent>(), Is.Not.Null);
+ Assert.That(_easyEvents.GetEvent>(), Is.Not.Null);
+ Assert.That(_easyEvents.GetEvent>(), Is.Not.Null);
+ Assert.That(_easyEvents.GetEvent>(), Is.Not.Null);
+ }
}
\ No newline at end of file
diff --git a/GFramework.Core.Tests/property/BindablePropertyTests.cs b/GFramework.Core.Tests/property/BindablePropertyTests.cs
index fd360d9..f77ed82 100644
--- a/GFramework.Core.Tests/property/BindablePropertyTests.cs
+++ b/GFramework.Core.Tests/property/BindablePropertyTests.cs
@@ -183,4 +183,166 @@ public class BindablePropertyTests
Assert.That(result, Is.EqualTo("42"));
}
+
+ ///
+ /// 测试并发场景下的属性值设置
+ ///
+ [Test]
+ public void Concurrent_Value_Set_Should_Be_Thread_Safe()
+ {
+ var property = new BindableProperty(0);
+ const int threadCount = 10;
+ const int iterationsPerThread = 100;
+ var tasks = new Task[threadCount];
+ var exceptions = new List();
+
+ for (var i = 0; i < threadCount; i++)
+ {
+ var threadId = i;
+ tasks[i] = Task.Run(() =>
+ {
+ try
+ {
+ for (var j = 0; j < iterationsPerThread; j++)
+ {
+ property.Value = threadId * iterationsPerThread + j;
+ }
+ }
+ catch (Exception ex)
+ {
+ lock (exceptions)
+ {
+ exceptions.Add(ex);
+ }
+ }
+ });
+ }
+
+ Task.WaitAll(tasks);
+
+ Assert.That(exceptions, Is.Empty, $"并发测试中发生异常: {string.Join(", ", exceptions.Select(e => e.Message))}");
+ }
+
+ ///
+ /// 测试并发场景下的事件注册和触发
+ ///
+ [Test]
+ public void Concurrent_Register_And_Trigger_Should_Be_Thread_Safe()
+ {
+ var property = new BindableProperty(0);
+ const int threadCount = 5;
+ var tasks = new Task[threadCount];
+ var exceptions = new List();
+ var callCounts = new int[threadCount];
+
+ for (var i = 0; i < threadCount; i++)
+ {
+ var threadId = i;
+ tasks[i] = Task.Run(() =>
+ {
+ try
+ {
+ property.Register(value => { Interlocked.Increment(ref callCounts[threadId]); });
+ }
+ catch (Exception ex)
+ {
+ lock (exceptions)
+ {
+ exceptions.Add(ex);
+ }
+ }
+ });
+ }
+
+ Task.WaitAll(tasks);
+
+ // 触发事件
+ property.Value = 42;
+
+ Assert.That(exceptions, Is.Empty);
+ Assert.That(callCounts.Sum(), Is.EqualTo(threadCount), "所有注册的处理器都应该被调用");
+ }
+
+ ///
+ /// 测试并发场景下的注册和取消注册
+ ///
+ [Test]
+ public void Concurrent_Register_And_UnRegister_Should_Be_Thread_Safe()
+ {
+ var property = new BindableProperty(0);
+ const int threadCount = 10;
+ const int iterationsPerThread = 50;
+ var tasks = new Task[threadCount];
+ var exceptions = new List();
+
+ for (var i = 0; i < threadCount; i++)
+ {
+ tasks[i] = Task.Run(() =>
+ {
+ try
+ {
+ for (var j = 0; j < iterationsPerThread; j++)
+ {
+ Action handler = _ => { };
+ property.Register(handler);
+ property.UnRegister(handler);
+ }
+ }
+ catch (Exception ex)
+ {
+ lock (exceptions)
+ {
+ exceptions.Add(ex);
+ }
+ }
+ });
+ }
+
+ Task.WaitAll(tasks);
+
+ Assert.That(exceptions, Is.Empty);
+ }
+
+ ///
+ /// 测试并发场景下RegisterWithInitValue的线程安全性
+ ///
+ [Test]
+ public void Concurrent_RegisterWithInitValue_Should_Be_Thread_Safe()
+ {
+ var property = new BindableProperty(42);
+ const int threadCount = 10;
+ var tasks = new Task[threadCount];
+ var exceptions = new List();
+ var receivedValues = new List();
+
+ for (var i = 0; i < threadCount; i++)
+ {
+ tasks[i] = Task.Run(() =>
+ {
+ try
+ {
+ property.RegisterWithInitValue(value =>
+ {
+ lock (receivedValues)
+ {
+ receivedValues.Add(value);
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ lock (exceptions)
+ {
+ exceptions.Add(ex);
+ }
+ }
+ });
+ }
+
+ Task.WaitAll(tasks);
+
+ Assert.That(exceptions, Is.Empty);
+ Assert.That(receivedValues.Count, Is.EqualTo(threadCount));
+ Assert.That(receivedValues.All(v => v == 42), Is.True, "所有初始值都应该是42");
+ }
}
\ No newline at end of file
diff --git a/GFramework.Core/coroutine/CoroutineScheduler.cs b/GFramework.Core/coroutine/CoroutineScheduler.cs
index 83b4b9c..18e5c2a 100644
--- a/GFramework.Core/coroutine/CoroutineScheduler.cs
+++ b/GFramework.Core/coroutine/CoroutineScheduler.cs
@@ -5,6 +5,7 @@ namespace GFramework.Core.coroutine;
///
/// 协程调度器,用于管理和执行协程
+/// 线程安全说明:此类设计为单线程使用,所有方法应在同一线程中调用
///
/// 时间源接口,提供时间相关数据
/// 实例ID,默认为1
@@ -32,6 +33,11 @@ public sealed class CoroutineScheduler(
///
public int ActiveCoroutineCount { get; private set; }
+ ///
+ /// 协程异常处理回调,当协程执行过程中发生异常时触发
+ ///
+ public event Action? OnCoroutineException;
+
///
/// 检查指定的协程句柄是否仍然存活
///
@@ -377,7 +383,24 @@ public sealed class CoroutineScheduler(
/// 异常对象
private void OnError(int slotIndex, Exception ex)
{
- Console.Error.WriteLine(ex);
+ var slot = _slots[slotIndex];
+ var handle = slot?.Handle ?? default;
+
+ try
+ {
+ // 触发异常回调
+ OnCoroutineException?.Invoke(handle, ex);
+ }
+ catch (Exception callbackEx)
+ {
+ // 防止回调异常导致调度器崩溃
+ Console.Error.WriteLine($"[CoroutineScheduler] Exception in error callback: {callbackEx}");
+ }
+
+ // 输出到控制台作为后备
+ Console.Error.WriteLine($"[CoroutineScheduler] Coroutine {handle} failed with exception: {ex}");
+
+ // 完成协程
Complete(slotIndex);
}
diff --git a/GFramework.Core/events/EasyEvents.cs b/GFramework.Core/events/EasyEvents.cs
index b4d729f..9f96b71 100644
--- a/GFramework.Core/events/EasyEvents.cs
+++ b/GFramework.Core/events/EasyEvents.cs
@@ -1,3 +1,4 @@
+using System.Collections.Concurrent;
using GFramework.Core.Abstractions.events;
namespace GFramework.Core.events;
@@ -5,6 +6,7 @@ namespace GFramework.Core.events;
///
/// EasyEvents事件管理器类,用于全局事件的注册、获取和管理
/// 提供了类型安全的事件系统,支持泛型事件的自动创建和检索
+/// 线程安全:所有公共方法都是线程安全的
///
public class EasyEvents
{
@@ -14,9 +16,9 @@ public class EasyEvents
private static readonly EasyEvents MGlobalEvents = new();
///
- /// 存储事件类型与事件实例映射关系的字典
+ /// 存储事件类型与事件实例映射关系的字典(线程安全)
///
- private readonly Dictionary _mTypeEvents = new();
+ private readonly ConcurrentDictionary _mTypeEvents = new();
///
/// 获取指定类型的全局事件实例
@@ -51,9 +53,13 @@ public class EasyEvents
/// 添加指定类型的事件到事件字典中
///
/// 事件类型,必须实现IEasyEvent接口且具有无参构造函数
+ /// 当事件类型已存在时抛出
public void AddEvent() where T : IEvent, new()
{
- _mTypeEvents.Add(typeof(T), new T());
+ if (!_mTypeEvents.TryAdd(typeof(T), new T()))
+ {
+ throw new ArgumentException($"Event type {typeof(T).Name} already registered.");
+ }
}
///
@@ -73,13 +79,6 @@ public class EasyEvents
/// 指定类型的事件实例
public T GetOrAddEvent() where T : IEvent, new()
{
- var eType = typeof(T);
- // 尝试从字典中获取事件实例
- if (_mTypeEvents.TryGetValue(eType, out var e)) return (T)e;
-
- // 如果不存在则创建新实例并添加到字典中
- var t = new T();
- _mTypeEvents.Add(eType, t);
- return t;
+ return (T)_mTypeEvents.GetOrAdd(typeof(T), _ => new T());
}
}
\ No newline at end of file
diff --git a/GFramework.Core/logging/appenders/AsyncLogAppender.cs b/GFramework.Core/logging/appenders/AsyncLogAppender.cs
index 1c864ee..fa35d73 100644
--- a/GFramework.Core/logging/appenders/AsyncLogAppender.cs
+++ b/GFramework.Core/logging/appenders/AsyncLogAppender.cs
@@ -90,20 +90,40 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
_channel.Writer.TryWrite(entry);
}
+ ///
+ /// 刷新缓冲区(ILogAppender 接口实现)
+ ///
+ void ILogAppender.Flush()
+ {
+ Flush();
+ }
+
///
/// 刷新缓冲区,等待所有日志写入完成
///
- public void Flush()
+ /// 超时时间(默认30秒)
+ /// 是否成功刷新所有日志
+ public bool Flush(TimeSpan? timeout = null)
{
- if (_disposed) return;
+ if (_disposed) return false;
- // 等待 Channel 中的所有消息被处理
+ var actualTimeout = timeout ?? TimeSpan.FromSeconds(30);
+ var startTime = DateTime.UtcNow;
+
+ // 使用 SpinWait 替代忙轮询,更高效
+ var spinWait = new SpinWait();
while (_channel.Reader.Count > 0)
{
- Thread.Sleep(10);
+ if (DateTime.UtcNow - startTime > actualTimeout)
+ {
+ return false; // 超时
+ }
+
+ spinWait.SpinOnce();
}
_innerAppender.Flush();
+ return true;
}
///
diff --git a/GFramework.Core/logging/appenders/FileAppender.cs b/GFramework.Core/logging/appenders/FileAppender.cs
index a92ea25..4818d8b 100644
--- a/GFramework.Core/logging/appenders/FileAppender.cs
+++ b/GFramework.Core/logging/appenders/FileAppender.cs
@@ -23,6 +23,8 @@ public sealed class FileAppender : ILogAppender, IDisposable
/// 日志文件路径
/// 日志格式化器
/// 日志过滤器(可选)
+ /// 当文件路径为空或无效时抛出
+ /// 当无法创建或打开日志文件时抛出
public FileAppender(
string filePath,
ILogFormatter? formatter = null,
@@ -35,8 +37,18 @@ public sealed class FileAppender : ILogAppender, IDisposable
_formatter = formatter ?? new DefaultLogFormatter();
_filter = filter;
- EnsureDirectoryExists();
- InitializeWriter();
+ try
+ {
+ EnsureDirectoryExists();
+ InitializeWriter();
+ }
+ catch
+ {
+ // 确保在初始化失败时清理资源
+ _writer?.Dispose();
+ _writer = null;
+ throw;
+ }
}
///
diff --git a/GFramework.Core/property/BindableProperty.cs b/GFramework.Core/property/BindableProperty.cs
index 624160f..7034e72 100644
--- a/GFramework.Core/property/BindableProperty.cs
+++ b/GFramework.Core/property/BindableProperty.cs
@@ -5,16 +5,22 @@ namespace GFramework.Core.property;
///
/// 可绑定属性类,用于实现数据绑定功能
+/// 线程安全:所有公共方法都是线程安全的
///
/// 属性值的类型
/// 属性的默认值
public class BindableProperty(T defaultValue = default!) : IBindableProperty
{
+ ///
+ /// 用于保护委托链和值访问的锁对象
+ ///
+ private readonly object _lock = new();
+
///
/// 属性值变化事件回调委托,当属性值发生变化时被调用
///
private Action? _mOnValueChanged;
-
+
///
/// 存储属性实际值的受保护字段
///
@@ -33,17 +39,25 @@ public class BindableProperty(T defaultValue = default!) : IBindableProperty<
get => GetValue();
set
{
- // 使用 default(T) 替代 null 比较,避免 SonarQube 警告
- if (EqualityComparer.Default.Equals(value, default!) &&
- EqualityComparer.Default.Equals(MValue, default!))
- return;
+ Action? callback = null;
- // 若新值与旧值相等则不执行后续操作
- if (!EqualityComparer.Default.Equals(value, default!) && Comparer(value, MValue))
- return;
+ lock (_lock)
+ {
+ // 使用 default(T) 替代 null 比较,避免 SonarQube 警告
+ if (EqualityComparer.Default.Equals(value, default!) &&
+ EqualityComparer.Default.Equals(MValue, default!))
+ return;
- SetValue(value);
- _mOnValueChanged?.Invoke(value);
+ // 若新值与旧值相等则不执行后续操作
+ if (!EqualityComparer.Default.Equals(value, default!) && Comparer(value, MValue))
+ return;
+
+ SetValue(value);
+ callback = _mOnValueChanged;
+ }
+
+ // 在锁外调用回调,避免死锁
+ callback?.Invoke(value);
}
}
@@ -78,7 +92,11 @@ public class BindableProperty(T defaultValue = default!) : IBindableProperty<
/// 可用于取消注册的接口
public IUnRegister Register(Action onValueChanged)
{
- _mOnValueChanged += onValueChanged;
+ lock (_lock)
+ {
+ _mOnValueChanged += onValueChanged;
+ }
+
return new BindablePropertyUnRegister(this, onValueChanged);
}
@@ -89,7 +107,13 @@ public class BindableProperty(T defaultValue = default!) : IBindableProperty<
/// 可用于取消注册的接口
public IUnRegister RegisterWithInitValue(Action action)
{
- action(MValue);
+ T currentValue;
+ lock (_lock)
+ {
+ currentValue = MValue;
+ }
+
+ action(currentValue);
return Register(action);
}
@@ -99,7 +123,10 @@ public class BindableProperty(T defaultValue = default!) : IBindableProperty<
/// 要取消注册的回调函数
public void UnRegister(Action onValueChanged)
{
- _mOnValueChanged -= onValueChanged;
+ lock (_lock)
+ {
+ _mOnValueChanged -= onValueChanged;
+ }
}
///