Skip to content

refactor(core): 优化核心组件的线程安全性和错误处理#67

Merged
GeWuYou merged 2 commits into
mainfrom
refactor/log-thread-safety
Mar 4, 2026
Merged

refactor(core): 优化核心组件的线程安全性和错误处理#67
GeWuYou merged 2 commits into
mainfrom
refactor/log-thread-safety

Conversation

@GeWuYou

@GeWuYou GeWuYou commented Mar 4, 2026

Copy link
Copy Markdown
Owner
  • 重构 AsyncLogAppender 的 Flush 方法,添加超时控制和 SpinWait 优化
  • 为 BindableProperty 添加线程安全锁保护,确保并发访问的安全性
  • 在 BindableProperty 中实现回调外部调用以避免死锁问题
  • 为 EasyEvents 使用 ConcurrentDictionary 替代 Dictionary 提高并发性能
  • 添加协程调度器异常处理回调机制,防止异常传播导致调度器崩溃
  • 为 FileAppender 添加初始化异常处理和资源清理逻辑
  • 补充完整的单元测试覆盖并发场景下的线程安全性验证

Summary by Sourcery

在属性、事件、日志记录以及协程调度等核心组件中提升线程安全性和错误恢复能力。

增强点:

  • 通过内部加锁使 BindableProperty 线程安全,同时在锁外调用变更回调以避免死锁。
  • AsyncLogAppender 添加基于超时的、更高效的 Flush 实现,并通过 ILogAppender 接口对外暴露。
  • CoroutineScheduler 中引入协程异常事件以及健壮的错误处理机制,防止用户回调导致调度器崩溃。
  • EasyEvents 切换为线程安全的并发存储结构,并强制每种事件类型仅允许单次注册,对于重复注册提供清晰的错误信息。
  • 加强 FileAppender 的初始化流程,包括参数校验、安全的 writer 设置,以及在初始化失败时进行清理。

测试:

  • BindableProperty 的数值设置、处理程序注册/反注册,以及 RegisterWithInitValue 添加以并发为重点的单元测试。
  • EasyEventsGetOrAdd 和不同事件类型的多线程并发注册添加单元测试,并验证重复调用 AddEvent 时的行为。
Original summary in English

Summary by Sourcery

Improve core components’ thread-safety and error resilience across properties, events, logging, and coroutine scheduling.

Enhancements:

  • Make BindableProperty thread-safe with internal locking and invoke change callbacks outside locks to avoid deadlocks.
  • Add a timeout-based, more efficient Flush implementation to AsyncLogAppender and expose it via the ILogAppender interface.
  • Introduce a coroutine exception event and robust error handling in CoroutineScheduler to prevent scheduler crashes from user callbacks.
  • Switch EasyEvents to a thread-safe concurrent backing store and enforce single registration per event type, with clear errors on duplicates.
  • Harden FileAppender initialization with argument validation, safe writer setup, and cleanup on initialization failure.

Tests:

  • Add concurrency-focused unit tests for BindableProperty value setting, handler registration/unregistration, and RegisterWithInitValue.
  • Add multi-threaded unit tests for EasyEvents GetOrAdd and concurrent registration of different event types, plus validation of duplicate AddEvent behavior.

- 重构 AsyncLogAppender 的 Flush 方法,添加超时控制和 SpinWait 优化
- 为 BindableProperty 添加线程安全锁保护,确保并发访问的安全性
- 在 BindableProperty 中实现回调外部调用以避免死锁问题
- 为 EasyEvents 使用 ConcurrentDictionary 替代 Dictionary 提高并发性能
- 添加协程调度器异常处理回调机制,防止异常传播导致调度器崩溃
- 为 FileAppender 添加初始化异常处理和资源清理逻辑
- 补充完整的单元测试覆盖并发场景下的线程安全性验证
@deepsource-io

deepsource-io Bot commented Mar 4, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in b417ece...718d7ca on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

Important

Some issues found as part of this review are outside of the diff in this pull request and aren't shown in the inline review comments due to GitHub's API limitations. You can see those issues on the DeepSource dashboard.

PR Report Card

Overall Grade  

Focus Area: Reliability
Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
C# Mar 4, 2026 2:56a.m. Review ↗
Secrets Mar 4, 2026 2:56a.m. Review ↗

