简体   繁体   中英

C# await tasks + infinite loop still freezing the UI

I am trying to get the proper 'structure' for monitoring the state of a game from external source(s) using (Tasks) async/await in order to run the tasks in an infinite loop, however the current way its written seems to just freeze up my UI.

What I have so far:

(in the "state machine" class)

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

Without the above "Task.Delay" line the UI completely freezes up. With the "Task.Delay" line it doesn't freeze up, but if I try to drag the window it skips back to where I began dragging it.

My assumption with the current code is that the 'await Task.Run()' executes and upon completion the 'await Task.Delay()' executes and then on completion returns to the beginning of the while(true) infinite loop. (ie. not running in parallel).

The CheckForStateChange() signature is as follows:

private void CheckForStateChange()
{
    // ... A bunch of code to determine and update the current state value of the object
}

Nothing special there, simple non-async method. I have read through lots of examples / questions here on StackOverflow and I used to have CheckForStateChange as returning a Task (with awaitable actions inside the method) and many other iterations of code (with the same results).

Finally I call the Start() method from the main win32 form (button) as follows:

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

I think the above code is the simplest form I have written the code structure in so far, but apparently its still not written 'properly'. Any help would be appreciated.

UPDATE: The publisher side (state machine class):

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

Where the StateText() method is just a temporary way of retrieving a 'text' representation of the current state (and is really a placeholder at this point until I organize it into a tidier struct)

IsRunning is purely a public bool.

And the handler in the UI thread:

private void OnGameStateChange(string stateText)
{
    // Game State Changed (update the status bar)
    labelGameState.Text = "State: " + stateText;
}

Why the UI freezes

In terms of the main question: you're already calling your CheckForStateChange via Task.Run , so there is no way that your CheckForStateChange will freeze the UI unless it includes calls which are marshalled back to the UI thread (ie Control.Invoke or SynchronizationContext.Post/Send used explicitly, or implicitly via a Task started on the UI TaskScheduler ).

The best place to start looking is your StateChange handlers (ie StateChangeHandler ). Also have a look at where the StateChange event is raised. You'll find thread marshalling code at one of these sites.

Other issues

You're passing the TaskScheduler pointing to the UI SynchronizationContext to the outer task. You're also passing in TaskCreationOptions.LongRunning . In simple terms you're telling the task factory to "start a task on a dedicated thread, and on the current thread". These two are mutually exclusive requirements and you can pretty safely drop them both.

If, as a result of the above, your outer task happens to execute on the UI thread, it won't really trip you up as the inner call is wrapped in Task.Run , but this probably isn't the behaviour you expect.

You are storing the result of Task.Factory.StartNew inside a task field or property. Note, however, that your Task.Factory.StartNew call returns a Task<Task> , so the saved Task instance will transition to completed state almost immediately unless you call Unwrap on it and get to the inner task. To avoid this entire mess, just use Task.Run to create the outer task (as it has Unwrap semantics built in). If you do that, you can ditch the inner Task.Run completely, like so:

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

Since you have a CancellationToken you need to be passing it to CheckForStateChange , and checking it periodically - otherwise it only gets checked once, when the Task is started, and then never again.

Note that I have also provided a different IsRunning implementation. Volatile state is hard to get right. If the framework is giving it to you for free, you should use it.

Final word

Overall this entire solution feels like a bit of a crutch for something that should be done more reactively - but I can think of scenarios where this sort of design is valid. I'm just not convinced that yours is really one of them.

EDIT: how to find what's blocking the UI

I'll get downvoted to oblivion for this, but here goes:

The sure way to find what's causing postbacks to the UI thread is to deadlock with it. There's plenty of threads here on SO telling you how to avoid that, but in your case - we'll cause it on purpose and you'll know exactly what calls you need to avoid when you're polling for changes - although whether or not it will be possible to avoid these calls, remains to be seen.

I've put a task.Wait instruction at the end of my code snippet. Provided that you call Start on the UI thread, that should cause a deadlock with something inside your CheckForStateChange , and you will know what it is that you need to work around.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM