9

This question is related to this one (Using C# 5 async to wait for something that executes over a number of game frames).

Context

When Miguel de Icaza first presented C# 5 async framework for games at Alt-Dev-Conf 2012, I really loved the idea of using async and await to handle "scripts" (so to speak, because they are in c#, and in my case, compiled---just in time but compiled anyway) in games.

Upcoming Paradox3D game engine seems to rely on the async framework to handle scripts too, but from my point of view, there is a real gap between the idea and the implementation.

In the linked related question, someone uses await to make a NPC perform a sequence of instructions while the rest of the game is still running.

Idea

I want to go a step further and allow a NPC to perform several actions at the same time, while expressing those actions in a sequential manner. Something along the lines of:

class NonPlayableCharacter 
{
    void Perform()
    {
        Task walking = Walk(destination); // Start walking
        Task sleeping = FallAsleep(1000); // Start sleeping but still walks
        Task speaking = Speak("I'm a sleepwalker"); // Start speaking
        await walking; // Wait till we stop moving.
        await sleeping; // Wait till we wake up.
        await speaking; // Wait till silence falls
    }
}

To do so, I used Jon Skeet's as-wonderful-as-ever answer from the related question.

Implementation

My toy implementation consists of two files, NPC.cs and Game.cs NPC.cs:

using System;
using System.Threading.Tasks;

namespace asyncFramework
{
    public class NPC
    {
        public NPC (int id)
        {
            this.id = id;

        }

        public async void Perform ()
        {
                    Task babbling = Speak("I have a superpower...");
            await Speak ("\t\t\t...I can talk while talking!");
            await babbling; 
            done = true;
        }

        public bool Done { get { return done; } }

        protected async Task Speak (string message)
        {
            int previousLetters = 0;
            double letters = 0.0;
            while (letters < message.Length) {
                double ellapsedTime = await Game.Frame;
                letters += ellapsedTime * LETTERS_PER_MILLISECOND;
                if (letters - previousLetters > 1.0) {
                    System.Console.Out.WriteLine ("[" + this.id.ToString () + "]" + message.Substring (0, (int)Math.Floor (Math.Min (letters, message.Length))));
                    previousLetters = (int)Math.Floor (letters);
                }
            }
        }

        private int id;
        private bool done = false;
        private readonly double LETTERS_PER_MILLISECOND = 0.002 * Game.Rand.Next(1, 10);
    }
}

Game.cs:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace asyncFramework
{
    class Game
    {
        static public Random Rand { 
            get { return rand; }
        }

        static public Task<double> Frame {
            get { return frame.Task; }
        }

        public static void Update (double ellapsedTime)
        {
            TaskCompletionSource<double> previousFrame = frame; // save the previous "frame"
            frame = new TaskCompletionSource<double> (); // create the new one
            previousFrame.SetResult (ellapsedTime); // consume the old one
        }

        public static void Main (string[] args)
        {
            int NPC_NUMBER = 10; // number of NPCs 10 is ok, 10000 is ko
            DateTime currentTime = DateTime.Now; // Measure current time
            List<NPC> npcs = new List<NPC> (); // our list of npcs
            for (int i = 0; i < NPC_NUMBER; ++i) { 
                NPC npc = new NPC (i); // a new npc
                npcs.Add (npc); 
                npc.Perform (); // trigger the npc actions
            }
            while (true) { // main loop
                DateTime oldTime = currentTime;
                currentTime = DateTime.Now;
                double ellapsedMilliseconds = currentTime.Subtract(oldTime).TotalMilliseconds; // compute ellapsedmilliseconds
                bool allDone = true;
                Game.Update (ellapsedMilliseconds); // generate a new frame
                for (int i = 0; i < NPC_NUMBER; ++i) {
                    allDone &= npcs [i].Done; // if one NPC is not done, allDone is false
                }
                if (allDone) // leave the main loop when all are done.
                    break;
            }
            System.Console.Out.WriteLine ("That's all folks!"); // show after main loop
        }

        private static TaskCompletionSource<double> frame = new TaskCompletionSource<double> ();
        private static Random rand = new Random ();
    }
}

This is quite a straightforward implementation!

Problem

However, it doesn't seem to work as expected.

More precisely, with NPC_NUMBER at 10, 100 or 1000, I have no problem. But at 10,000 or above, the program doesn't complete anymore, it write "speaking" lines for a while, then nothing more gets on Console. While I'm not thinking of having 10,000 NPCs in my game at once, they also won't writeline silly dialogs, but also move, animate, load textures and so on. So I'd like to know what is wrong with my implementation and if I have any chance of fixing it.

I must precise that the code is running under Mono. Also, the "problematic" value could be different at your place, it can be a computer specific thing. If the problem can't seem to be reproduced under .Net, I will try it under Windows.

EDIT

In .Net, it runs up to 1000000, although it requires time to initialise, it may be a Mono specific problem. Debugguer data tell me that there are indeed NPCs that aren't done. No info as to why yet, sadly.

EDIT 2

Under Monodevelop, launching the application without a debugger seems to correct the problem. No idea as to why however...

End word

I realise this is a really, really lengthy question, and I hope you will take the time to read it, I'd really like to understand what I did wrong.

Thank you very much in advance.

Community
  • 1
  • 1
dureuill
  • 2,526
  • 17
  • 24
  • 2
    I think you should try this on a non-Mono implementation for comparison to exclude platform-based issues. – Dave Swersky Feb 18 '14 at 19:12
  • Will do as soon as Windows finishes rebooting. – dureuill Feb 18 '14 at 19:15
  • Have you tried debugging your main loop that updates the game state? There is the risk that the calculated time deltas (`ellapsedMilliseconds`) per iteration step may turn out to be 0 or too small to make your `Speak` method print new output. You should artificially enforce some kind of locked frame rate and keep that delta the same amount per iteration step, while letting the main loop sleep for a bit if necessary. – tiguchi Feb 18 '14 at 19:31
  • 1
    Does this happen even in debug mode? – Jon Senchyna Feb 18 '14 at 20:25
  • Release and debug don't change a thing. However running in Monodevelop without debugging (CTRL+F5) seems to fix the problem. – dureuill Feb 18 '14 at 20:37
  • If you can consistently reproduce it in debug, I recommend running your program there, waiting until it stops writing, and then throwing a breakpoint down on your `if (allDone)` line to see what that variable is being set to. – Jon Senchyna Feb 18 '14 at 20:41
  • allDone is false because several NPCs have `done` at false. – dureuill Feb 18 '14 at 20:52
  • Try setting your debugger to catch any exceptions when they are thrown and start up the application in debug mode. It's possible that something in one of your threads is causing it to generate an exception and fail. I'm not sure how this would be handled by `async-await`. – Jon Senchyna Feb 18 '14 at 20:57
  • @JonSenchyna I don't know how to do that in Monodevelop – dureuill Feb 18 '14 at 20:59
  • @dureuill Forget that; throw a breakpoint in the `Speak` method after your `NPC`s stop writing to the console, specifically inside your `if (letters - previousLetters > 1.0)` statement. If it doesn't get hit after a few seconds, throw another breakpoint on the line before it. – Jon Senchyna Feb 18 '14 at 21:06
  • If I insert the breakpoints after the console stops writing, it is not hit. Nor does *any* breakpoint in the method, for that matter. Maybe we should move to chat, if that's ok for you? – dureuill Feb 18 '14 at 21:10
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/47795/discussion-between-dureuill-and-jon-senchyna) – dureuill Feb 18 '14 at 21:11
  • I managed to run this (and reproduce the issue) in LINQPAd on a machine with .NET 4.5. It definitely looks like an issue with the console (possibly due to the large amount of text written to it). If I remove the writes to the Console, or add a call to clear the console every now and then, it finishes. – Jon Senchyna Feb 19 '14 at 04:18

