我正在编写一个回合制游戏,并且我一直在使用异步方法来正确控制连续动作。每当提示玩家选择要交互的对象时,游戏管理器都会为该玩家调用以下异步方法(在附加到玩家对象的脚本中):
public async Task<List<GameObject>> SubmitObjs(List<GameObject> options, int selN)
{
List<GameObject> result = new List<GameObject>();
centralTime.ToggleOutlines(options, 1, true);
while (result.Count < selN)
{
selection = new TaskCompletionSource<GameObject>(TaskCreationOptions.RunContinuationsAsynchronously);
GameObject obj = await selection.Task; //Awaited TCS here...
if (obj && options.Contains(obj))
if (result.Contains(obj))
{
result.Remove(obj);
centralTime.ToggleOutlines(new List<GameObject>() { obj }, 1, true);
}
else
{
result.Add(obj);
centralTime.ToggleOutlines(new List<GameObject>() { obj }, 2, true);
}
}
centralTime.OutlineOff();
return result;
}
为了允许玩家单击他们想要交互的对象,我也在相同的脚本中编写了
Update()
,如下所示:
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
GameObject DO = DetectValObj();
if(DO) selection.TrySetResult(DO); //TCS is Set here...
}
}
如您所见,异步
SubmitObjs()
应该等待 TrySetResult()
中的 Update
,以便当玩家单击该对象时,它会恢复提交玩家决定的对象的过程。
但是,真正发生的情况是,单击对象并运行
selection.TrySetResult()
不会恢复 SubmitObjs()
中的处理。我测试了 DetectValObjs()
,它正在正确地将 DO
传输到 TrySetResult()
,更令人沮丧的是,在十分之一的尝试中,SubmitObjs()
会恢复并处理传递的对象。
老实说,我认为我没有正确理解
TaskCompletionSource
和SetResult()
。我查阅了很多参考资料和示例,但我不明白为什么 TrySetResult()
仅在某些时候恢复等待的任务。
我也尝试完全放弃 TCS,将其替换为一个简单的私有
GameObject sel
,如果 Task.Yield()
中未分配 sel
,则使用 Update()
:
while (!sel)
await Task.Yield();
obj = sel;
这个版本结果更糟糕,因为我发现
Update()
这次根本没有被调用。
这里到底发生了什么,我该如何解决这个问题以使它们按预期工作?
+编辑(2024-01-07):我检查了
SynchronizationContext
和Update()
中的SubmitObjs()
是否不同,它们都在UnityEngine.UnitySynchronizationContext
下。事实证明,使 Update()
与 await Task.Yield()
异步也是无效的。
+编辑(2024-01-09):根据Anton Tykhyy的建议,我尝试将TaskCompletionSource的使用替换为System.Threading.Channels。检查
SubmitObjs()
和 Update()
是否使用相同的 Channel<GameObject>
实例表明它们实际上是不同的,即使在脚本范围内只有一个单一的通道引用。除了脚本中的声明之外,没有对 Channel 进行任何初始化(声明为 readonly
)。
问题不在于
TaskCompletionSource
本身,而在于共享变量 selection
,您正在从多个线程读取和写入该变量,而没有任何同步。没有什么可以保证 Update()
在您在 TrySetResult()
中分配的同一个 TaskCompletionSource
对象上调用 SubmitObjs()
。此外,当 SumbitObjs()
正在处理它已读取的更新时,可能会发生多次更新,从而导致更新丢失。您对代码应该如何工作的想法似乎适合生产者/消费者模式,因此对您来说最简单的解决方案是使用 System.Threading.Channels
而不是裸露的 TaskCompletionSource
。通道为您处理所有同步细节,并为您提供易于理解的抽象来使用。
// at top level
private readonly Channel<GameObject> selections = Channel.CreateUnbounded<GameObject>() ;
// Update()
if(DO) selections.Writer.TryWrite(DO);
// SubmitObjs()
while (result.Count < selN)
{
GameObject obj = await selections.Reader.ReadAsync();