Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 110 additions & 2 deletions GFramework.Core.Tests/events/EasyEventsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public void SetUp()
private EasyEvents _easyEvents = null!;

/// <summary>
/// 测试单参数事件的功能验证事件能够正确接收并传递int类型参数
/// 测试单参数事件的功能,验证事件能够正确接收并传递int类型参数
/// </summary>
[Test]
public void Get_EventT_Should_Trigger_With_Parameter()
Expand All @@ -38,7 +38,7 @@ public void Get_EventT_Should_Trigger_With_Parameter()
}

/// <summary>
/// 测试双参数事件的功能验证事件能够正确接收并传递int和string类型的参数
/// 测试双参数事件的功能,验证事件能够正确接收并传递int和string类型的参数
/// </summary>
[Test]
public void Get_EventTTK_Should_Trigger_With_Two_Parameters()
Expand All @@ -59,4 +59,112 @@ public void Get_EventTTK_Should_Trigger_With_Two_Parameters()
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);
}
Comment on lines +88 to +91

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock held in an improper manner


Trying to acquire a lock on a local variable is error-prone as different threads may end up locking on different instances of the same variable. Instead, you should be locking on a class field that is preferably designated as private and readonly, i.e. a dedicated object. Microsoft guidelines explicitly state that you should avoid locking on:

}
});
}

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switch expression does not have `default` case


The default case is executed when none of the specified cases in the switch statement match. This is particularly useful when switch fails to cover all the possible values, either because all the cases weren't specified or that the range of values was later expanded.

{
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);
}
Comment on lines +151 to +154

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock held in an improper manner


Trying to acquire a lock on a local variable is error-prone as different threads may end up locking on different instances of the same variable. Instead, you should be locking on a class field that is preferably designated as private and readonly, i.e. a dedicated object. Microsoft guidelines explicitly state that you should avoid locking on:

}
});
}

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);
}
}
162 changes: 162 additions & 0 deletions GFramework.Core.Tests/property/BindablePropertyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,166 @@ public void ToString_Should_Return_Value_As_String()

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);
}
Comment on lines +213 to +216

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock held in an improper manner


Trying to acquire a lock on a local variable is error-prone as different threads may end up locking on different instances of the same variable. Instead, you should be locking on a class field that is preferably designated as private and readonly, i.e. a dedicated object. Microsoft guidelines explicitly state that you should avoid locking on:

}
});
}

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);
}
Comment on lines +249 to +252

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock held in an improper manner


Trying to acquire a lock on a local variable is error-prone as different threads may end up locking on different instances of the same variable. Instead, you should be locking on a class field that is preferably designated as private and readonly, i.e. a dedicated object. Microsoft guidelines explicitly state that you should avoid locking on:

}
});
}

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);
}
Comment on lines +293 to +296

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock held in an improper manner


Trying to acquire a lock on a local variable is error-prone as different threads may end up locking on different instances of the same variable. Instead, you should be locking on a class field that is preferably designated as private and readonly, i.e. a dedicated object. Microsoft guidelines explicitly state that you should avoid locking on:

}
});
}

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);
}
Comment on lines +326 to +329

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock held in an improper manner


Trying to acquire a lock on a local variable is error-prone as different threads may end up locking on different instances of the same variable. Instead, you should be locking on a class field that is preferably designated as private and readonly, i.e. a dedicated object. Microsoft guidelines explicitly state that you should avoid locking on:

});
}
catch (Exception ex)
{
lock (exceptions)
{
exceptions.Add(ex);
}
Comment on lines +334 to +337

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock held in an improper manner


Trying to acquire a lock on a local variable is error-prone as different threads may end up locking on different instances of the same variable. Instead, you should be locking on a class field that is preferably designated as private and readonly, i.e. a dedicated object. Microsoft guidelines explicitly state that you should avoid locking on:

}
});
}

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");
}
}
33 changes: 32 additions & 1 deletion GFramework.Core/coroutine/CoroutineScheduler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace GFramework.Core.coroutine;

/// <summary>
/// 协程调度器,用于管理和执行协程
/// 线程安全说明:此类设计为单线程使用,所有方法应在同一线程中调用
/// </summary>
/// <param name="timeSource">时间源接口,提供时间相关数据</param>
/// <param name="instanceId">实例ID,默认为1</param>
Expand Down Expand Up @@ -32,6 +33,12 @@ public sealed class CoroutineScheduler(
/// </summary>
public int ActiveCoroutineCount { get; private set; }

/// <summary>
/// 协程异常处理回调,当协程执行过程中发生异常时触发
/// 注意:事件处理程序会在独立任务中异步调用,以避免阻塞调度器主循环
/// </summary>
public event Action<CoroutineHandle, Exception>? OnCoroutineException;

/// <summary>
/// 检查指定的协程句柄是否仍然存活
/// </summary>
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -377,7 +384,31 @@ private void Complete(int slotIndex)
/// <param name="ex">异常对象</param>
private void OnError(int slotIndex, Exception ex)
{
Console.Error.WriteLine(ex);
var slot = _slots[slotIndex];
var handle = slot?.Handle ?? default;

// 将异常回调派发到线程池,避免阻塞调度器主循环
var handler = OnCoroutineException;
if (handler != null)
{
Task.Run(() =>
{
try
{
handler(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);
}

Expand Down
Loading
Loading