Skip to content

refactor(state): 将状态机实现为完全异步操作并改进线程安全机制#28

Merged
GeWuYou merged 3 commits into
mainfrom
refactor/state-machine-async-refactor
Feb 15, 2026
Merged

refactor(state): 将状态机实现为完全异步操作并改进线程安全机制#28
GeWuYou merged 3 commits into
mainfrom
refactor/state-machine-async-refactor

Conversation

@GeWuYou

@GeWuYou GeWuYou commented Feb 15, 2026

Copy link
Copy Markdown
Owner
  • 添加 SemaphoreSlim 锁确保状态转换的线程安全性
  • 将所有同步方法重构为异步方法并移除旧的同步实现
  • 使用异步锁替代传统的 lock 机制提升并发性能
  • 优化状态历史记录的处理时机和逻辑
  • 移除过时的同步状态转换内部方法
  • 统一异常处理和资源释放机制

Summary by Sourcery

Refactor the state machine to use fully asynchronous, thread-safe state transitions and simplify the public API by routing synchronous calls through async implementations.

Enhancements:

  • Introduce an asynchronous semaphore-based transition lock to improve thread safety during state changes and unregistration.
  • Unify synchronous and asynchronous state machine APIs by delegating sync methods to their async counterparts and removing legacy synchronous internals.
  • Adjust state transition history handling to record history after asynchronous exit logic while keeping behavior consistent for callers.
  • Simplify state change extension behavior in StateMachineSystem to rely on the asynchronous transition pipeline for emitting state change events.

- 添加 SemaphoreSlim 锁确保状态转换的线程安全性
- 将所有同步方法重构为异步方法并移除旧的同步实现
- 使用异步锁替代传统的 lock 机制提升并发性能
- 优化状态历史记录的处理时机和逻辑
- 移除过时的同步状态转换内部方法
- 统一异常处理和资源释放机制
@sourcery-ai

sourcery-ai Bot commented Feb 15, 2026

Copy link
Copy Markdown

Reviewer's Guide

Refactors the state machine to use fully asynchronous state transitions with a dedicated SemaphoreSlim-based transition lock, removes legacy synchronous transition paths, and adjusts history/notification ordering to be consistent with async execution and thread safety.

Sequence diagram for asynchronous state transition with SemaphoreSlim lock

sequenceDiagram
actor Client
participant StateMachineSystem
participant TransitionLock as SemaphoreSlim_transitionLock
participant OldState as Old_state
participant NextState as Next_state

Client->>StateMachineSystem: ChangeToAsync~T~()
StateMachineSystem->>TransitionLock: WaitAsync()
TransitionLock-->>StateMachineSystem: lock_acquired
StateMachineSystem->>StateMachineSystem: Resolve target state T
StateMachineSystem->>StateMachineSystem: Get Current snapshot
alt has current state
  StateMachineSystem->>OldState: CanTransitionToAsync(Old_state, Next_state)
  alt transition_rejected
    StateMachineSystem->>StateMachineSystem: OnTransitionRejectedAsync(Old_state, Next_state)
    StateMachineSystem-->>Client: false
    StateMachineSystem->>TransitionLock: Release()
  else transition_allowed
    StateMachineSystem->>StateMachineSystem: ChangeInternalAsync(Next_state)
    StateMachineSystem->>StateMachineSystem: AddToHistory(Old_state)
    StateMachineSystem->>OldState: ExecuteExitAsync(Old_state, Next_state)
    StateMachineSystem->>NextState: ExecuteEnterAsync(Next_state, Old_state)
    StateMachineSystem->>StateMachineSystem: OnStateChangedAsync(Old_state, Next_state)
    StateMachineSystem-->>Client: true
    StateMachineSystem->>TransitionLock: Release()
  end
else no current state
  StateMachineSystem->>StateMachineSystem: ChangeInternalAsync(Next_state)
  StateMachineSystem->>StateMachineSystem: AddToHistory(null)
  StateMachineSystem->>NextState: ExecuteEnterAsync(Next_state, null)
  StateMachineSystem->>StateMachineSystem: OnStateChangedAsync(null, Next_state)
  StateMachineSystem-->>Client: true
  StateMachineSystem->>TransitionLock: Release()