2 Answers2

2

There is one important point about TaskCompletionSource.SetResult: the continuation callback triggered by SetResult is normally synchronous.

This is especially true for a single-threaded application with no synchronization context object installed on its main thread, like yours. I could not spot any true asynchronicity in your sample app, anything that would cause a thread switch, e.g. await Task.Delay(). Essentially, your use of TaskCompletionSource.SetResult is similar to synchronously firing game loop events (which are handled with await Game.Frame).

The fact that SetResult may (and usually does) complete synchronously is often overlooked, but it may cause implicit recursion, stack overflow and deadlocks. I just happened to answer a related question, if you're interested in more details.

That said, I could not spot any recursion in your app, either. It's hard to tell what's confusing Mono here. For the sake of experiment, try doing periodic garbage collection, see if that helps:

Game.Update(ellapsedMilliseconds); // generate a new frame
GC.Collect(0, GCCollectionMode.Optimized, true);

Updated, try to introduce the actual concurrency here and see if this changes anything. The easiest way would be to change the Speak method like this (note await Task.Yield()):

protected async Task Speak(string message)
{
    int previousLetters = 0;
    double letters = 0.0;
    while (letters < message.Length)
    {
        double ellapsedTime = await Game.Frame;

        await Task.Yield();
        Console.WriteLine("Speak on thread:  " + System.Threading.Thread.CurrentThread.ManagedThreadId);

        letters += ellapsedTime * LETTERS_PER_MILLISECOND;
        if (letters - previousLetters > 1.0)
        {
            System.Console.Out.WriteLine("[" + this.id.ToString() + "]" + message.Substring(0, (int)Math.Floor(Math.Min(letters, message.Length))));
            previousLetters = (int)Math.Floor(letters);
        }
    }
}
Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • No help from periodic garbage collection, sorry! – dureuill Feb 19 '14 at 18:23
  • @dureuill, I suggest you report this as a bug to Xamarin. However, check my update about introducing concurrency. – noseratio Feb 20 '14 at 01:26
  • Very late answer, but I tried your update about introducing concurrency. It raises a `System.InvalidOperationException` at the end of the `Speak` method with the message "The task has already completed" after running for a while. At this point, and since only runs in the debugguer suffer from the bug, I think the best I can do is report to Xamarin. – dureuill May 01 '14 at 19:37
1

Not sure if this is related, but this line stood out to me:

allDone &= npcs [i].Done; // if one NPC is not done, allDone is false

I would recommend awaiting on your Perform method. Since you want all NPCs to run asynchronously, add their Perform Task to a list and use Task.WaitAll(...) for completion.

In turn you could do something like this:

var scriptList = new List<Task>(npcs.Count);

for (int i = 0; i < NPC_NUMBER; ++i) 
{
  var scriptTask = npcs[i].Perform();
  scriptList.Add(scriptTask);

  scriptTask.Start();
}

Task.WaitAll(scriptList.ToArray());

Just some food for thought.

I've used the await/async keywords with the Mono Task library without issue, so I would not be so quick to jump to blame Mono.

Erik
  • 12,730
  • 5
  • 36
  • 42
  • I'm not blaming anything, I love Mono. I just try both platform to make sure the matter is not platform related. I will test your answer in a moment – dureuill Feb 18 '14 at 19:26
  • @SiLo, all NPCs run synchronously in @dureuill's code, it's single threaded ([more details](http://stackoverflow.com/a/21867740/1768303)). Doing `Task.WaitAll(scriptList)` would just cause a deadlock. – noseratio Feb 19 '14 at 12:33