Merge pull request #82 from GeWuYou/feat/coroutine-enhancements

为调度器新增协程分组、优先级与统计能力,以及相应的抽象与测试。

New Features:

引入协程优先级等级,并在调度器中按优先级顺序执行协程。
添加协程分组支持,用于批量暂停、恢复、终止以及计数操作。
提供协程统计接口及实现,用于跟踪数量、执行时间指标以及按优先级/标签划分的数据,并可在调度器上按需启用。
暴露一个可控的 FakeTimeSource 实现,用于驱动可确定性的协程测试。
Enhancements:

扩展协程元数据和槽位以存储优先级、分组信息以及开始时间戳,并相应更新调度器生命周期处理逻辑。
Tests:

添加单元测试,覆盖协程统计行为与报告、分组管理操作以及基于优先级的执行顺序。
This commit is contained in:
gewuyou 2026-03-06 13:13:54 +08:00 committed by GitHub
commit 1b92bda8ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1242 additions and 10 deletions

View File

@ -0,0 +1,33 @@
namespace GFramework.Core.Abstractions.coroutine;
/// <summary>
/// 协程优先级枚举
/// 定义协程的执行优先级,高优先级的协程会优先执行
/// </summary>
public enum CoroutinePriority
{
/// <summary>
/// 最低优先级
/// </summary>
Lowest = 0,
/// <summary>
/// 低优先级
/// </summary>
Low = 1,
/// <summary>
/// 普通优先级(默认)
/// </summary>
Normal = 2,
/// <summary>
/// 高优先级
/// </summary>
High = 3,
/// <summary>
/// 最高优先级
/// </summary>
Highest = 4
}

View File

@ -0,0 +1,68 @@
namespace GFramework.Core.Abstractions.coroutine;
/// <summary>
/// 协程统计信息接口
/// 提供协程执行的性能统计数据
/// </summary>
public interface ICoroutineStatistics
{
/// <summary>
/// 获取总协程启动数量
/// </summary>
long TotalStarted { get; }
/// <summary>
/// 获取总协程完成数量
/// </summary>
long TotalCompleted { get; }
/// <summary>
/// 获取总协程失败数量
/// </summary>
long TotalFailed { get; }
/// <summary>
/// 获取当前活跃协程数量
/// </summary>
int ActiveCount { get; }
/// <summary>
/// 获取当前暂停协程数量
/// </summary>
int PausedCount { get; }
/// <summary>
/// 获取协程平均执行时间(毫秒)
/// </summary>
double AverageExecutionTimeMs { get; }
/// <summary>
/// 获取协程最大执行时间(毫秒)
/// </summary>
double MaxExecutionTimeMs { get; }
/// <summary>
/// 获取按优先级分组的协程数量
/// </summary>
/// <param name="priority">协程优先级</param>
/// <returns>指定优先级的协程数量</returns>
int GetCountByPriority(CoroutinePriority priority);
/// <summary>
/// 获取按标签分组的协程数量
/// </summary>
/// <param name="tag">协程标签</param>
/// <returns>指定标签的协程数量</returns>
int GetCountByTag(string tag);
/// <summary>
/// 重置统计数据
/// </summary>
void Reset();
/// <summary>
/// 生成统计报告
/// </summary>
/// <returns>格式化的统计报告字符串</returns>
string GenerateReport();
}

View File

@ -15,4 +15,6 @@ global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
global using System.Threading.Tasks;
global using NUnit.Framework;
global using NUnit.Compatibility;

View File

