2

I'm working on a C# WPF game that uses multi-threading, and I encountered a bug, where the game would just suddenly stop, when I'm adding a new entity to the playground.

I have 2 Tasks running each tick that makes the game work. The first is the one doing all the calculations and running the simulation. The second is used to update the renderer in every tick.

This code is run when the game starts:

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

The problem start with the List of entities in which they're stored in, as I need to access it with both of my Task, and also I need to create a new entity when a button is clicked. The deadlock appears when the UI thread wants to check, if there is already another entity of the same type in the entity List.

Here's the code that get the entities:

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;
    }
}

So when I try to get the entities of the same type when adding an entity, execution stops on the lock. My theory is that the UI thread can't find an opening, because the 2 other threads/tasks are constantly locking the Entities List. (they only lock the list when they're iterating through it)

(I should also point out that the 2 tasks/threads run perfectly essentially forever if left alone. It only stops when trying to create/add an entity from the UI thread)

Here's the relevant code that the Tasks perform:

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));
}
/* ... */

My question is: How can I fix this issue?

Should I make a copy of the List and iterate through it, so the List is only locked for the time of copying? or Should I delegate the entity-adding-process to the thread doing all the calculations?

Any help is greatly appreciated!

Jason Aller
  • 3,541
  • 28
  • 38
  • 38
  • 2
    Never call `Task.Run` without `await`. Replace `Thread.Sleep` by `await Task.Delay`. – Clemens Apr 18 '20 at 09:37
  • I tried `Task.Delay` before and it did nothing. The while run as fast as it could, and in only a couple of second the code died, with to much memory usage. I'm using `Thread.Sleep` to create a makeshift timer that is relatively stable. – palatinus-sb Apr 18 '20 at 10:37
  • Probably because you did not await it. – Clemens Apr 18 '20 at 11:15

1 Answers1

0

Not sure if this will fix your problem, but it will certainly help in keeping your spawned tasks under control. A new loop will not be started before the completion of the currently running tasks. It uses await and Task.Delay instead of 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;
    }
});
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • @Clemens indeed. But it doesn't hurt either. – Theodor Zoulias Apr 18 '20 at 10:00
  • If it wouldn't be wrapped inside a Task.Run, the while would be on the UI thread (yes, this code is currently syncronous, that's why I used a Task.Run) – palatinus-sb Apr 18 '20 at 10:13
  • But it's still wrong not to await it. And there would be nothing except the check for the loop condition `state != GameLevelState.Ended` that would *actually* run in the UI thread. Everything else is awaited async calls. – Clemens Apr 18 '20 at 10:17
  • @Soma Balazsi it is OK to run the loop in the UI thread, as long as you don't block the UI thread with `Thread.Sleep`. What is concerning with your code, beyond spawning fire-and-forget tasks, is that you are invoking the dispatcher too often. If I remember correctly, there is a soft limit of ~100 invocations per second, that a UI app shouldn'd surpass in order to avoid having performance issues. – Theodor Zoulias Apr 18 '20 at 10:22
  • @TheodorZoulias Ahh I see, thank you for noticing this. I was actually experimenting around which would be better: dispatch the whole loop / only dispatch the actual stuff that need to be dispatched. – palatinus-sb Apr 18 '20 at 10:35
  • @Soma Balazsi what do you think that is the most expensive part of your program, the calculations of the simulation or the updating of the UI? If it's the UI, then you can't make it faster by mutlithreading, because the UI thread is the only thread allowed to touch the UI elements. If it's 50-50, then you could have one thread calculating the next frame while the UI thread is updated with the previously calculated frame. This way you could eliminate the need for synchronization (locking) by giving to each thread a different set of data to work with. – Theodor Zoulias Apr 18 '20 at 11:20
  • @TheodorZoulias Both of them can be heavy when having a lot of entities (from my initial tests). But that will change as the program will evolve. Hovever this calculate-the-next-frame idea sounds interesting and useful. I'll investigate it. – palatinus-sb Apr 18 '20 at 13:07
  • @TheodorZoulias Also, I just tried out your solution. It seems to work, no sign of deadlock or other bugs. So thank you. Hovever `Task.Delay` seems slower and more unstable than `Thread.Sleep`. Nonetheless, I marked it as the correct answer. – palatinus-sb Apr 18 '20 at 13:43
  • 1000/60 is ~17msec, which is a quite small time-span, near the default [time resolution](https://stackoverflow.com/questions/3744032/why-are-net-timers-limited-to-15-ms-resolution) of Windows. So it is possible that the `Task.Delay` has different micro-behavior than `Thread.Sleep`. – Theodor Zoulias Apr 18 '20 at 14:31
  • Something else you could try is to increase the number of threads that are initially created by the `ThreadPool`, using the method [`SetMinThreads`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.threadpool.setminthreads). This may be useful if you are making heavy use of `Task.Run`, to avoid hiccups caused by thread-pool starvation at the early stages of the program's lifetime. – Theodor Zoulias Apr 18 '20 at 14:31
  • 1
    @SomaBalazsi I updated my answer by adding a synchronous version of the initial suggestion. – Theodor Zoulias Apr 18 '20 at 17:59
  • 1
    Thank you @TheodorZoulias, I will definatelly try it out. – palatinus-sb Apr 19 '20 at 19:46