@sourcery-ai

sourcery-ai Bot commented Mar 4, 2026

Copy link
Copy Markdown

审阅者指南

重构多个核心组件以提升线程安全性、错误处理能力和健壮性,包括 BindableProperty、EasyEvents、AsyncLogAppender、CoroutineScheduler、FileAppender,并新增并发场景测试。

核心线程与日志组件的更新类图

classDiagram

class IBindableProperty_T_ {
  <<interface>>
}

class IUnRegister {
  <<interface>>
}

class IEvent {
  <<interface>>
}

class ILogAppender {
  <<interface>>
  +void Append(LogEntry entry)
  +void Flush()
}

class ILogFormatter {
  <<interface>>
}

class ILogFilter {
  <<interface>>
}

class LogEntry {
}

class CoroutineHandle {
}

class BindableProperty_T_ {
  -object _lock
  -Action_T_ _mOnValueChanged
  -T MValue
  +BindableProperty_T_(T defaultValue)
  +T Value
  +T GetValue()
  +void SetValue(T value)
  +IUnRegister Register(Action_T_ onValueChanged)
  +IUnRegister RegisterWithInitValue(Action_T_ action)
  +void UnRegister(Action_T_ onValueChanged)
}

class BindablePropertyUnRegister_T_ {
  -BindableProperty_T_ _property
  -Action_T_ _onValueChanged
  +BindablePropertyUnRegister_T_(BindableProperty_T_ property, Action_T_ onValueChanged)
  +void UnRegister()
}

class EasyEvents {
  -static EasyEvents MGlobalEvents
  -ConcurrentDictionary~Type, IEvent~ _mTypeEvents
  +EasyEvents()
  +static EasyEvents GlobalEvents
  +static T Get_T_()
  +void AddEvent_T_()
  +T GetEvent_T_()
  +T GetOrAddEvent_T_()
}

class AsyncLogAppender {
  -ILogAppender _innerAppender
  -Channel~LogEntry~ _channel
  -bool _disposed
  +AsyncLogAppender(ILogAppender innerAppender)
  +void Append(LogEntry entry)
  +bool Flush(TimeSpan timeout)
  +void Dispose()
}

class CoroutineScheduler {
  -ITimeSource _timeSource
  -int _instanceId
  -CoroutineSlot[] _slots
  -int ActiveCoroutineCount
  +CoroutineScheduler(ITimeSource timeSource, int instanceId)
  +int ActiveCoroutineCount
  +bool IsAlive(CoroutineHandle handle)
  +CoroutineHandle StartCoroutine(IEnumerator routine)
  +void Update(float deltaTime)
  +event Action~CoroutineHandle, Exception~ OnCoroutineException
  -void Complete(int slotIndex)
  -void OnError(int slotIndex, Exception ex)
}

class FileAppender {
  -string _filePath
  -ILogFormatter _formatter
  -ILogFilter _filter
  -StreamWriter _writer
  +FileAppender(string filePath, ILogFormatter formatter, ILogFilter filter)
  +void Append(LogEntry entry)
  +void Flush()
  +void Dispose()
  -void EnsureDirectoryExists()
  -void InitializeWriter()
}

class ITimeSource {
  <<interface>>
}

class CoroutineSlot {
  -CoroutineHandle Handle
}

BindableProperty_T_ ..|> IBindableProperty_T_
BindablePropertyUnRegister_T_ ..|> IUnRegister
BindableProperty_T_ o-- BindablePropertyUnRegister_T_
EasyEvents o-- IEvent
AsyncLogAppender ..|> ILogAppender
AsyncLogAppender o-- ILogAppender : _innerAppender
FileAppender ..|> ILogAppender
FileAppender o-- ILogFormatter
FileAppender o-- ILogFilter
CoroutineScheduler o-- CoroutineSlot
CoroutineScheduler o-- ITimeSource
Loading

文件级变更

Change Details Files
使 BindableProperty 线程安全,并在调用回调时避免死锁。
  • 引入一个私有锁对象来保护值和委托的访问。
  • 更新 Value 的 setter:在获取锁后检查值是否变化,更新底层字段,捕获当前委托,然后在锁外调用回调。
  • 在相同的锁下包装 Register 和 UnRegister 对回调委托的修改。
  • 将 RegisterWithInitValue 改为在锁内读取当前值,使用该快照调用回调,然后再注册回调以处理后续变更。
  • 新增以并发为重点的单元测试,覆盖设置值、事件注册/触发、注册/反注册以及 RegisterWithInitValue 等场景。