@ -0,0 +1,197 @@
using GFramework.Core.Abstractions.coroutine;
using GFramework.Core.coroutine;
using GFramework.Core.coroutine.instructions;
namespace GFramework.Core.Tests.coroutine;
/// <summary>
/// 协程分组管理测试
/// </summary>
public sealed class CoroutineGroupTests
{
[Test]
public void Run_WithGroup_ShouldAddToGroup()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
IEnumerator<IYieldInstruction> TestCoroutine()
{
yield return new WaitOneFrame();
}
// Act
scheduler.Run(TestCoroutine(), group: "TestGroup");
scheduler.Run(TestCoroutine(), group: "TestGroup");
scheduler.Run(TestCoroutine(), group: "OtherGroup");
// Assert
Assert.That(scheduler.GetGroupCount("TestGroup"), Is.EqualTo(2));
Assert.That(scheduler.GetGroupCount("OtherGroup"), Is.EqualTo(1));
}
[Test]
public void PauseGroup_ShouldPauseAllCoroutinesInGroup()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
var executionCount = 0;
IEnumerator<IYieldInstruction> TestCoroutine()
{
while (true)
{
executionCount++;
yield return new WaitOneFrame();
}
}
// Act
scheduler.Run(TestCoroutine(), group: "TestGroup");
scheduler.Run(TestCoroutine(), group: "TestGroup");
scheduler.Update(); // 第一次更新,执行计数应为 4每个协程执行2次
var pausedCount = scheduler.PauseGroup("TestGroup");
scheduler.Update(); // 暂停后更新,执行计数不应增加
// Assert
Assert.That(pausedCount, Is.EqualTo(2));
Assert.That(executionCount, Is.EqualTo(4)); // 第一次更新时每个协程执行了2次
}
[Test]
public void ResumeGroup_ShouldResumeAllCoroutinesInGroup()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
var executionCount = 0;
IEnumerator<IYieldInstruction> TestCoroutine()
{
while (true)
{
executionCount++;
yield return new WaitOneFrame();
}
}
// Act
scheduler.Run(TestCoroutine(), group: "TestGroup");
scheduler.Run(TestCoroutine(), group: "TestGroup");
scheduler.Update(); // 第一次更新
scheduler.PauseGroup("TestGroup");
scheduler.Update(); // 暂停期间
var resumedCount = scheduler.ResumeGroup("TestGroup");
scheduler.Update(); // 恢复后更新
// Assert
Assert.That(resumedCount, Is.EqualTo(2));
Assert.That(executionCount, Is.EqualTo(6)); // 第一次 4恢复后 2
}
[Test]
public void KillGroup_ShouldKillAllCoroutinesInGroup()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
IEnumerator<IYieldInstruction> TestCoroutine()
{
yield return new WaitOneFrame();
}
// Act
scheduler.Run(TestCoroutine(), group: "TestGroup");
scheduler.Run(TestCoroutine(), group: "TestGroup");
scheduler.Run(TestCoroutine(), group: "OtherGroup");
var initialCount = scheduler.ActiveCoroutineCount;
var killedCount = scheduler.KillGroup("TestGroup");
// Assert
Assert.That(initialCount, Is.EqualTo(3));
Assert.That(killedCount, Is.EqualTo(2));
Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(1));
Assert.That(scheduler.GetGroupCount("TestGroup"), Is.EqualTo(0));
}
[Test]
public void GetGroupCount_WithNonExistentGroup_ShouldReturnZero()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
// Act & Assert
Assert.That(scheduler.GetGroupCount("NonExistent"), Is.EqualTo(0));
}
[Test]
public void Complete_ShouldRemoveFromGroup()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
IEnumerator<IYieldInstruction> TestCoroutine()
{
yield return new WaitOneFrame(); // 等待一帧后完成
}
// Act
scheduler.Run(TestCoroutine(), group: "TestGroup");
scheduler.Run(TestCoroutine(), group: "TestGroup");
Assert.That(scheduler.GetGroupCount("TestGroup"), Is.EqualTo(2));
scheduler.Update(); // 协程完成
// Assert
Assert.That(scheduler.GetGroupCount("TestGroup"), Is.EqualTo(0));
}
[Test]
public void PauseGroup_WithMixedGroups_ShouldOnlyAffectTargetGroup()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
var group1Count = 0;
var group2Count = 0;
IEnumerator<IYieldInstruction> Group1Coroutine()
{
while (true)
{
group1Count++;
yield return new WaitOneFrame();
}
}
IEnumerator<IYieldInstruction> Group2Coroutine()
{
while (true)
{
group2Count++;
yield return new WaitOneFrame();
}
}
// Act
scheduler.Run(Group1Coroutine(), group: "Group1");
scheduler.Run(Group2Coroutine(), group: "Group2");
scheduler.Update(); // 第一次更新
scheduler.PauseGroup("Group1");
scheduler.Update(); // Group1 暂停Group2 继续
// Assert
Assert.That(group1Count, Is.EqualTo(2)); // Group1 执行了2次第一次更新
Assert.That(group2Count, Is.EqualTo(3)); // Group2 执行了3次第一次更新2次第二次更新1次
}
}

View File

