mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
refactor(core): 优化核心组件的线程安全性和错误处理
- 重构 AsyncLogAppender 的 Flush 方法,添加超时控制和 SpinWait 优化 - 为 BindableProperty 添加线程安全锁保护,确保并发访问的安全性 - 在 BindableProperty 中实现回调外部调用以避免死锁问题 - 为 EasyEvents 使用 ConcurrentDictionary 替代 Dictionary 提高并发性能 - 添加协程调度器异常处理回调机制,防止异常传播导致调度器崩溃 - 为 FileAppender 添加初始化异常处理和资源清理逻辑 - 补充完整的单元测试覆盖并发场景下的线程安全性验证
This commit is contained in:
parent
b417ece73a
commit
f984f4a600
@ -21,7 +21,7 @@ public class EasyEventsTests
|
||||
private EasyEvents _easyEvents = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 测试单参数事件的功能,验证事件能够正确接收并传递int类型参数
|
||||
/// 测试单参数事件的功能,验证事件能够正确接收并传递int类型参数
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Get_EventT_Should_Trigger_With_Parameter()
|
||||
@ -38,7 +38,7 @@ public class EasyEventsTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试双参数事件的功能,验证事件能够正确接收并传递int和string类型的参数
|
||||
/// 测试双参数事件的功能,验证事件能够正确接收并传递int和string类型的参数
|
||||
/// </summary>
|
||||
[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"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试并发场景下GetOrAdd的线程安全性
|
||||
/// </summary>
|
||||
[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<Exception>();
|
||||
|
||||
for (var i = 0; i < threadCount; i++)
|
||||
{
|
||||
tasks[i] = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
for (var j = 0; j < iterationsPerThread; j++)
|
||||
{
|
||||
var @event = _easyEvents.GetOrAddEvent<Event<int>>();
|
||||
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))}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试并发场景下AddEvent的行为
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void AddEvent_Should_Throw_When_Already_Registered()
|
||||
{
|
||||
_easyEvents.AddEvent<Event<int>>();
|
||||
|
||||
Assert.Throws<ArgumentException>(() => _easyEvents.AddEvent<Event<int>>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试并发场景下多个不同事件类型的注册
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Concurrent_Registration_Of_Different_Event_Types_Should_Work()
|
||||
{
|
||||
const int threadCount = 5;
|
||||
var tasks = new Task[threadCount];
|
||||
var exceptions = new List<Exception>();
|
||||
|
||||
// 每个线程注册不同类型的事件
|
||||
for (var i = 0; i < threadCount; i++)
|
||||
{
|
||||
var index = i;
|
||||
tasks[i] = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
_easyEvents.GetOrAddEvent<Event<int>>();
|
||||
break;
|
||||
case 1:
|
||||
_easyEvents.GetOrAddEvent<Event<string>>();
|
||||
break;
|
||||
case 2:
|
||||
_easyEvents.GetOrAddEvent<Event<bool>>();
|
||||
break;
|
||||
case 3:
|
||||
_easyEvents.GetOrAddEvent<Event<int, string>>();
|
||||
break;
|
||||
case 4:
|
||||
_easyEvents.GetOrAddEvent<Event<double>>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lock (exceptions)
|
||||
{
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Task.WaitAll(tasks);
|
||||
|
||||
Assert.That(exceptions, Is.Empty);
|
||||
|
||||
// 验证所有事件都已注册
|
||||
Assert.That(_easyEvents.GetEvent<Event<int>>(), Is.Not.Null);
|
||||
Assert.That(_easyEvents.GetEvent<Event<string>>(), Is.Not.Null);
|
||||
Assert.That(_easyEvents.GetEvent<Event<bool>>(), Is.Not.Null);
|
||||
Assert.That(_easyEvents.GetEvent<Event<int, string>>(), Is.Not.Null);
|
||||
Assert.That(_easyEvents.GetEvent<Event<double>>(), Is.Not.Null);
|
||||
}
|
||||
}
|
||||
@ -183,4 +183,166 @@ public class BindablePropertyTests
|
||||
|
||||
Assert.That(result, Is.EqualTo("42"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试并发场景下的属性值设置
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Concurrent_Value_Set_Should_Be_Thread_Safe()
|
||||
{
|
||||
var property = new BindableProperty<int>(0);
|
||||
const int threadCount = 10;
|
||||
const int iterationsPerThread = 100;
|
||||
var tasks = new Task[threadCount];
|
||||
var exceptions = new List<Exception>();
|
||||
|
||||
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))}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试并发场景下的事件注册和触发
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Concurrent_Register_And_Trigger_Should_Be_Thread_Safe()
|
||||
{
|
||||
var property = new BindableProperty<int>(0);
|
||||
const int threadCount = 5;
|
||||
var tasks = new Task[threadCount];
|
||||
var exceptions = new List<Exception>();
|
||||
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), "所有注册的处理器都应该被调用");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试并发场景下的注册和取消注册
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Concurrent_Register_And_UnRegister_Should_Be_Thread_Safe()
|
||||
{
|
||||
var property = new BindableProperty<int>(0);
|
||||
const int threadCount = 10;
|
||||
const int iterationsPerThread = 50;
|
||||
var tasks = new Task[threadCount];
|
||||
var exceptions = new List<Exception>();
|
||||
|
||||
for (var i = 0; i < threadCount; i++)
|
||||
{
|
||||
tasks[i] = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
for (var j = 0; j < iterationsPerThread; j++)
|
||||
{
|
||||
Action<int> handler = _ => { };
|
||||
property.Register(handler);
|
||||
property.UnRegister(handler);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lock (exceptions)
|
||||
{
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Task.WaitAll(tasks);
|
||||
|
||||
Assert.That(exceptions, Is.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试并发场景下RegisterWithInitValue的线程安全性
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Concurrent_RegisterWithInitValue_Should_Be_Thread_Safe()
|
||||
{
|
||||
var property = new BindableProperty<int>(42);
|
||||
const int threadCount = 10;
|
||||
var tasks = new Task[threadCount];
|
||||
var exceptions = new List<Exception>();
|
||||
var receivedValues = new List<int>();
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ namespace GFramework.Core.coroutine;
|
||||
|
||||
/// <summary>
|
||||
/// 协程调度器,用于管理和执行协程
|
||||
/// 线程安全说明:此类设计为单线程使用,所有方法应在同一线程中调用
|
||||
/// </summary>
|
||||
/// <param name="timeSource">时间源接口,提供时间相关数据</param>
|
||||
/// <param name="instanceId">实例ID,默认为1</param>
|
||||
@ -32,6 +33,11 @@ public sealed class CoroutineScheduler(
|
||||
/// </summary>
|
||||
public int ActiveCoroutineCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 协程异常处理回调,当协程执行过程中发生异常时触发
|
||||
/// </summary>
|
||||
public event Action<CoroutineHandle, Exception>? OnCoroutineException;
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定的协程句柄是否仍然存活
|
||||
/// </summary>
|
||||
@ -377,7 +383,24 @@ public sealed class CoroutineScheduler(
|
||||
/// <param name="ex">异常对象</param>
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
/// <summary>
|
||||
/// EasyEvents事件管理器类,用于全局事件的注册、获取和管理
|
||||
/// 提供了类型安全的事件系统,支持泛型事件的自动创建和检索
|
||||
/// 线程安全:所有公共方法都是线程安全的
|
||||
/// </summary>
|
||||
public class EasyEvents
|
||||
{
|
||||
@ -14,9 +16,9 @@ public class EasyEvents
|
||||
private static readonly EasyEvents MGlobalEvents = new();
|
||||
|
||||
/// <summary>
|
||||
/// 存储事件类型与事件实例映射关系的字典
|
||||
/// 存储事件类型与事件实例映射关系的字典(线程安全)
|
||||
/// </summary>
|
||||
private readonly Dictionary<Type, IEvent> _mTypeEvents = new();
|
||||
private readonly ConcurrentDictionary<Type, IEvent> _mTypeEvents = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定类型的全局事件实例
|
||||
@ -51,9 +53,13 @@ public class EasyEvents
|
||||
/// 添加指定类型的事件到事件字典中
|
||||
/// </summary>
|
||||
/// <typeparam name="T">事件类型,必须实现IEasyEvent接口且具有无参构造函数</typeparam>
|
||||
/// <exception cref="ArgumentException">当事件类型已存在时抛出</exception>
|
||||
public void AddEvent<T>() 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.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -73,13 +79,6 @@ public class EasyEvents
|
||||
/// <returns>指定类型的事件实例</returns>
|
||||
public T GetOrAddEvent<T>() 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());
|
||||
}
|
||||
}
|
||||
@ -90,20 +90,40 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
|
||||
_channel.Writer.TryWrite(entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新缓冲区(ILogAppender 接口实现)
|
||||
/// </summary>
|
||||
void ILogAppender.Flush()
|
||||
{
|
||||
Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新缓冲区,等待所有日志写入完成
|
||||
/// </summary>
|
||||
public void Flush()
|
||||
/// <param name="timeout">超时时间(默认30秒)</param>
|
||||
/// <returns>是否成功刷新所有日志</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -23,6 +23,8 @@ public sealed class FileAppender : ILogAppender, IDisposable
|
||||
/// <param name="filePath">日志文件路径</param>
|
||||
/// <param name="formatter">日志格式化器</param>
|
||||
/// <param name="filter">日志过滤器(可选)</param>
|
||||
/// <exception cref="ArgumentException">当文件路径为空或无效时抛出</exception>
|
||||
/// <exception cref="IOException">当无法创建或打开日志文件时抛出</exception>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -5,16 +5,22 @@ namespace GFramework.Core.property;
|
||||
|
||||
/// <summary>
|
||||
/// 可绑定属性类,用于实现数据绑定功能
|
||||
/// 线程安全:所有公共方法都是线程安全的
|
||||
/// </summary>
|
||||
/// <typeparam name="T">属性值的类型</typeparam>
|
||||
/// <param name="defaultValue">属性的默认值</param>
|
||||
public class BindableProperty<T>(T defaultValue = default!) : IBindableProperty<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于保护委托链和值访问的锁对象
|
||||
/// </summary>
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 属性值变化事件回调委托,当属性值发生变化时被调用
|
||||
/// </summary>
|
||||
private Action<T>? _mOnValueChanged;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 存储属性实际值的受保护字段
|
||||
/// </summary>
|
||||
@ -33,17 +39,25 @@ public class BindableProperty<T>(T defaultValue = default!) : IBindableProperty<
|
||||
get => GetValue();
|
||||
set
|
||||
{
|
||||
// 使用 default(T) 替代 null 比较,避免 SonarQube 警告
|
||||
if (EqualityComparer<T>.Default.Equals(value, default!) &&
|
||||
EqualityComparer<T>.Default.Equals(MValue, default!))
|
||||
return;
|
||||
Action<T>? callback = null;
|
||||
|
||||
// 若新值与旧值相等则不执行后续操作
|
||||
if (!EqualityComparer<T>.Default.Equals(value, default!) && Comparer(value, MValue))
|
||||
return;
|
||||
lock (_lock)
|
||||
{
|
||||
// 使用 default(T) 替代 null 比较,避免 SonarQube 警告
|
||||
if (EqualityComparer<T>.Default.Equals(value, default!) &&
|
||||
EqualityComparer<T>.Default.Equals(MValue, default!))
|
||||
return;
|
||||
|
||||
SetValue(value);
|
||||
_mOnValueChanged?.Invoke(value);
|
||||
// 若新值与旧值相等则不执行后续操作
|
||||
if (!EqualityComparer<T>.Default.Equals(value, default!) && Comparer(value, MValue))
|
||||
return;
|
||||
|
||||
SetValue(value);
|
||||
callback = _mOnValueChanged;
|
||||
}
|
||||
|
||||
// 在锁外调用回调,避免死锁
|
||||
callback?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,7 +92,11 @@ public class BindableProperty<T>(T defaultValue = default!) : IBindableProperty<
|
||||
/// <returns>可用于取消注册的接口</returns>
|
||||
public IUnRegister Register(Action<T> onValueChanged)
|
||||
{
|
||||
_mOnValueChanged += onValueChanged;
|
||||
lock (_lock)
|
||||
{
|
||||
_mOnValueChanged += onValueChanged;
|
||||
}
|
||||
|
||||
return new BindablePropertyUnRegister<T>(this, onValueChanged);
|
||||
}
|
||||
|
||||
@ -89,7 +107,13 @@ public class BindableProperty<T>(T defaultValue = default!) : IBindableProperty<
|
||||
/// <returns>可用于取消注册的接口</returns>
|
||||
public IUnRegister RegisterWithInitValue(Action<T> action)
|
||||
{
|
||||
action(MValue);
|
||||
T currentValue;
|
||||
lock (_lock)
|
||||
{
|
||||
currentValue = MValue;
|
||||
}
|
||||
|
||||
action(currentValue);
|
||||
return Register(action);
|
||||
}
|
||||
|
||||
@ -99,7 +123,10 @@ public class BindableProperty<T>(T defaultValue = default!) : IBindableProperty<
|
||||
/// <param name="onValueChanged">要取消注册的回调函数</param>
|
||||
public void UnRegister(Action<T> onValueChanged)
|
||||
{
|
||||
_mOnValueChanged -= onValueChanged;
|
||||
lock (_lock)
|
||||
{
|
||||
_mOnValueChanged -= onValueChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user