【问题标题】:Locking on List Produces Deadlock锁定列表会产生死锁
【发布时间】:2020-07-31 20:42:59
【问题描述】:

我正在开发一个使用 多线程C# WPF 游戏,我遇到了一个错误,当我在向操场添加一个新实体。

我有 2 个任务运行每个刻度,使游戏正常运行。第一个是执行所有计算并运行模拟的那个。第二个用于在每个滴答中更新 renderer

此代码在游戏开始时运行:

Task.Run(() =>
{
    while (state != GameLevelState.Ended)
    {
        Thread.Sleep(1000 / 60);
        if (state == GameLevelState.Paused)
            continue;
        Task.Run(RunSimulation);
        Task.Run(UpdateRenderer);
    }
});

问题从存储它们的实体列表开始,因为我需要使用我的两个任务来访问它,并且我还需要在单击按钮时创建一个新实体。当UI线程想要检查实体List中是否已经存在另一个相同类型的实体时,就会出现死锁。

这是获取实体的代码:

public List<GameEntity> GetGameEntities(Func<GameEntity, bool> predicate = null)
{
    if (predicate == null)
        predicate = (GameEntity e) => true;

    List<GameEntity> selected = new List<GameEntity>();

    // this is where the execution stops
    lock (Entities)
    {
        foreach (var entityList in Entities)
            selected.AddRange(entityList.Value.Where(predicate));
        return selected;
    }
}

因此,当我在添加实体时尝试获取相同类型的实体时,执行会在锁上停止。 我的理论是 UI 线程找不到开口,因为其他 2 个线程/任务不断锁定实体列表。 (他们只在迭代列表时锁定列表)

(我还应该指出,如果不理会这两个任务/线程基本上永远完美运行。 它仅在尝试从 UI 线程创建/添加实体时停止)

这是任务执行的相关代码:

private void RunSimulation()

/* ... */
lock (Entities)
{
    foreach (var entityList in Entities.ToArray())
        foreach (GameEntity entity in entityList.Value.ToArray())
            if (!entity.Tick())
            {
                entityList.Value.Remove(entity);
                Application.Current.Dispatcher.Invoke(() => renderer.RemoveEntity(entity));
            }
}
/* ... */

private void UpdateRenderer()

/* ... */
lock (Entities)
{
    foreach (var entityList in Entities)
        foreach (GameEntity entity in entityList.Value.ToArray())
            Application.Current.Dispatcher.Invoke(() => renderer.DrawEntity(entity));
}
/* ... */

我的问题是: 我该如何解决这个问题?

我是否应该复制 List 并遍历它,以便 List 仅在复制时被锁定?或者我应该将 entity-adding-process 委托给执行所有计算的线程吗?

非常感谢任何帮助!

【问题讨论】:

  • 切勿在没有await 的情况下致电Task.Run。将Thread.Sleep 替换为await Task.Delay
  • 我之前试过Task.Delay,但它什么也没做。 while 尽可能快地运行,并且仅在几秒钟内代码就死了,内存使用量很大。我正在使用Thread.Sleep 创建一个相对稳定的临时计时器。
  • 可能是因为你没有等待它。

标签: c# wpf multithreading list iteration


【解决方案1】:

不确定这是否能解决您的问题,但它肯定有助于控制您生成的任务。在当前正在运行的任务完成之前,不会启动新的循环。它使用awaitTask.Delay 而不是Thread.Sleep

Task.Run(async () =>
{
    while (state != GameLevelState.Ended)
    {
        var delayTask = Task.Delay(1000 / 60);
        if (state != GameLevelState.Paused)
        {
            var task1 = Task.Run(RunSimulation);
            var task2 = Task.Run(UpdateRenderer);
            await Task.WhenAll(task1, task2);
        }
        await delayTask;
    }
});

【讨论】:

  • @Clemens 确实如此。但它也没有伤害。
  • 如果它不被包裹在 Task.Run 中,while 将在 UI 线程上(是的,这段代码目前是同步的,这就是我使用 Task.Run 的原因)
  • 但是不等待它仍然是错误的。除了检查循环条件state != GameLevelState.Ended 之外什么都没有,它实际上 在 UI 线程中运行。其他一切都在等待异步调用。
  • @SomaBalazsi 我通过添加初始建议的同步版本更新了我的答案。
  • 谢谢@TheodorZoulias,我一定会试一试的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-12-28
  • 2020-03-11
  • 2015-03-18
  • 1970-01-01
  • 2013-03-06
  • 1970-01-01
相关资源
最近更新 更多