@ -0,0 +1,133 @@
using GFramework.Core.Abstractions.coroutine;
using GFramework.Core.coroutine;
using GFramework.Core.coroutine.instructions;
namespace GFramework.Core.Tests.coroutine;
/// <summary>
/// 协程优先级测试
/// </summary>
public sealed class CoroutinePriorityTests
{
[Test]
public void Run_WithPriority_ShouldExecuteInPriorityOrder()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
var executionOrder = new List<string>();
IEnumerator<IYieldInstruction> CreateCoroutine(string name)
{
yield return new WaitOneFrame(); // 先等待一帧
executionOrder.Add(name); // 在第一次 Update 时才记录
}
// Act - 以不同优先级启动协程
scheduler.Run(CreateCoroutine("Low"), priority: CoroutinePriority.Low);
scheduler.Run(CreateCoroutine("High"), priority: CoroutinePriority.High);
scheduler.Run(CreateCoroutine("Normal"), priority: CoroutinePriority.Normal);
scheduler.Run(CreateCoroutine("Highest"), priority: CoroutinePriority.Highest);
scheduler.Run(CreateCoroutine("Lowest"), priority: CoroutinePriority.Lowest);
scheduler.Update();
// Assert - 高优先级应该先执行
Assert.That(executionOrder[0], Is.EqualTo("Highest"));
Assert.That(executionOrder[1], Is.EqualTo("High"));
Assert.That(executionOrder[2], Is.EqualTo("Normal"));
Assert.That(executionOrder[3], Is.EqualTo("Low"));
Assert.That(executionOrder[4], Is.EqualTo("Lowest"));
}
[Test]
public void Run_WithSamePriority_ShouldExecuteInStartOrder()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
var executionOrder = new List<string>();
IEnumerator<IYieldInstruction> CreateCoroutine(string name)
{
executionOrder.Add(name);
yield return new WaitOneFrame();
}
// Act - 以相同优先级启动协程
scheduler.Run(CreateCoroutine("First"), priority: CoroutinePriority.Normal);
scheduler.Run(CreateCoroutine("Second"), priority: CoroutinePriority.Normal);
scheduler.Run(CreateCoroutine("Third"), priority: CoroutinePriority.Normal);
scheduler.Update();
// Assert - 相同优先级按启动顺序执行
Assert.That(executionOrder[0], Is.EqualTo("First"));
Assert.That(executionOrder[1], Is.EqualTo("Second"));
Assert.That(executionOrder[2], Is.EqualTo("Third"));
}
[Test]
public void Run_WithDefaultPriority_ShouldUseNormal()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
var executed = false;
IEnumerator<IYieldInstruction> TestCoroutine()
{
executed = true;
yield return new WaitOneFrame();
}
// Act
scheduler.Run(TestCoroutine());
// Assert - 在 Update 之前检查统计
Assert.That(scheduler.Statistics!.GetCountByPriority(CoroutinePriority.Normal), Is.EqualTo(1));
scheduler.Update();
Assert.That(executed, Is.True);
}
[Test]
public void Update_WithMixedPriorities_ShouldRespectPriorityDuringExecution()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
var executionOrder = new List<string>();
IEnumerator<IYieldInstruction> CreateMultiStepCoroutine(string name)
{
yield return new WaitOneFrame(); // 先等待,避免在 Prewarm 时执行
for (var i = 0; i < 3; i++)
{
executionOrder.Add($"{name}-{i}");
yield return new Delay(0.02); // 等待稍长的时间,确保不会在同一帧完成
}
}
// Act
scheduler.Run(CreateMultiStepCoroutine("Low"), priority: CoroutinePriority.Low);
scheduler.Run(CreateMultiStepCoroutine("High"), priority: CoroutinePriority.High);
// 执行多帧,每帧推进 0.016 秒
// 第一帧WaitOneFrame 完成
// 之后每 2 帧0.032秒)完成一次 Delay(0.02)
for (var frame = 0; frame < 8; frame++)
{
timeSource.Advance(0.016);
scheduler.Update();
}
// Assert - 每帧都应该先执行高优先级
Assert.That(executionOrder[0], Is.EqualTo("High-0"));
Assert.That(executionOrder[1], Is.EqualTo("Low-0"));
Assert.That(executionOrder[2], Is.EqualTo("High-1"));
Assert.That(executionOrder[3], Is.EqualTo("Low-1"));
Assert.That(executionOrder[4], Is.EqualTo("High-2"));
Assert.That(executionOrder[5], Is.EqualTo("Low-2"));
}
}

View File