GFramework.Core/property/BindableProperty.cs
GFramework.Core.Tests/property/BindablePropertyTests.cs
使用 ConcurrentDictionary 使 EasyEvents 线程安全,并清晰定义重复注册时的行为。
  • 将内部的 Dictionary<Type,IEvent> 替换为 ConcurrentDictionary<Type,IEvent>。
  • 将 AddEvent 改为使用 TryAdd,并在事件类型已被注册时抛出 ArgumentException。
  • 重写 GetOrAddEvent,委托给 ConcurrentDictionary.GetOrAdd 以实现原子创建和获取。
  • 新增单元测试,覆盖并发 GetOrAdd 调用、重复 AddEvent 行为以及不同事件类型的并发注册。
  • 在测试中对文档注释标点进行小幅调整。
GFramework.Core/events/EasyEvents.cs
GFramework.Core.Tests/events/EasyEventsTests.cs
增强 AsyncLogAppender 的刷新行为,引入超时和更高效的等待方式,并暴露接口级的 Flush 实现。
  • 添加显式的 ILogAppender.Flush 接口实现,并将其委托给公共的 Flush 方法。
  • 将 Flush 修改为返回表示成功与否的 bool,接受可选超时参数(默认 30 秒),且在已释放时提前返回。
  • 使用 SpinWait 等待通道读取端清空,而不是 Thread.Sleep,并遵守超时以避免无限阻塞。
  • 确保仅在通道清空后才调用内部 appender 的 Flush,并在成功时返回 true。
GFramework.Core/logging/appenders/AsyncLogAppender.cs
在 CoroutineScheduler 中为协程异常添加健壮的错误处理。
  • 新增公共事件 OnCoroutineException,以便外部处理协程失败。
  • 更新 OnError:基于 slot 构建 handle,在 try/catch 中调用 OnCoroutineException 回调,防止回调失败导致调度器崩溃。
  • 将回调异常和原始协程异常都以更清晰的消息输出到 Console.Error。
  • 确保仍然调用 Complete 来完成失败的协程槽位清理。
  • 文档中注明调度器预期在单线程环境中使用。
GFramework.Core/coroutine/CoroutineScheduler.cs
强化 FileAppender 的初始化过程,在失败时完成清理并记录错误条件。
  • 为构造函数添加 XML 文档注释,说明可能抛出的异常(路径无效时抛出 ArgumentException,文件相关问题抛出 IOException)。
  • 在构造函数中用 try/catch 包裹 EnsureDirectoryExists 和 InitializeWriter,在异常时释放任何部分创建的 writer 并将其置为 null,然后重新抛出异常。
  • 在保留现有初始化行为的同时,确保初始化失败时不会泄漏资源。
GFramework.Core/logging/appenders/FileAppender.cs

提示与命令

与 Sourcery 交互

  • 触发新评审: 在 Pull Request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的评审评论。
  • 从评审评论生成 GitHub issue: 在评审评论下回复,请求 Sourcery 从该评论创建 issue。你也可以在评审评论中回复 @sourcery-ai issue 来从中创建 issue。
  • 生成 Pull Request 标题: 在 Pull Request 标题任意位置写上 @sourcery-ai,即可随时生成标题。你也可以在 Pull Request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 Pull Request 总结: 在 Pull Request 正文任意位置写上 @sourcery-ai summary,即可在指定位置生成 PR 总结。你也可以在 Pull Request 中评论 @sourcery-ai summary 来(重新)生成总结。
  • 生成审阅者指南: 在 Pull Request 中评论 @sourcery-ai guide,即可(重新)生成审阅者指南。
  • 解决所有 Sourcery 评论: 在 Pull Request 中评论 @sourcery-ai resolve,即可将所有 Sourcery 评论标记为已解决。如果你已经处理完所有评论且不希望再看到它们,这会很有用。
  • 忽略所有 Sourcery 评审: 在 Pull Request 中评论 @sourcery-ai dismiss,即可忽略所有现有的 Sourcery 评审。特别适用于你希望从新的评审开始——别忘了再评论 @sourcery-ai review 来触发新评审!