end
Loading

Class diagram for the refactored asynchronous StateMachine

classDiagram
class IState {
}

class IStateMachine {
}

class StateMachine {
  -object _lock
  -HashSet~IState~ _registeredStates
  -Stack~IState~ _stateHistory
  -SemaphoreSlim _transitionLock
  +IState Current
  +Dictionary~Type, IState~ States
  +StateMachine(maxHistorySize int)
  +IStateMachine Register(state IState)
  +IStateMachine Unregister~T~()
  +Task~IStateMachine~ UnregisterAsync~T~()
  +bool CanChangeTo~T~()
  +Task~bool~ CanChangeToAsync~T~()
  +bool ChangeTo~T~()
  +Task~bool~ ChangeToAsync~T~()
  +IReadOnlyList~IState~ GetStateHistory()
  +bool GoBack()
  +Task~bool~ GoBackAsync()
  -IState PrepareUnregister~T~(isCurrentState bool)
  -void CompleteUnregister(stateToUnregister IState)
  -IState FindValidPreviousState()
  -void AddToHistory(state IState)
  -Task ChangeInternalWithoutHistoryAsync(next IState)
  -Task ChangeInternalAsync(next IState)
  -Task~bool~ CanTransitionToAsync(current IState, target IState)
  -Task ExecuteExitAsync(old IState, next IState)
  -Task ExecuteEnterAsync(next IState, previous IState)
  -Task OnStateChangingAsync(old IState, next IState)
  -Task OnStateChangedAsync(old IState, next IState)
  -Task OnTransitionRejectedAsync(current IState, target IState)
}

class StateMachineSystem {
  +void Destroy()
  +Task ChangeInternalWithoutHistoryAsync(next IState)
  +Task ChangeInternalAsync(next IState)
}

IStateMachine <|.. StateMachine
IState <|.. StateMachine
StateMachineSystem --|> StateMachine
Loading

File-Level Changes

Change Details Files
Introduce SemaphoreSlim-based async transition locking and make synchronous APIs thin wrappers over async implementations.
  • Add a SemaphoreSlim _transitionLock for serializing state transitions while keeping existing _lock for protecting shared structures like Current and States.
  • Wrap Unregister, CanChangeTo, ChangeTo, and GoBack synchronous methods to call their async counterparts via GetAwaiter().GetResult().
  • Guard UnregisterAsync, CanChangeToAsync, ChangeToAsync, and GoBackAsync bodies with _transitionLock.WaitAsync()/Release() for thread-safe transitions.
GFramework.Core/state/StateMachine.cs
Unify on async transition pipeline and remove legacy synchronous internal transition helpers.
  • Delete ChangeInternal and ChangeInternalWithoutHistory synchronous methods in StateMachine and rely exclusively on ChangeInternalAsync/ChangeInternalWithoutHistoryAsync.
  • Update ChangeToAsync to perform capability checks using CanTransitionToAsync and call ChangeInternalAsync, including async rejection handling via OnTransitionRejectedAsync.
  • Adjust GoBackAsync to use ChangeInternalWithoutHistoryAsync for rollback while holding the transition lock.
GFramework.Core/state/StateMachine.cs
GFramework.Core/state/StateMachineSystem.cs
Adjust state history and lifecycle ordering in async transitions and update collection initializations.
  • Change ChangeInternalAsync to call ExecuteExitAsync before adding to history, then add the previous state snapshot (old) to history instead of Current.
  • Ensure ExecuteEnterAsync is awaited for the new Current and async change notifications (OnStateChangingAsync/OnStateChangedAsync) wrap the full lifecycle.
  • Use collection expressions (e.g., new HashSet = [];) for _registeredStates and keep _stateHistory as Stack with unchanged semantics.