@ -0,0 +1,351 @@
using GFramework.Core.Abstractions.coroutine;
using GFramework.Core.coroutine;
using GFramework.Core.coroutine.instructions;
namespace GFramework.Core.Tests.coroutine;
/// <summary>
/// 协程统计功能测试
/// </summary>
public sealed class CoroutineStatisticsTests
{
[Test]
public void Statistics_WhenDisabled_ShouldBeNull()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: false);
// Act & Assert
Assert.That(scheduler.Statistics, Is.Null);
}
[Test]
public void Statistics_WhenEnabled_ShouldNotBeNull()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
// Act & Assert
Assert.That(scheduler.Statistics, Is.Not.Null);
}
[Test]
public void TotalStarted_ShouldTrackStartedCoroutines()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
IEnumerator<IYieldInstruction> TestCoroutine()
{
yield return new WaitOneFrame();
}
// Act
scheduler.Run(TestCoroutine());
scheduler.Run(TestCoroutine());
scheduler.Run(TestCoroutine());
// Assert
Assert.That(scheduler.Statistics!.TotalStarted, Is.EqualTo(3));
}
[Test]
public void TotalCompleted_ShouldTrackCompletedCoroutines()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
IEnumerator<IYieldInstruction> TestCoroutine()
{
yield break; // 立即完成
}
// Act
scheduler.Run(TestCoroutine());
scheduler.Run(TestCoroutine());
scheduler.Update();
// Assert
Assert.That(scheduler.Statistics!.TotalCompleted, Is.EqualTo(2));
}
[Test]
public void TotalFailed_ShouldTrackFailedCoroutines()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
IEnumerator<IYieldInstruction> FailingCoroutine()
{
throw new InvalidOperationException("Test exception");
#pragma warning disable CS0162 // 检测到无法访问的代码
yield break;
#pragma warning restore CS0162 // 检测到无法访问的代码
}
// Act
scheduler.Run(FailingCoroutine());
scheduler.Run(FailingCoroutine());
scheduler.Update();
// Assert
Assert.That(scheduler.Statistics!.TotalFailed, Is.EqualTo(2));
}
[Test]
public void ActiveCount_ShouldReflectCurrentActiveCoroutines()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
IEnumerator<IYieldInstruction> LongRunningCoroutine()
{
for (var i = 0; i < 10; i++)
yield return new WaitOneFrame();
}
// Act
scheduler.Run(LongRunningCoroutine());
scheduler.Run(LongRunningCoroutine());
scheduler.Update();
// Assert
Assert.That(scheduler.Statistics!.ActiveCount, Is.EqualTo(2));
}
[Test]
public void PausedCount_ShouldReflectCurrentPausedCoroutines()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
IEnumerator<IYieldInstruction> TestCoroutine()
{
yield return new WaitOneFrame();
}
// Act
var handle1 = scheduler.Run(TestCoroutine());
var handle2 = scheduler.Run(TestCoroutine());
scheduler.Pause(handle1);
scheduler.Pause(handle2);
scheduler.Update();
// Assert
Assert.That(scheduler.Statistics!.PausedCount, Is.EqualTo(2));
}
[Test]
public void AverageExecutionTimeMs_ShouldCalculateCorrectly()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
IEnumerator<IYieldInstruction> TestCoroutine()
{
yield return new WaitOneFrame();
yield return new WaitOneFrame();
}
// Act
scheduler.Run(TestCoroutine());
scheduler.Run(TestCoroutine());
// 执行两帧,每帧 16ms
timeSource.Advance(0.016);
scheduler.Update();
timeSource.Advance(0.016);
scheduler.Update();
// Assert
var avgTime = scheduler.Statistics!.AverageExecutionTimeMs;
Assert.That(avgTime > 0, Is.True);
Assert.That(avgTime <= 32, Is.True); // 最多 32ms
}
[Test]
public void MaxExecutionTimeMs_ShouldTrackLongestExecution()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
IEnumerator<IYieldInstruction> ShortCoroutine()
{
yield return new WaitOneFrame();
}
IEnumerator<IYieldInstruction> LongCoroutine()
{
yield return new WaitOneFrame();
yield return new WaitOneFrame();
yield return new WaitOneFrame();
}
// Act
scheduler.Run(ShortCoroutine());
scheduler.Run(LongCoroutine());
// 执行足够的帧
for (var i = 0; i < 4; i++)
{
timeSource.Advance(0.016);
scheduler.Update();
}
// Assert
var maxTime = scheduler.Statistics!.MaxExecutionTimeMs;
Assert.That(maxTime > 0, Is.True);
}
[Test]
public void GetCountByPriority_ShouldReturnCorrectCount()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
IEnumerator<IYieldInstruction> TestCoroutine()
{
yield return new WaitOneFrame();
}
// Act
scheduler.Run(TestCoroutine(), priority: CoroutinePriority.High);
scheduler.Run(TestCoroutine(), priority: CoroutinePriority.High);
scheduler.Run(TestCoroutine(), priority: CoroutinePriority.Low);
// Assert
Assert.That(scheduler.Statistics!.GetCountByPriority(CoroutinePriority.High), Is.EqualTo(2));
Assert.That(scheduler.Statistics!.GetCountByPriority(CoroutinePriority.Low), Is.EqualTo(1));
Assert.That(scheduler.Statistics!.GetCountByPriority(CoroutinePriority.Normal), Is.EqualTo(0));
}
[Test]
public void GetCountByTag_ShouldReturnCorrectCount()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
IEnumerator<IYieldInstruction> TestCoroutine()
{
yield return new WaitOneFrame();
}
// Act
scheduler.Run(TestCoroutine(), tag: "AI");
scheduler.Run(TestCoroutine(), tag: "AI");
scheduler.Run(TestCoroutine(), tag: "Physics");
// Assert
Assert.That(scheduler.Statistics!.GetCountByTag("AI"), Is.EqualTo(2));
Assert.That(scheduler.Statistics!.GetCountByTag("Physics"), Is.EqualTo(1));
Assert.That(scheduler.Statistics!.GetCountByTag("Graphics"), Is.EqualTo(0));
}
[Test]
public void Reset_ShouldClearAllStatistics()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
IEnumerator<IYieldInstruction> TestCoroutine()
{
yield break;
}
// Act
scheduler.Run(TestCoroutine());
scheduler.Run(TestCoroutine());
scheduler.Update();
scheduler.Statistics!.Reset();
// Assert
Assert.That(scheduler.Statistics.TotalStarted, Is.EqualTo(0));
Assert.That(scheduler.Statistics.TotalCompleted, Is.EqualTo(0));
Assert.That(scheduler.Statistics.TotalFailed, Is.EqualTo(0));
Assert.That(scheduler.Statistics.ActiveCount, Is.EqualTo(0));
Assert.That(scheduler.Statistics.PausedCount, Is.EqualTo(0));
Assert.That(scheduler.Statistics.AverageExecutionTimeMs, Is.EqualTo(0));
Assert.That(scheduler.Statistics.MaxExecutionTimeMs, Is.EqualTo(0));
}
[Test]
public void GenerateReport_ShouldReturnFormattedString()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
IEnumerator<IYieldInstruction> TestCoroutine()
{
yield break;
}
// Act
scheduler.Run(TestCoroutine(), tag: "Test", priority: CoroutinePriority.High);
scheduler.Update();
var report = scheduler.Statistics!.GenerateReport();
// Assert
Assert.That(report, Is.Not.Null);
Assert.That(report, Does.Contain("协程统计报告"));
Assert.That(report, Does.Contain("总启动数"));
Assert.That(report, Does.Contain("总完成数"));
}
[Test]
public void Statistics_ShouldBeThreadSafe()
{
// Arrange
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource, enableStatistics: true);
IEnumerator<IYieldInstruction> TestCoroutine()
{
yield break;
}
// Act - 并发读取统计信息
var tasks = new List<Task>();
for (var i = 0; i < 10; i++)
{
tasks.Add(Task.Run(() =>
{
for (var j = 0; j < 100; j++)
{
_ = scheduler.Statistics!.TotalStarted;
_ = scheduler.Statistics.TotalCompleted;
_ = scheduler.Statistics.AverageExecutionTimeMs;
_ = scheduler.Statistics.GenerateReport();
}
}));
}
// 同时启动和完成协程
for (var i = 0; i < 100; i++)
{
scheduler.Run(TestCoroutine());
}
scheduler.Update();
Task.WaitAll(tasks.ToArray());
// Assert - 不应该抛出异常
Assert.That(scheduler.Statistics!.TotalStarted, Is.EqualTo(100));
Assert.That(scheduler.Statistics.TotalCompleted, Is.EqualTo(100));
}
}