自定义使用体验

访问你的 控制面板 可以:

  • 启用或禁用评审功能,例如 Sourcery 自动生成的 Pull Request 总结、审阅者指南等。
  • 更改评审语言。
  • 添加、移除或编辑自定义评审指令。
  • 调整其他评审设置。

获取帮助

Original review guide in English

Reviewer's Guide

Refactors several core components to improve thread safety, error handling, and robustness, including BindableProperty, EasyEvents, AsyncLogAppender, CoroutineScheduler, FileAppender, and adds concurrent scenario tests.

Updated class diagram for core threading and logging components

classDiagram

class IBindableProperty_T_ {
  <<interface>>
}

class IUnRegister {
  <<interface>>
}

class IEvent {
  <<interface>>
}

class ILogAppender {
  <<interface>>
  +void Append(LogEntry entry)
  +void Flush()
}

class ILogFormatter {
  <<interface>>
}

class ILogFilter {
  <<interface>>
}

class LogEntry {
}

class CoroutineHandle {
}

class BindableProperty_T_ {
  -object _lock
  -Action_T_ _mOnValueChanged
  -T MValue
  +BindableProperty_T_(T defaultValue)
  +T Value
  +T GetValue()
  +void SetValue(T value)
  +IUnRegister Register(Action_T_ onValueChanged)
  +IUnRegister RegisterWithInitValue(Action_T_ action)
  +void UnRegister(Action_T_ onValueChanged)
}

class BindablePropertyUnRegister_T_ {
  -BindableProperty_T_ _property
  -Action_T_ _onValueChanged
  +BindablePropertyUnRegister_T_(BindableProperty_T_ property, Action_T_ onValueChanged)
  +void UnRegister()
}

class EasyEvents {
  -static EasyEvents MGlobalEvents
  -ConcurrentDictionary~Type, IEvent~ _mTypeEvents
  +EasyEvents()
  +static EasyEvents GlobalEvents
  +static T Get_T_()
  +void AddEvent_T_()
  +T GetEvent_T_()
  +T GetOrAddEvent_T_()
}

class AsyncLogAppender {
  -ILogAppender _innerAppender
  -Channel~LogEntry~ _channel
  -bool _disposed
  +AsyncLogAppender(ILogAppender innerAppender)
  +void Append(LogEntry entry)
  +bool Flush(TimeSpan timeout)
  +void Dispose()
}

class CoroutineScheduler {
  -ITimeSource _timeSource
  -int _instanceId
  -CoroutineSlot[] _slots
  -int ActiveCoroutineCount
  +CoroutineScheduler(ITimeSource timeSource, int instanceId)
  +int ActiveCoroutineCount
  +bool IsAlive(CoroutineHandle handle)
  +CoroutineHandle StartCoroutine(IEnumerator routine)
  +void Update(float deltaTime)
  +event Action~CoroutineHandle, Exception~ OnCoroutineException
  -void Complete(int slotIndex)
  -void OnError(int slotIndex, Exception ex)
}

class FileAppender {
  -string _filePath
  -ILogFormatter _formatter
  -ILogFilter _filter
  -StreamWriter _writer
  +FileAppender(string filePath, ILogFormatter formatter, ILogFilter filter)
  +void Append(LogEntry entry)
  +void Flush()
  +void Dispose()
  -void EnsureDirectoryExists()
  -void InitializeWriter()
}

class ITimeSource {
  <<interface>>
}

class CoroutineSlot {
  -CoroutineHandle Handle
}

BindableProperty_T_ ..|> IBindableProperty_T_
BindablePropertyUnRegister_T_ ..|> IUnRegister
BindableProperty_T_ o-- BindablePropertyUnRegister_T_
EasyEvents o-- IEvent
AsyncLogAppender ..|> ILogAppender
AsyncLogAppender o-- ILogAppender : _innerAppender
FileAppender ..|> ILogAppender
FileAppender o-- ILogFormatter
FileAppender o-- ILogFilter
CoroutineScheduler o-- CoroutineSlot
CoroutineScheduler o-- ITimeSource
Loading

File-Level Changes