GFramework.Core/state/StateMachine.cs
Remove synchronous override path in StateMachineSystem and rely on async change notification.
  • Remove StateMachineSystem.ChangeInternal override that raised StateChangedEvent after synchronous transitions.
  • Keep the async override path (ChangeInternalAsync in base and its async event pipeline) as the single source for raising StateChangedEvent going forward.
GFramework.Core/state/StateMachineSystem.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

@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 - I've found 1 issue, and left some high level feedback:

  • The synchronous wrappers (Unregister, CanChangeTo, ChangeTo, GoBack) now just call the async versions via GetAwaiter().GetResult(), which can cause deadlocks and defeats the goal of a fully async API; consider either removing the sync API or clearly isolating/blocking it outside of the async locking path.
  • ChangeToAsync combines _transitionLock.WaitAsync() with an inner lock (_lock) to read States/Current; consolidating to a single synchronization primitive (e.g., only _transitionLock) would simplify the concurrency model and avoid potential deadlock ordering issues.
  • Since _lock now appears to be used only in ChangeToAsync, consider either removing it entirely in favor of the new async semaphore or clearly separating responsibilities between the two locks to avoid confusion for future maintainers.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The synchronous wrappers (`Unregister`, `CanChangeTo`, `ChangeTo`, `GoBack`) now just call the async versions via `GetAwaiter().GetResult()`, which can cause deadlocks and defeats the goal of a fully async API; consider either removing the sync API or clearly isolating/blocking it outside of the async locking path.
- `ChangeToAsync` combines `_transitionLock.WaitAsync()` with an inner `lock (_lock)` to read `States`/`Current`; consolidating to a single synchronization primitive (e.g., only `_transitionLock`) would simplify the concurrency model and avoid potential deadlock ordering issues.
- Since `_lock` now appears to be used only in `ChangeToAsync`, consider either removing it entirely in favor of the new async semaphore or clearly separating responsibilities between the two locks to avoid confusion for future maintainers.

## Individual Comments

### Comment 1
<location> `GFramework.Core/state/StateMachine.cs:48-51` </location>
<code_context>
-
-        CompleteUnregister(stateToUnregister);
-        return this;
+        return UnregisterAsync<T>().GetAwaiter().GetResult();
     }

</code_context>

<issue_to_address>
**issue (bug_risk):** Using GetAwaiter().GetResult() on async methods can introduce deadlock risk and subtle context issues.

All synchronous APIs (`Unregister`, `CanChangeTo`, `ChangeTo`, `GoBack`) now block on their async counterparts. Because those async flows await `SemaphoreSlim.WaitAsync` and invoke user-provided callbacks (`ExecuteEnterAsync`, `ExecuteExitAsync`, `CanTransitionToAsync`, etc.), any captured synchronization context (UI, ASP.NET classic, context-bound schedulers) can cause deadlocks for sync callers.

Consider either (1) keeping a truly synchronous path that shares a non-async core; (2) explicitly limiting usage to context-free/thread-pool scenarios; or (3) moving to async-only transition APIs and deprecating the sync ones. The current design makes it easy for callers to introduce deadlocks in common hosting environments.
</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/state/StateMachine.cs Outdated
- 移除 IStateMachine 接口中 Unregister<T>() 方法的同步定义
- 移除 StateMachine 类中 Unregister<T>() 方法的同步实现
- 移除 IStateMachine 接口中 CanChangeTo<T>() 方法的同步定义
- 移除 StateMachine 类中 CanChangeTo<T>() 方法的同步实现
- 移除 IStateMachine 接口中 ChangeTo<T>() 方法的同步定义
- 移除 StateMachine 类中 ChangeTo<T>() 方法的同步实现
- 移除 IStateMachine 接口中 GoBack() 方法的同步定义
- 移除 StateMachine 类中 GoBack() 方法的同步实现
- 删除了 StateMachineSystemTests.cs 中关于 ChangeTo 方法的基本功能测试
- 删除了 StateMachineTests.cs 中关于状态切换、注册注销等基础功能的测试用例
- 保留了异步操作相关的测试方法以简化测试套件
- 减少了测试文件的代码量并提高维护效率
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