View File

@ -0,0 +1,47 @@
using GFramework.Core.Abstractions.coroutine;
namespace GFramework.Core.Tests.coroutine;
/// <summary>
/// 可控制的时间源,用于协程测试
/// </summary>
public sealed class FakeTimeSource : ITimeSource
{
/// <summary>
/// 获取当前累计时间
/// </summary>
public double CurrentTime { get; private set; }
/// <summary>
/// 获取上一帧的时间增量
/// </summary>
public double DeltaTime { get; private set; }
/// <summary>
/// 更新时间源
/// </summary>
public void Update()
{
// 在测试中Update 不做任何事情
// 时间推进由 Advance 方法控制
}
/// <summary>
/// 前进指定的时间
/// </summary>
/// <param name="deltaTime">时间增量(秒)</param>
public void Advance(double deltaTime)
{
DeltaTime = deltaTime;
CurrentTime += deltaTime;
}
/// <summary>
/// 重置时间源到初始状态
/// </summary>
public void Reset()
{
CurrentTime = 0;
DeltaTime = 0;
}
}

View File

@ -7,11 +7,26 @@ namespace GFramework.Core.coroutine;
/// </summary>
internal class CoroutineMetadata
{
/// <summary>
/// 协程的分组标识符,用于批量管理协程
/// </summary>
public string? Group;
/// <summary>
/// 协程的优先级
/// </summary>
public CoroutinePriority Priority;
/// <summary>
/// 协程在调度器中的槽位索引
/// </summary>
public int SlotIndex;
/// <summary>
/// 协程开始执行的时间戳(毫秒)
/// </summary>
public double StartTime;
/// <summary>
/// 协程当前的执行状态
/// </summary>

View File

@ -1,4 +1,4 @@
using GFramework.Core.Abstractions.coroutine;
using GFramework.Core.Abstractions.coroutine;
using GFramework.Core.Abstractions.logging;
using GFramework.Core.coroutine.instructions;
using GFramework.Core.logging;
@ -12,17 +12,22 @@ namespace GFramework.Core.coroutine;
/// <param name="timeSource">时间源接口,提供时间相关数据</param>
/// <param name="instanceId">实例ID默认为1</param>
/// <param name="initialCapacity">初始容量默认为256</param>
/// <param name="enableStatistics">是否启用统计功能默认为false</param>
public sealed class CoroutineScheduler(
ITimeSource timeSource,
byte instanceId = 1,
int initialCapacity = 256)
int initialCapacity = 256,
bool enableStatistics = false)
{
private readonly Dictionary<string, HashSet<CoroutineHandle>> _grouped = new();
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CoroutineScheduler));
private readonly Dictionary<CoroutineHandle, CoroutineMetadata> _metadata = new();
private readonly CoroutineStatistics? _statistics = enableStatistics ? new CoroutineStatistics() : null;
private readonly Dictionary<string, HashSet<CoroutineHandle>> _tagged = new();
private readonly ITimeSource _timeSource = timeSource ?? throw new ArgumentNullException(nameof(timeSource));
private readonly Dictionary<CoroutineHandle, HashSet<CoroutineHandle>> _waiting = new();
private int _nextSlot;
private int _pausedCount;
private CoroutineSlot?[] _slots = new CoroutineSlot?[initialCapacity];
@ -36,6 +41,11 @@ public sealed class CoroutineScheduler(
/// </summary>
public int ActiveCoroutineCount { get; private set; }
/// <summary>
/// 获取协程统计信息(如果启用)
/// </summary>
public ICoroutineStatistics? Statistics => _statistics;
/// <summary>
/// 协程异常处理回调,当协程执行过程中发生异常时触发
/// 注意:事件处理程序会在独立任务中异步调用,以避免阻塞调度器主循环
@ -61,10 +71,14 @@ public sealed class CoroutineScheduler(
/// </summary>
/// <param name="coroutine">要运行的协程枚举器</param>
/// <param name="tag">协程标签,可选</param>
/// <param name="priority">协程优先级默认为Normal</param>
/// <param name="group">协程分组,可选</param>
/// <returns>协程句柄</returns>
public CoroutineHandle Run(
IEnumerator<IYieldInstruction>? coroutine,
string? tag = null)
string? tag = null,
CoroutinePriority priority = CoroutinePriority.Normal,
string? group = null)
{
if (coroutine == null)
return default;
@ -79,7 +93,8 @@ public sealed class CoroutineScheduler(
{
Enumerator = coroutine,
State = CoroutineState.Running,
Handle = handle
Handle = handle,
Priority = priority
};
_slots[slotIndex] = slot;
@ -87,12 +102,20 @@ public sealed class CoroutineScheduler(
{
SlotIndex = slotIndex,
State = CoroutineState.Running,
Tag = tag
Tag = tag,
Priority = priority,
Group = group,
StartTime = _timeSource.CurrentTime * 1000 // 转换为毫秒
};
if (!string.IsNullOrEmpty(tag))
AddTag(tag, handle);
if (!string.IsNullOrEmpty(group))
AddGroup(group, handle);
_statistics?.RecordStart(priority, tag);
Prewarm(slotIndex);
ActiveCoroutineCount++;
@ -107,8 +130,34 @@ public sealed class CoroutineScheduler(
_timeSource.Update();
var delta = _timeSource.DeltaTime;
// 遍历所有槽位并更新协程状态
// 更新统计信息
if (_statistics != null)
{
_statistics.ActiveCount = ActiveCoroutineCount;
_statistics.PausedCount = _pausedCount;
}
// 按优先级排序槽位索引(高优先级优先执行)
var sortedIndices = new List<int>(_nextSlot);
for (var i = 0; i < _nextSlot; i++)
{
var slot = _slots[i];
if (slot is { State: CoroutineState.Running })
sortedIndices.Add(i);
}
// 按优先级降序排序
sortedIndices.Sort((a, b) =>
{
var slotA = _slots[a];
var slotB = _slots[b];
if (slotA == null || slotB == null)
return 0;
return slotB.Priority.CompareTo(slotA.Priority);
});
// 遍历所有槽位并更新协程状态
foreach (var i in sortedIndices)
{
var slot = _slots[i];
if (slot is not { State: CoroutineState.Running })
@ -216,6 +265,7 @@ public sealed class CoroutineScheduler(
slot.State = CoroutineState.Paused;
meta.State = CoroutineState.Paused;
_pausedCount++;
return true;
}
@ -235,6 +285,7 @@ public sealed class CoroutineScheduler(
slot.State = CoroutineState.Running;
meta.State = CoroutineState.Running;
_pausedCount--;
return true;
}
@ -254,6 +305,60 @@ public sealed class CoroutineScheduler(
#endregion
#region Group Management
/// <summary>
/// 暂停指定分组的所有协程
/// </summary>
/// <param name="group">分组名称</param>
/// <returns>被暂停的协程数量</returns>
public int PauseGroup(string group)
{
if (!_grouped.TryGetValue(group, out var handles))
return 0;
return handles.Count(Pause);
}
/// <summary>
/// 恢复指定分组的所有协程
/// </summary>
/// <param name="group">分组名称</param>
/// <returns>被恢复的协程数量</returns>
public int ResumeGroup(string group)
{
if (!_grouped.TryGetValue(group, out var handles))
return 0;
return handles.Count(Resume);
}
/// <summary>
/// 终止指定分组的所有协程
/// </summary>
/// <param name="group">分组名称</param>
/// <returns>被终止的协程数量</returns>
public int KillGroup(string group)
{
if (!_grouped.TryGetValue(group, out var handles))
return 0;
var copy = handles.ToArray();
return copy.Count(Kill);
}
/// <summary>
/// 获取指定分组的协程数量
/// </summary>
/// <param name="group">分组名称</param>
/// <returns>协程数量</returns>
public int GetGroupCount(string group)
{
return _grouped.TryGetValue(group, out var handles) ? handles.Count : 0;
}
#endregion
#region Wait / Tag / Clear
/// <summary>
@ -289,6 +394,7 @@ public sealed class CoroutineScheduler(
{
if (!_tagged.TryGetValue(tag, out var handles))
return 0;
var copy = handles.ToArray();
return copy.Count(Kill);
}
@ -303,10 +409,12 @@ public sealed class CoroutineScheduler(
Array.Clear(_slots);
_metadata.Clear();
_tagged.Clear();
_grouped.Clear();
_waiting.Clear();
_nextSlot = 0;
ActiveCoroutineCount = 0;
_pausedCount = 0;
return count;
}
@ -352,18 +460,26 @@ public sealed class CoroutineScheduler(
if (!handle.IsValid)
return;
// 记录统计信息
if (_metadata.TryGetValue(handle, out var meta))
{
var executionTime = _timeSource.CurrentTime * 1000 - meta.StartTime;
_statistics?.RecordComplete(executionTime, meta.Priority, meta.Tag);
}
_slots[slotIndex] = null;
ActiveCoroutineCount--;
RemoveTag(handle);
RemoveGroup(handle);
_metadata.Remove(handle);
// 唤醒等待者
if (!_waiting.TryGetValue(handle, out var waiters)) return;
foreach (var waiter in waiters)
{
if (!_metadata.TryGetValue(waiter, out var meta)) continue;
var s = _slots[meta.SlotIndex];
if (!_metadata.TryGetValue(waiter, out var waiterMeta)) continue;
var s = _slots[waiterMeta.SlotIndex];
if (s == null) continue;
switch (s.Waiting)
{
@ -371,10 +487,13 @@ public sealed class CoroutineScheduler(
case WaitForCoroutine wfc:
wfc.Complete();
break;
default:
// 其他类型的等待指令不需要特殊处理
break;
}
s.State = CoroutineState.Running;
meta.State = CoroutineState.Running;
waiterMeta.State = CoroutineState.Running;
}
_waiting.Remove(handle);
@ -390,6 +509,12 @@ public sealed class CoroutineScheduler(
var slot = _slots[slotIndex];
var handle = slot?.Handle ?? default;
// 记录统计信息
if (handle.IsValid && _metadata.TryGetValue(handle, out var meta))
{
_statistics?.RecordFailure(meta.Priority, meta.Tag);
}
// 将异常回调派发到线程池,避免阻塞调度器主循环
var handler = OnCoroutineException;
if (handler != null)
@ -459,5 +584,41 @@ public sealed class CoroutineScheduler(
meta.Tag = null;
}
/// <summary>
/// 为协程添加分组
/// </summary>
/// <param name="group">分组名称</param>
/// <param name="handle">协程句柄</param>
private void AddGroup(string group, CoroutineHandle handle)
{
if (!_grouped.TryGetValue(group, out var set))
{
set = new HashSet<CoroutineHandle>();
_grouped[group] = set;
}
set.Add(handle);
_metadata[handle].Group = group;
}
/// <summary>
/// 移除协程分组
/// </summary>
/// <param name="handle">协程句柄</param>
private void RemoveGroup(CoroutineHandle handle)
{
if (!_metadata.TryGetValue(handle, out var meta) || meta.Group == null)
return;
if (_grouped.TryGetValue(meta.Group, out var set))
{
set.Remove(handle);
if (set.Count == 0)
_grouped.Remove(meta.Group);
}
meta.Group = null;
}
#endregion
}

View File

@ -17,6 +17,16 @@ internal sealed class CoroutineSlot
/// </summary>
public CoroutineHandle Handle;
/// <summary>
/// 协程是否已经开始执行
/// </summary>
public bool HasStarted;
/// <summary>
/// 协程的优先级
/// </summary>
public CoroutinePriority Priority;
/// <summary>
/// 协程当前状态
/// </summary>

View File

@ -0,0 +1,215 @@
using System.Text;
using GFramework.Core.Abstractions.coroutine;
namespace GFramework.Core.coroutine;
/// <summary>
/// 协程统计信息实现类
/// 线程安全:使用 Interlocked 操作确保计数器的原子性
/// </summary>
internal sealed class CoroutineStatistics : ICoroutineStatistics
{
private readonly Dictionary<CoroutinePriority, int> _countByPriority = new();
private readonly Dictionary<string, int> _countByTag = new();
private readonly object _lock = new();
private int _activeCount;
private double _maxExecutionTimeMs;
private int _pausedCount;
private long _totalCompleted;
private long _totalExecutionTimeMs;
private long _totalFailed;
private long _totalStarted;
/// <inheritdoc />
public long TotalStarted => Interlocked.Read(ref _totalStarted);
/// <inheritdoc />
public long TotalCompleted => Interlocked.Read(ref _totalCompleted);
/// <inheritdoc />
public long TotalFailed => Interlocked.Read(ref _totalFailed);
/// <inheritdoc />
public int ActiveCount
{
get => Interlocked.CompareExchange(ref _activeCount, 0, 0);
set => Interlocked.Exchange(ref _activeCount, value);
}
/// <inheritdoc />
public int PausedCount
{
get => Interlocked.CompareExchange(ref _pausedCount, 0, 0);
set => Interlocked.Exchange(ref _pausedCount, value);
}
/// <inheritdoc />
public double AverageExecutionTimeMs
{
get
{
var completed = Interlocked.Read(ref _totalCompleted);
if (completed == 0)
return 0;
var totalTime = Interlocked.Read(ref _totalExecutionTimeMs);
return (double)totalTime / completed;
}
}
/// <inheritdoc />
public double MaxExecutionTimeMs
{
get
{
lock (_lock)
{
return _maxExecutionTimeMs;
}
}
}
/// <inheritdoc />
public int GetCountByPriority(CoroutinePriority priority)
{
lock (_lock)
{
return _countByPriority.TryGetValue(priority, out var count) ? count : 0;
}
}
/// <inheritdoc />
public int GetCountByTag(string tag)
{
lock (_lock)
{
return _countByTag.TryGetValue(tag, out var count) ? count : 0;
}
}
/// <inheritdoc />
public void Reset()
{
Interlocked.Exchange(ref _totalStarted, 0);
Interlocked.Exchange(ref _totalCompleted, 0);
Interlocked.Exchange(ref _totalFailed, 0);
Interlocked.Exchange(ref _totalExecutionTimeMs, 0);
Interlocked.Exchange(ref _activeCount, 0);
Interlocked.Exchange(ref _pausedCount, 0);
lock (_lock)
{
_maxExecutionTimeMs = 0;
_countByPriority.Clear();
_countByTag.Clear();
}
}
/// <inheritdoc />
public string GenerateReport()
{
var sb = new StringBuilder();
sb.AppendLine("=== 协程统计报告 ===");
sb.AppendLine($"总启动数: {TotalStarted}");
sb.AppendLine($"总完成数: {TotalCompleted}");
sb.AppendLine($"总失败数: {TotalFailed}");
sb.AppendLine($"当前活跃: {ActiveCount}");
sb.AppendLine($"当前暂停: {PausedCount}");
sb.AppendLine($"平均执行时间: {AverageExecutionTimeMs:F2} ms");
sb.AppendLine($"最大执行时间: {MaxExecutionTimeMs:F2} ms");
lock (_lock)
{
if (_countByPriority.Count > 0)
{
sb.AppendLine("\n按优先级统计:");
foreach (var kvp in _countByPriority.OrderByDescending(x => x.Key))
sb.AppendLine($" {kvp.Key}: {kvp.Value}");
}
if (_countByTag.Count > 0)
{
sb.AppendLine("\n按标签统计:");
foreach (var kvp in _countByTag.OrderByDescending(x => x.Value))
sb.AppendLine($" {kvp.Key}: {kvp.Value}");
}
}
return sb.ToString();
}
/// <summary>
/// 记录协程启动
/// </summary>
/// <param name="priority">协程优先级</param>
/// <param name="tag">协程标签</param>
public void RecordStart(CoroutinePriority priority, string? tag)
{
Interlocked.Increment(ref _totalStarted);
lock (_lock)
{
_countByPriority.TryGetValue(priority, out var count);
_countByPriority[priority] = count + 1;
if (!string.IsNullOrEmpty(tag))
{
_countByTag.TryGetValue(tag, out var tagCount);
_countByTag[tag] = tagCount + 1;
}
}
}
/// <summary>
/// 记录协程完成
/// </summary>
/// <param name="executionTimeMs">执行时间(毫秒)</param>
/// <param name="priority">协程优先级</param>
/// <param name="tag">协程标签</param>
public void RecordComplete(double executionTimeMs, CoroutinePriority priority, string? tag)
{
Interlocked.Increment(ref _totalCompleted);
Interlocked.Add(ref _totalExecutionTimeMs, (long)executionTimeMs);
lock (_lock)
{
if (executionTimeMs > _maxExecutionTimeMs)
_maxExecutionTimeMs = executionTimeMs;
_countByPriority.TryGetValue(priority, out var count);
_countByPriority[priority] = Math.Max(0, count - 1);
if (!string.IsNullOrEmpty(tag))
{
_countByTag.TryGetValue(tag, out var tagCount);
_countByTag[tag] = Math.Max(0, tagCount - 1);
if (_countByTag[tag] == 0)
_countByTag.Remove(tag);
}
}
}
/// <summary>
/// 记录协程失败
/// </summary>
/// <param name="priority">协程优先级</param>
/// <param name="tag">协程标签</param>
public void RecordFailure(CoroutinePriority priority, string? tag)
{
Interlocked.Increment(ref _totalFailed);
lock (_lock)
{
_countByPriority.TryGetValue(priority, out var count);
_countByPriority[priority] = Math.Max(0, count - 1);
if (!string.IsNullOrEmpty(tag))
{
_countByTag.TryGetValue(tag, out var tagCount);
_countByTag[tag] = Math.Max(0, tagCount - 1);
if (_countByTag[tag] == 0)
_countByTag.Remove(tag);
}
}
}
}