Change Details Files
Make BindableProperty thread-safe and avoid deadlocks when invoking callbacks.
  • Introduce a private lock object to guard value and delegate access.
  • Update Value setter to acquire the lock, check for value changes, update the backing field, capture the delegate, then invoke callbacks outside the lock.
  • Wrap Register and UnRegister modifications of the callback delegate in the same lock.
  • Change RegisterWithInitValue to read the current value under lock, invoke the callback with that snapshot, then register it for future changes.
  • Add concurrency-focused unit tests for value setting, event registering/triggering, register/unregister, and RegisterWithInitValue.
GFramework.Core/property/BindableProperty.cs
GFramework.Core.Tests/property/BindablePropertyTests.cs
Make EasyEvents thread-safe using ConcurrentDictionary and clearly define behavior on duplicate registration.
  • Replace the internal Dictionary<Type,IEvent> with ConcurrentDictionary<Type,IEvent>.
  • Change AddEvent to use TryAdd and throw ArgumentException when the event type is already registered.
  • Rewrite GetOrAddEvent to delegate to ConcurrentDictionary.GetOrAdd for atomic creation and retrieval.
  • Add unit tests covering concurrent GetOrAdd calls, duplicate AddEvent behavior, and concurrent registration of different event types.
  • Minor doc comment punctuation tweaks in tests.
GFramework.Core/events/EasyEvents.cs
GFramework.Core.Tests/events/EasyEventsTests.cs
Enhance AsyncLogAppender flushing behavior with timeout and more efficient waiting, and expose interface Flush implementation.
  • Add explicit ILogAppender.Flush interface implementation that delegates to the public Flush method.
  • Change Flush to return a bool indicating success, accept an optional timeout with a 30-second default, and early-out if disposed.
  • Use SpinWait to wait for the channel reader to drain instead of Thread.Sleep, and honor the timeout to avoid indefinite blocking.
  • Ensure the inner appender Flush is called only after the channel is drained and return true on success.
GFramework.Core/logging/appenders/AsyncLogAppender.cs
Add robust error handling for coroutine exceptions in CoroutineScheduler.
  • Introduce a public OnCoroutineException event to allow external handling of coroutine failures.
  • Update OnError to build a handle from the slot, invoke the OnCoroutineException callback in a try/catch to prevent callback failures from crashing the scheduler.
  • Log both callback exceptions and the original coroutine exception to Console.Error with clearer messages.
  • Ensure Complete is still called to finalize the failed coroutine slot.
  • Document that the scheduler is intended for single-threaded use.
GFramework.Core/coroutine/CoroutineScheduler.cs
Harden FileAppender initialization to clean up on failure and document error conditions.
  • Add XML documentation comments for constructor exceptions (ArgumentException for invalid path and IOException for file issues).
  • Wrap EnsureDirectoryExists and InitializeWriter in a try/catch in the constructor, disposing any partially created writer and nulling it before rethrowing.
  • Keep existing initialization behavior while ensuring resources are not leaked when initialization fails.
GFramework.Core/logging/appenders/FileAppender.cs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

