我正在努力寻找合适的结构'用于监视来自外部源的游戏状态使用(任务)async / await以便在无限循环中运行任务,但是其编写的当前方式似乎只是冻结了我的UI。
到目前为止我所拥有的:
(在"状态机"类中)
// Start monitoring the game state for changes
public void Start()
{
tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
IsRunning = true;
task = Task.Factory.StartNew(async () =>
{
while (true)
{
await Task.Run(()=>CheckForStateChange());
await Task.Delay(1000); // Pause 1 second before checking state again
}
}, token, TaskCreationOptions.LongRunning, TaskScheduler.FromCurrentSynchronizationContext());
}
没有上述" Task.Delay" UI完全冻结了。使用" Task.Delay"它没有冻结,但如果我试图拖动窗口,它会跳回到我开始拖动它的位置。
我对当前代码的假设是“等待Task.Run()'执行并在完成后等待Task.Delay()'执行然后在完成时返回到while(true)无限循环的开头。 (即不是并行运行)。
CheckForStateChange()签名如下:
private void CheckForStateChange()
{
// ... A bunch of code to determine and update the current state value of the object
}
没有什么特别的,简单的非异步方法。我已经在StackOverflow上阅读了很多示例/问题,我曾经将CheckForStateChange作为返回一个Task(在方法中有等待的动作)和许多其他代码迭代(具有相同的结果)。
最后,我从主win32表单(按钮)调用Start()方法,如下所示:
private void btnStartSW_Click(object sender, EventArgs e)
{
// Start the subscription of the event handler
if(!state.IsRunning)
{
state.StateChange += new SummonersWar.StateChangeHandler(OnGameStateChange);
state.Start();
}
}
我认为上面的代码是迄今为止我编写代码结构的最简单的代码,但显然它仍然没有写好'正确'。任何帮助将不胜感激。
更新: 发布者方(状态机类):
// ------ Publisher of the event ---
public delegate void StateChangeHandler(string stateText);
public event StateChangeHandler StateChange;
protected void OnStateChange() // TODO pass text?
{
if (StateChange != null)
StateChange(StateText());
}
StateText()方法只是一种检索“文本”的临时方法。当前状态的表示(此时它实际上是一个占位符,直到我将它组织成一个更整洁的结构)
IsRunning纯粹是一个公共布尔。
UI线程中的处理程序:
private void OnGameStateChange(string stateText)
{
// Game State Changed (update the status bar)
labelGameState.Text = "State: " + stateText;
}
答案 0 :(得分:2)
为什么用户界面冻结
就主要问题而言:您已经通过CheckForStateChange
致电Task.Run
,因此无法您的CheckForStateChange
会冻结UI,除非它包含编组回UI线程的调用(即显式使用Control.Invoke
或SynchronizationContext.Post/Send
,或通过UI Task
上启动的TaskScheduler
隐式调用。 / p>
开始寻找的最佳位置是StateChange
处理程序(即StateChangeHandler
)。另请查看StateChange
事件的引发位置。您可以在其中一个站点找到线程编组代码。
其他问题
您将指向UI TaskScheduler
的{{1}}传递给外部任务。你也传递了SynchronizationContext
。简单来说,您告诉任务工厂“在专用线程和当前线程上启动任务”。这两个是相互排斥的要求,你可以非常安全地放弃它们。
如果由于上述原因,您的外部任务恰好在UI线程上执行,那么内部调用将包含在TaskCreationOptions.LongRunning
中,它不会真正使您失望,但这可能不是你期望的行为。
您将Task.Run
的结果存储在Task.Factory.StartNew
字段或属性中。不过请注意,您的task
调用会返回Task.Factory.StartNew
,因此保存的Task<Task>
实例几乎会立即转换为已完成状态,除非您在其上调用Task
并转到内心的任务。为了避免这种混乱,只需使用Unwrap
来创建外部任务(因为它内置了Task.Run
语义)。如果你这样做,你可以完全抛弃内部Unwrap
,如下:
Task.Run
由于你有一个public bool IsRunning
{
get
{
return task.Status == TaskStatus.Running;
}
}
public void Start()
{
tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
task = Task.Run(async () =>
{
while (true)
{
CheckForStateChange(token);
token.ThrowIfCancellationRequested();
await Task.Delay(1000); // Pause 1 second before checking state again
}
}, token);
// Uncomment this and step through `CheckForStateChange`.
// When the execution hangs, you'll know what's causing the
// postbacks to the UI thread and *may* be able to take it out.
// task.Wait();
}
,你需要将它传递给CancellationToken
,并定期检查它 - 否则只会检查一次,CheckForStateChange
启动时,然后永远不会试。
请注意,我还提供了不同的Task
实现。挥发性状态很难做到。如果框架是免费提供给您的,您应该使用它。
最后一句话
总的来说,整个解决方案对于应该更加反应性的事情感觉有点过分 - 但我可以想到这种设计有效的场景。我只是不相信你的确是其中之一。
编辑:如何找到阻止用户界面的内容
我会为此遗忘,但这里有:
找到导致UI线程回发的原因的确定方法是使用它进行死锁。这里有很多线索告诉你如何避免这种情况,但在你的情况下 - 我们会故意这样做,你会确切地知道你在轮询变化时需要避免哪些呼叫 - 尽管是否是可以避免这些电话,还有待观察。
我在代码片段末尾添加了IsRunning
条指令。如果您在UI线程上调用task.Wait
,这会导致Start
内的某事出现死锁,并且您将知道需要解决的问题。