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; + } } ///