{
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.

Comment on lines +88 to +91
lock (exceptions)
{
exceptions.Add(ex);
}

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:

Comment on lines +151 to +154
lock (exceptions)
{
exceptions.Add(ex);
}

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:

Comment on lines +213 to +216
lock (exceptions)
{
exceptions.Add(ex);
}

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:

Comment on lines +249 to +252
lock (exceptions)
{
exceptions.Add(ex);
}

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:

Comment on lines +293 to +296
lock (exceptions)
{
exceptions.Add(ex);
}

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:

Comment on lines +326 to +329
lock (receivedValues)
{
receivedValues.Add(value);
}

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:

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

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:

public IUnRegister RegisterWithInitValue(Action<T> action)
{
action(MValue);
T currentValue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Variable `currentValue` is uninitialized


While some variables such as fields can be initialized and be assigned a default value, it is possible for local variables to not be initialized. This however is not a good practice as it is capable of critically altering your program's path, thereby affecting its logic.

It is therefore recommended that you always initialize variables, ideally where they're being declared.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - 我发现了 1 个问题,并提供了一些整体层面的反馈:

  • 新增的 AsyncLogAppender.Flush 使用 SpinWait_channel.Reader.Count 上循环,这既可能在最长 30 秒内持续占用 CPU(busy‑spin),又仍然会与正在进行中的读取产生竞态条件(在消费者处理数据时 Count 可能为 0);建议使用一个更明确的完成信号(例如等待一个在消费者耗尽通道时完成的任务 Task),或者将 SpinWait.SpinUntil 与一个更可靠的条件组合使用。
  • BindableProperty<T> 中,只有修改操作和订阅变更是在 _lock 下进行,而 Valueget 访问路径以及 GetValue() 仍然是未同步的,这可能导致撕裂读取,或者在类已经被文档声明为完全线程安全的情况下,仍然观察到部分更新的状态;建议在读取时也使用同一把锁(或采用线程安全的底层存储机制),以与对外宣称的保证保持一致。
  • AsyncLogAppenderFlush(TimeSpan?) 现在会返回一个 bool,但 ILogAppender.Flush 的显式接口实现丢弃了这个结果,因此通过接口调用的使用方无法对超时/部分刷新的情况作出反应;如果超时结果很重要,建议通过接口暴露该结果,或者提供另一种可观察的机制。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `AsyncLogAppender.Flush` loops on `_channel.Reader.Count` with `SpinWait`, which can both busy-spin the CPU for up to 30 seconds and still race with in‑flight reads (Count can be 0 while a consumer is processing); consider using a proper completion signal (e.g., waiting on a Task that completes when the consumer drains the channel) or `SpinWait.SpinUntil` combined with a more reliable condition.
- In `BindableProperty<T>`, only mutations and subscription changes are under `_lock` while `Value`'s `get` path and `GetValue()` remain unsynchronized, which can lead to torn reads or observing partially-updated state despite the class now being documented as fully thread-safe; consider taking the same lock on reads (or using a thread-safe backing mechanism) to match the advertised guarantees.
- `AsyncLogAppender` now returns a `bool` from `Flush(TimeSpan?)` but the `ILogAppender.Flush` explicit interface implementation discards this result, so callers via the interface cannot react to timeout/partial flush; if the timeout outcome is important, consider exposing it through the interface or providing another observable mechanism.

## Individual Comments

### Comment 1
<location path="GFramework.Core/coroutine/CoroutineScheduler.cs" line_range="39-43" />
<code_context>
+    /// <summary>
+    ///     协程异常处理回调,当协程执行过程中发生异常时触发
+    /// </summary>
+    public event Action<CoroutineHandle, Exception>? OnCoroutineException;
+
     /// <summary>
</code_context>
<issue_to_address>
**suggestion (performance):** Invoking OnCoroutineException synchronously in OnError may let user callbacks block the scheduler loop.

Because `OnCoroutineException` is invoked directly from `OnError`, any slow or blocking subscriber will run on the scheduler’s path and can stall it, even though exceptions are caught.

To keep scheduler latency predictable, consider dispatching this event to another context (e.g., `Task.Run`, a dedicated error-handling queue, or a configurable dispatcher), or clearly document that `OnCoroutineException` handlers must be fast and non-blocking since they execute on the scheduler thread.

Suggested implementation:

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

```

```csharp
            var handler = OnCoroutineException;
            if (handler != null)
            {
                // 将回调调度到线程池,避免在调度器主循环中执行耗时或阻塞操作
                System.Threading.Tasks.Task.Run(() =>
                {
                    try
                    {
                        handler(handle, exception);
                    }
                    catch
                    {
                        // 这里可以根据需要记录日志,但必须吞掉异常,避免影响调度器
                    }
                });
            }

```

If the file does not already reference `System.Threading.Tasks`, you may optionally add `using System.Threading.Tasks;` at the top and then shorten `System.Threading.Tasks.Task.Run` to `Task.Run`. Also ensure that the `SEARCH` block for `OnCoroutineException?.Invoke(handle, exception);` matches the actual invocation line in your `OnError` (or equivalent) method; adjust parameter order/names if they differ (e.g. `OnCoroutineException?.Invoke(coroutineHandle, ex);`).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
帮助我变得更有用!请在每条评论上点击 👍 或 👎,我会根据这些反馈改进后续的审查建议。
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • The new AsyncLogAppender.Flush loops on _channel.Reader.Count with SpinWait, which can both busy-spin the CPU for up to 30 seconds and still race with in‑flight reads (Count can be 0 while a consumer is processing); consider using a proper completion signal (e.g., waiting on a Task that completes when the consumer drains the channel) or SpinWait.SpinUntil combined with a more reliable condition.
  • In BindableProperty<T>, only mutations and subscription changes are under _lock while Value's get path and GetValue() remain unsynchronized, which can lead to torn reads or observing partially-updated state despite the class now being documented as fully thread-safe; consider taking the same lock on reads (or using a thread-safe backing mechanism) to match the advertised guarantees.
  • AsyncLogAppender now returns a bool from Flush(TimeSpan?) but the ILogAppender.Flush explicit interface implementation discards this result, so callers via the interface cannot react to timeout/partial flush; if the timeout outcome is important, consider exposing it through the interface or providing another observable mechanism.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `AsyncLogAppender.Flush` loops on `_channel.Reader.Count` with `SpinWait`, which can both busy-spin the CPU for up to 30 seconds and still race with in‑flight reads (Count can be 0 while a consumer is processing); consider using a proper completion signal (e.g., waiting on a Task that completes when the consumer drains the channel) or `SpinWait.SpinUntil` combined with a more reliable condition.
- In `BindableProperty<T>`, only mutations and subscription changes are under `_lock` while `Value`'s `get` path and `GetValue()` remain unsynchronized, which can lead to torn reads or observing partially-updated state despite the class now being documented as fully thread-safe; consider taking the same lock on reads (or using a thread-safe backing mechanism) to match the advertised guarantees.
- `AsyncLogAppender` now returns a `bool` from `Flush(TimeSpan?)` but the `ILogAppender.Flush` explicit interface implementation discards this result, so callers via the interface cannot react to timeout/partial flush; if the timeout outcome is important, consider exposing it through the interface or providing another observable mechanism.

## Individual Comments

### Comment 1
<location path="GFramework.Core/coroutine/CoroutineScheduler.cs" line_range="39-43" />
<code_context>
+    /// <summary>
+    ///     协程异常处理回调,当协程执行过程中发生异常时触发
+    /// </summary>
+    public event Action<CoroutineHandle, Exception>? OnCoroutineException;
+
     /// <summary>
</code_context>
<issue_to_address>
**suggestion (performance):** Invoking OnCoroutineException synchronously in OnError may let user callbacks block the scheduler loop.

Because `OnCoroutineException` is invoked directly from `OnError`, any slow or blocking subscriber will run on the scheduler’s path and can stall it, even though exceptions are caught.

To keep scheduler latency predictable, consider dispatching this event to another context (e.g., `Task.Run`, a dedicated error-handling queue, or a configurable dispatcher), or clearly document that `OnCoroutineException` handlers must be fast and non-blocking since they execute on the scheduler thread.

Suggested implementation:

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

```

```csharp
            var handler = OnCoroutineException;
            if (handler != null)
            {
                // 将回调调度到线程池,避免在调度器主循环中执行耗时或阻塞操作
                System.Threading.Tasks.Task.Run(() =>
                {
                    try
                    {
                        handler(handle, exception);
                    }
                    catch
                    {
                        // 这里可以根据需要记录日志,但必须吞掉异常,避免影响调度器
                    }
                });
            }

```

If the file does not already reference `System.Threading.Tasks`, you may optionally add `using System.Threading.Tasks;` at the top and then shorten `System.Threading.Tasks.Task.Run` to `Task.Run`. Also ensure that the `SEARCH` block for `OnCoroutineException?.Invoke(handle, exception);` matches the actual invocation line in your `OnError` (or equivalent) method; adjust parameter order/names if they differ (e.g. `OnCoroutineException?.Invoke(coroutineHandle, ex);`).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread GFramework.Core/coroutine/CoroutineScheduler.cs
- 实现了基于信号量的可靠Flush完成通知机制
- 添加了OnFlushCompleted事件用于监控刷新操作结果
- 修复了BindaleProperty的线程安全问题,添加锁保护
- 将协程异常回调改为异步执行,防止阻塞调度器主循环
- 优化了AsyncLogAppender的资源清理逻辑
- 增强了Flush方法的超时处理机制
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant