diff --git a/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs b/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
index 7f7948a9..7e94d6a3 100644
--- a/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
+++ b/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using GFramework.Core.Architectures;
using GFramework.Core.Extensions;
@@ -78,7 +79,7 @@ public class ArchitectureConfigIntegrationTests
/// 这样依赖配置的 utility 无需再自行阻塞等待配置系统完成启动。
///
[Test]
- public async Task ConfigModuleShould_Load_Config_Before_Dependent_Utility_Initialization()
+ public async Task ConfigModuleShouldLoadConfigBeforeDependentUtilityInitialization()
{
var rootPath = CreateTempConfigRoot();
ConsumerArchitecture? architecture = null;
@@ -112,7 +113,7 @@ public class ArchitectureConfigIntegrationTests
/// 避免共享内部 bootstrap 状态导致跨架构生命周期混淆。
///
[Test]
- public async Task GameConfigModuleShould_Reject_Reusing_The_Same_Module_Instance()
+ public async Task GameConfigModuleShouldRejectReusingTheSameModuleInstance()
{
var rootPath = CreateTempConfigRoot();
ModuleOnlyArchitecture? firstArchitecture = null;
@@ -151,6 +152,99 @@ public class ArchitectureConfigIntegrationTests
}
}
+ ///
+ /// 验证配置启动帮助器在同步阻塞且存在 的线程上仍可完成初始化,
+ /// 避免架构生命周期钩子的同步桥接因为 await 捕获上下文而死锁。
+ ///
+ [Test]
+ public void GameConfigBootstrapShouldSupportSynchronousBridgeOnBlockingSynchronizationContext()
+ {
+ var rootPath = CreateTempConfigRoot();
+ GameConfigBootstrap? bootstrap = null;
+ try
+ {
+ bootstrap = CreateBootstrap(rootPath);
+
+ RunBlockingOnSynchronizationContext(
+ () => bootstrap.InitializeAsync(),
+ TimeSpan.FromSeconds(5));
+
+ var monsterTable = bootstrap.Registry.GetMonsterTable();
+ Assert.Multiple(() =>
+ {
+ Assert.That(bootstrap.IsInitialized, Is.True);
+ Assert.That(monsterTable.Get(1).Name, Is.EqualTo("Slime"));
+ Assert.That(monsterTable.FindByFaction("dungeon").Count(), Is.EqualTo(2));
+ });
+ }
+ finally
+ {
+ bootstrap?.Dispose();
+ DeleteDirectoryIfExists(rootPath);
+ }
+ }
+
+ ///
+ /// 验证模块在架构已经越过安装窗口时会拒绝安装,
+ /// 并且失败不会消耗模块实例,便于后续在新的架构上重试安装。
+ ///
+ [Test]
+ public async Task GameConfigModuleShouldRejectLateInstallationWithoutConsumingTheModuleInstance()
+ {
+ var rootPath = CreateTempConfigRoot();
+ ReadyOnlyArchitecture? readyArchitecture = null;
+ ModuleOnlyArchitecture? retryArchitecture = null;
+ var readyArchitectureInitialized = false;
+ var retryArchitectureInitialized = false;
+ try
+ {
+ var module = CreateModule(rootPath);
+
+ readyArchitecture = new ReadyOnlyArchitecture();
+ await readyArchitecture.InitializeAsync();
+ readyArchitectureInitialized = true;
+
+ var exception = Assert.Throws(() => readyArchitecture.InstallModule(module));
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(exception, Is.Not.Null);
+ Assert.That(exception!.Message, Does.Contain("BeforeUtilityInit"));
+ Assert.That(readyArchitecture.Context.GetUtilities(), Is.Empty);
+ Assert.That(module.IsInitialized, Is.False);
+ });
+
+ await readyArchitecture.DestroyAsync();
+ readyArchitectureInitialized = false;
+ readyArchitecture = null;
+ GameContext.Clear();
+
+ retryArchitecture = new ModuleOnlyArchitecture(module);
+ await retryArchitecture.InitializeAsync();
+ retryArchitectureInitialized = true;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(module.IsInitialized, Is.True);
+ Assert.That(retryArchitecture.Registry.GetMonsterTable().Get(1).Name, Is.EqualTo("Slime"));
+ });
+ }
+ finally
+ {
+ if (retryArchitecture is not null && retryArchitectureInitialized)
+ {
+ await retryArchitecture.DestroyAsync();
+ }
+
+ if (readyArchitecture is not null && readyArchitectureInitialized)
+ {
+ await readyArchitecture.DestroyAsync();
+ }
+
+ DeleteDirectoryIfExists(rootPath);
+ }
+ }
+
private static string CreateTempConfigRoot()
{
var rootPath = Path.Combine(Path.GetTempPath(), "GFramework.ConfigArchitecture", Guid.NewGuid().ToString("N"));
@@ -188,23 +282,78 @@ public class ArchitectureConfigIntegrationTests
}
///
- /// 创建一个使用配置模块的架构实例。
+ /// 在不处理消息队列的同步上下文线程上执行阻塞等待,
+ /// 用于回归验证初始化异步链不会依赖原上下文恢复 continuation。
+ ///
+ /// 要同步阻塞执行的异步操作。
+ /// 等待线程结束的超时时间。
+ private static void RunBlockingOnSynchronizationContext(Func action, TimeSpan timeout)
+ {
+ ArgumentNullException.ThrowIfNull(action);
+
+ Exception? capturedException = null;
+ var workerThread = new Thread(() =>
+ {
+ SynchronizationContext.SetSynchronizationContext(new NonPumpingSynchronizationContext());
+
+ try
+ {
+ action().GetAwaiter().GetResult();
+ }
+ catch (Exception exception)
+ {
+ capturedException = exception;
+ }
+ })
+ {
+ IsBackground = true
+ };
+
+ workerThread.Start();
+ if (!workerThread.Join(timeout))
+ {
+ Assert.Fail("The blocking synchronization-context bridge did not complete within the expected timeout.");
+ }
+
+ if (capturedException != null)
+ {
+ throw new AssertionException(
+ $"The blocking synchronization-context bridge failed: {capturedException}");
+ }
+ }
+
+ private static GameConfigBootstrap CreateBootstrap(string configRoot)
+ {
+ return new GameConfigBootstrap(CreateBootstrapOptions(configRoot));
+ }
+
+ ///
+ /// 创建一个使用配置模块的模块实例。
///
/// 测试配置根目录。
/// 已配置的模块实例。
private static GameConfigModule CreateModule(string configRoot)
{
- return new GameConfigModule(
- new GameConfigBootstrapOptions
- {
- RootPath = configRoot,
- ConfigureLoader = static loader =>
- loader.RegisterAllGeneratedConfigTables(
- new GeneratedConfigRegistrationOptions
- {
- IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
- })
- });
+ return new GameConfigModule(CreateBootstrapOptions(configRoot));
+ }
+
+ ///
+ /// 创建供测试复用的配置启动选项。
+ ///
+ /// 测试配置根目录。
+ /// 可用于模块或直接 bootstrap 的启动选项。
+ private static GameConfigBootstrapOptions CreateBootstrapOptions(string configRoot)
+ {
+ return new GameConfigBootstrapOptions
+ {
+ RootPath = configRoot,
+ ConfigureLoader = static loader =>
+ loader.RegisterAllGeneratedConfigTables(
+ new GeneratedConfigRegistrationOptions
+ {
+ IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
+ })
+ };
}
private const string MonsterSchemaJson = @"{
@@ -284,6 +433,8 @@ public class ArchitectureConfigIntegrationTests
{
public GameConfigModule ConfigModule => configModule;
+ public IConfigRegistry Registry => configModule.Registry;
+
///
/// 安装外部传入的配置模块。
///
@@ -293,6 +444,19 @@ public class ArchitectureConfigIntegrationTests
}
}
+ ///
+ /// 仅用于把架构推进到 Ready 阶段的空壳架构。
+ ///
+ private sealed class ReadyOnlyArchitecture : Architecture
+ {
+ ///
+ /// 该测试架构不注册任何组件,仅验证模块的安装窗口约束。
+ ///
+ protected override void OnInitialize()
+ {
+ }
+ }
+
///
/// 在 utility 初始化阶段直接读取配置表的探针工具。
/// 如果模块没有在 utility 阶段开始前完成首次加载,这个探针会在初始化时失败。
@@ -327,4 +491,20 @@ public class ArchitectureConfigIntegrationTests
InitializedWithLoadedConfig = true;
}
}
+
+ ///
+ /// 模拟一个不会主动处理 回调的阻塞线程上下文。
+ /// 如果初始化链错误地捕获该上下文,continuation 会永久悬挂,从而暴露同步桥接死锁。
+ ///
+ private sealed class NonPumpingSynchronizationContext : SynchronizationContext
+ {
+ ///
+ /// 丢弃异步投递的 continuation,模拟被同步阻塞且未泵消息的宿主线程。
+ ///
+ /// 要执行的回调。
+ /// 回调状态。
+ public override void Post(SendOrPostCallback d, object? state)
+ {
+ }
+ }
}
diff --git a/GFramework.Game/Config/GameConfigBootstrap.cs b/GFramework.Game/Config/GameConfigBootstrap.cs
index 242002be..9ffe34ec 100644
--- a/GFramework.Game/Config/GameConfigBootstrap.cs
+++ b/GFramework.Game/Config/GameConfigBootstrap.cs
@@ -1,3 +1,4 @@
+using System.Threading;
using GFramework.Core.Abstractions.Events;
using GFramework.Game.Abstractions.Config;
@@ -127,6 +128,11 @@ public sealed class GameConfigBootstrap : IDisposable
/// 该方法只能成功调用一次,避免同一个生命周期对象在运行中被重新拼装为另一套加载约定。
///
/// 取消令牌。
+ ///
+ /// 该入口会被 的同步生命周期钩子桥接调用,
+ /// 因此内部所有异步等待都必须使用 ConfigureAwait(false),避免在 Unity 主线程、
+ /// UI Dispatcher 或带 的测试线程上发生同步阻塞死锁。
+ ///
/// 表示异步初始化流程的任务。
/// 当当前实例已释放时抛出。
/// 当当前实例已经初始化成功时抛出。
@@ -152,7 +158,7 @@ public sealed class GameConfigBootstrap : IDisposable
{
var loader = new YamlConfigLoader(RootPath);
_options.ConfigureLoader!(loader);
- await loader.LoadAsync(Registry, cancellationToken);
+ await loader.LoadAsync(Registry, cancellationToken).ConfigureAwait(false);
if (_options.EnableHotReload)
{
diff --git a/GFramework.Game/Config/GameConfigModule.cs b/GFramework.Game/Config/GameConfigModule.cs
index 087102d3..4b726877 100644
--- a/GFramework.Game/Config/GameConfigModule.cs
+++ b/GFramework.Game/Config/GameConfigModule.cs
@@ -20,6 +20,10 @@ namespace GFramework.Game.Config;
///
public sealed class GameConfigModule : IArchitectureModule
{
+ private const int InstallStateNotInstalled = 0;
+ private const int InstallStateInstalling = 1;
+ private const int InstallStateInstalled = 2;
+
private readonly GameConfigBootstrap _bootstrap;
private readonly ModuleBootstrapLifetimeUtility _lifetimeUtility;
private int _installState;
@@ -76,15 +80,31 @@ public sealed class GameConfigModule : IArchitectureModule
{
ArgumentNullException.ThrowIfNull(architecture);
- if (Interlocked.Exchange(ref _installState, 1) != 0)
+ ValidateInstallationPhase(architecture);
+
+ if (Interlocked.CompareExchange(
+ ref _installState,
+ InstallStateInstalling,
+ InstallStateNotInstalled) != InstallStateNotInstalled)
{
throw new InvalidOperationException(
"The same GameConfigModule instance cannot be installed more than once.");
}
- architecture.RegisterUtility(Registry);
- architecture.RegisterUtility(_lifetimeUtility);
- architecture.RegisterLifecycleHook(new BootstrapInitializationHook(_bootstrap));
+ try
+ {
+ // 先注册生命周期钩子,确保任何“已错过 BeforeUtilityInit”的安装都会在暴露注册表之前失败,
+ // 避免架构看到永远不会完成首次加载的半安装配置入口。
+ architecture.RegisterLifecycleHook(new BootstrapInitializationHook(_bootstrap));
+ architecture.RegisterUtility(Registry);
+ architecture.RegisterUtility(_lifetimeUtility);
+ Volatile.Write(ref _installState, InstallStateInstalled);
+ }
+ catch
+ {
+ Volatile.Write(ref _installState, InstallStateNotInstalled);
+ throw;
+ }
}
///
@@ -127,7 +147,30 @@ public sealed class GameConfigModule : IArchitectureModule
// 架构生命周期钩子当前是同步接口,因此这里显式桥接到统一的 bootstrap 异步实现,
// 让 Architecture 模式和独立运行时模式保持同一套加载、诊断和热重载启动语义。
- bootstrap.InitializeAsync().GetAwaiter().GetResult();
+ bootstrap.InitializeAsync().ConfigureAwait(false).GetAwaiter().GetResult();
+ }
+ }
+
+ ///
+ /// 验证模块仍处于允许接入的生命周期窗口。
+ /// 该模块依赖 钩子完成首次加载,
+ /// 因此一旦架构已经离开 ,继续安装只会错过首载时机。
+ ///
+ /// 目标架构实例。
+ ///
+ /// 当目标架构已经开始组件初始化阶段时抛出。
+ ///
+ private static void ValidateInstallationPhase(IArchitecture architecture)
+ {
+ if (architecture is not Architecture concreteArchitecture)
+ {
+ return;
+ }
+
+ if (concreteArchitecture.CurrentPhase != ArchitecturePhase.None)
+ {
+ throw new InvalidOperationException(
+ "GameConfigModule must be installed before the architecture enters BeforeUtilityInit.");
}
}
diff --git a/GFramework.Game/Config/YamlConfigLoader.cs b/GFramework.Game/Config/YamlConfigLoader.cs
index 012caa32..15e38b1b 100644
--- a/GFramework.Game/Config/YamlConfigLoader.cs
+++ b/GFramework.Game/Config/YamlConfigLoader.cs
@@ -69,7 +69,8 @@ public sealed class YamlConfigLoader : IConfigLoader
foreach (var registration in _registrations)
{
cancellationToken.ThrowIfCancellationRequested();
- loadedTables.Add(await registration.LoadAsync(_rootPath, _deserializer, cancellationToken));
+ loadedTables.Add(
+ await registration.LoadAsync(_rootPath, _deserializer, cancellationToken).ConfigureAwait(false));
}
CrossTableReferenceValidator.Validate(registry, loadedTables);
@@ -442,7 +443,8 @@ public sealed class YamlConfigLoader : IConfigLoader
if (!string.IsNullOrEmpty(SchemaRelativePath))
{
var schemaPath = Path.Combine(rootPath, SchemaRelativePath);
- schema = await YamlConfigSchemaValidator.LoadAsync(Name, schemaPath, cancellationToken);
+ schema = await YamlConfigSchemaValidator.LoadAsync(Name, schemaPath, cancellationToken)
+ .ConfigureAwait(false);
referencedTableNames = schema.ReferencedTableNames;
}
@@ -463,7 +465,7 @@ public sealed class YamlConfigLoader : IConfigLoader
string yaml;
try
{
- yaml = await File.ReadAllTextAsync(file, cancellationToken);
+ yaml = await File.ReadAllTextAsync(file, cancellationToken).ConfigureAwait(false);
}
catch (Exception exception)
{
@@ -987,8 +989,8 @@ public sealed class YamlConfigLoader : IConfigLoader
{
try
{
- await Task.Delay(_debounceDelay, reloadTokenSource.Token);
- await ReloadTableAsync(tableName, reloadTokenSource.Token);
+ await Task.Delay(_debounceDelay, reloadTokenSource.Token).ConfigureAwait(false);
+ await ReloadTableAsync(tableName, reloadTokenSource.Token).ConfigureAwait(false);
}
catch (OperationCanceledException) when (reloadTokenSource.IsCancellationRequested)
{
@@ -1018,7 +1020,7 @@ public sealed class YamlConfigLoader : IConfigLoader
}
var reloadLock = _reloadLocks[tableName];
- await reloadLock.WaitAsync(cancellationToken);
+ await reloadLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
@@ -1031,8 +1033,9 @@ public sealed class YamlConfigLoader : IConfigLoader
foreach (var affectedTableName in affectedTableNames)
{
cancellationToken.ThrowIfCancellationRequested();
- loadedTables.Add(await _registrations[affectedTableName].LoadAsync(_rootPath, _deserializer,
- cancellationToken));
+ loadedTables.Add(
+ await _registrations[affectedTableName].LoadAsync(_rootPath, _deserializer, cancellationToken)
+ .ConfigureAwait(false));
}
CrossTableReferenceValidator.Validate(_registry, loadedTables);
@@ -1125,4 +1128,4 @@ public sealed class YamlConfigLoader : IConfigLoader
}
}
}
-}
\ No newline at end of file
+}
diff --git a/GFramework.Game/Config/YamlConfigSchemaValidator.cs b/GFramework.Game/Config/YamlConfigSchemaValidator.cs
index f435c10a..381f321c 100644
--- a/GFramework.Game/Config/YamlConfigSchemaValidator.cs
+++ b/GFramework.Game/Config/YamlConfigSchemaValidator.cs
@@ -51,7 +51,7 @@ internal static class YamlConfigSchemaValidator
string schemaText;
try
{
- schemaText = await File.ReadAllTextAsync(schemaPath, cancellationToken);
+ schemaText = await File.ReadAllTextAsync(schemaPath, cancellationToken).ConfigureAwait(false);
}
catch (Exception exception)
{