17

await does not guarantee continuation on the same task for spawned tasks:

private void TestButton_Click(object sender, RoutedEventArgs e)
{
    Task.Run(async () =>
    {
        Debug.WriteLine("running on task " + Task.CurrentId);
        await Task.Delay(TimeSpan.FromMilliseconds(100));
        Debug.WriteLine("running on task " + Task.CurrentId);
    });
}

The output of this is:

running on task 1
running on task

so we can see that not only the execution has moved to another task, but also to the UI-thread. How can i create a dedicated task, and enforce await to always continue on this task? Long-running tasks don't do this either.

I have seen several SynchronizationContext implementations, but so far none of them worked, in this case because it uses threads and System.Threading.Thread is not available for uwp.

svick
  • 236,525
  • 50
  • 385
  • 514
Benni
  • 1,030
  • 2
  • 11
  • 18
  • May this can help you http://stackoverflow.com/questions/34000838/system-threading-thread-replacement-in-uwp-windows-10-mobile – Alexandr Sargsyan Mar 13 '17 at 08:50
  • 4
    You seem to be conflating the concepts of threads and tasks here - tasks are meant to be a higher, more "logical" level. There's no real concept of "continuing on the same task". – Damien_The_Unbeliever Mar 13 '17 at 08:51
  • I am not conflating these concepts - i would use threads if i could, but i cannot, so tasks are all i have. The UI thread enforces continuation on itself, so there has got to be some way to enforce it for spawned tasks too. – Benni Mar 13 '17 at 09:12
  • 1
    _"...same thread..."_ - If you really must dump the current thread for a given task, use `Environment.CurrentManagedThreadId`. Though in your example they will be the **same** –  Mar 13 '17 at 09:13
  • 1
    @Benni - but outside of the UI thread, threads are meant to be interchangable. *Why* do you believe that it's important to return to a *specific* other thread? What feature are you trying to use that stops working? – Damien_The_Unbeliever Mar 13 '17 at 09:13
  • Also, because you don't `await` the `Task` returned from `Task.Run()` you run the risk of the program exiting before the task completes. –  Mar 13 '17 at 09:15
  • I (want to) use one thread to consistently maintain the states of sessions of the signal protocol, and to access a websocket connection. The members are not threadsafe, thus i need either massive locking or to enforce my execution to stay in one task. – Benni Mar 13 '17 at 09:20
  • 1
    @MickyD even in this simple example, the `CurrentManagedThreadId` is not always the same, just test it. – Benni Mar 13 '17 at 09:21
  • 3
    @Benni: You only need "massive locking" if you are calling a non thread safe API from multiple threads at the same time. Unless the API is limited to be called from a single thread it shouldn't matter what thread you are calling from. If the API is limited to be called from a single thread then you need to carefully control that thread and using async and await is probably a bad idea. Also, you seem to be confusing threads with tasks. If you are concerned about the thread used to call your API then you shouldn't care about `Task.CurrentId`. – Martin Liversage Mar 13 '17 at 10:09
  • @Benni My mistake, the IDs _should_ be different. Again, use `CurrentManagedThreadId` not `Task.CurrentId` as the latter has nothing to do with threads –  Mar 13 '17 at 10:43
  • If i await multiple async calls in one task (i.e. executing two async void functions which call other async functions with await), and one of them is being scheduled to a different task, my task is effectively split into two - which threatens my thread safety. – Benni Mar 13 '17 at 10:48
  • If you _"await multiple async calls"_ that's **multiple tasks** not _"one"_. `async void` is bad, don't do it. –  Mar 13 '17 at 12:46
  • 1
    You have a to-do list. It says (1) set your alarm for ten minutes from now; while you're waiting, go do something else, (2) after the alarm goes off, mow the lawn. So you do that. As you are setting your alarm, someone asks you "what item are you on in your to-do list?" You say "one". During your ten minute wait, you make yourself a sandwich. When you continue doing the stuff on your to-do list after making the sandwich, you start mowing the lawn. Someone says again "where are you on your to-do list?" You say "two". Your question is "I want to say 'one' both times; how?" – Eric Lippert Mar 13 '17 at 13:26
  • 6
    Threads are workers. At no moment in this analogy was there a second worker hired. But tasks are just items on a to-do list; whether they are divided up amongst several workers, and how those workers cooperate to decide how they are divided up, does not ever change the fact that there is a big difference between the name of a worker, and the position of a task on the to-do list. – Eric Lippert Mar 13 '17 at 13:28
  • 5
    It's also very unclear to me why you are using Task.Run in an event handler, and why the returned task is not awaited. Both the code you've written and the question you're asking about it are bizarre. I suspect you have what we call an "XY problem". You have some wrong idea about how to solve the problem and you're asking us about that wrong solution, rather than asking for help solving the real problem. Use async-await as it was intended to be used: to build a workflow of tasks. – Eric Lippert Mar 13 '17 at 13:31
  • I am obviously *not* doing that in an event handler, this is an oversimplified example. I'd throw out tasks and async-await immediately if i could, but a) uwp does not allow you to use real threads and b) the networking libraries do not offer non-async methods, so i am forced to deal with it. By calling two async voids i am able to await two different methods (await read from socket and await output available), and to continue each *while being sure that the other is not running simultaneously*, if (and only) if i would be sure that each await returns to the original task. – Benni Mar 13 '17 at 14:25
  • 1
    Tasks are simply not something you *return to*. You're going to find it difficult to get an answer to your question while you persist in talking about tasks as things that are "returned to". – Eric Lippert Mar 13 '17 at 18:46
  • 1
    Async voids are a bad practice because they give the code which invoked the asynchronous operation *no ability to track the completion of the task*. If you are in a position where there are async void methods representing operations that must be sequenced in a workflow then you have a big problem. Find the entity which provided those badly-designed methods and encourage them to produce instead a task-returning method. – Eric Lippert Mar 13 '17 at 18:48

2 Answers2

12

so we can see that not only the execution has moved to another task, but also to the UI-thread.

No, it's not on the UI thread. It's just technically not on a task, either. I explain why this happens in my blog post on Task.CurrentId in async methods.

How can i create a dedicated task, and enforce await to always continue on this task? Long-running tasks don't do this either.

You're on the right track: you need a custom SynchronizationContext (or a custom TaskScheduler).

I have seen several SynchronizationContext implementations, but so far none of them worked, in this case because it uses threads and System.Threading.Thread is not available for uwp.

Try out mine. It should work on UWP 10.0.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks, i will give it a shot! Will look at the source code too as soon as i find the time. – Benni Mar 13 '17 at 19:54
3

Don't use Task.Run, just make the event handler async

 private async void TestButton_Click(object sender, RoutedEventArgs e)
 {
     await dedicated();
 }

 private async Task dedicated()
 {
     Console.WriteLine("running on task {0}", Task.CurrentId.HasValue ? Task.CurrentId.ToString() : "null");
     await Task.Delay(TimeSpan.FromMilliseconds(100));
     Console.WriteLine("running on task {0}", Task.CurrentId.HasValue ? Task.CurrentId.ToString() : "null");
 }

Read more about Task.CurrentId in Async Methods here:

So Task.CurrentId returns null because there is no task actually executing.

Reply to comments

  1. "It still runs on the UI thread, and not a spawned task."

The case with a thread pool thread is included in the link. In particular look at this example

static void Main(string[] args)
{
    var task = Task.Run(() => MainAsync());
    task.Wait();
    taskRun = task.Id.ToString();

    Console.WriteLine(beforeYield + "," + afterYield + "," + taskRun);
    Console.ReadKey();
}

static async Task MainAsync()
{
    beforeYield = Task.CurrentId.HasValue ? Task.CurrentId.ToString() : "null";
    await Task.Yield();
    afterYield = Task.CurrentId.HasValue ? Task.CurrentId.ToString() : "null";
}

Again, the clarification is

the null comes into play because the async method is first executed as an actual task on the thread pool. However, after its await, it resumes as a regular delegate on the thread pool (not an actual task).

  1. "my question is how to stop it from doing that"

This is an implementation detail of the async call, I can only quote the link again:

It’s likely that this behavior is just the result of the easiest and most efficient implementation.

So you can't and you should not stop it from doing that, as far as a truly async call is concerned.

What you describe as the expected behavior instead is equivalent to a Task.Run without await

 private void expected()
 {
     Task task = Task.Run(() =>
     {
         Console.WriteLine("Before - running on task {0} {1}", 
            Task.CurrentId.HasValue ? Task.CurrentId.ToString() : "null",
            Environment.CurrentManagedThreadId);
         Task.Delay(TimeSpan.FromMilliseconds(100)).Wait();
         Console.WriteLine("After - running on task {0} {1}", 
            Task.CurrentId.HasValue ? Task.CurrentId.ToString() : "null",
            Environment.CurrentManagedThreadId);
     });
 }

Or a nested Task.Run

 private void expected()
 {
     Task task = Task.Run(() =>
     {
         Console.WriteLine("Before - running on task {0} {1}",
             Task.CurrentId.HasValue ? Task.CurrentId.ToString() : "null",
             Environment.CurrentManagedThreadId);
         var inner = Task.Run( async () =>
             await Task.Delay(TimeSpan.FromMilliseconds(100)));
         inner.Wait();
         Console.WriteLine("After - running on task {0} {1}",
             Task.CurrentId.HasValue ? Task.CurrentId.ToString() : "null",
             Environment.CurrentManagedThreadId);
     });
 }

Output

Before - running on task 312 11
After - running on task 312 11
Before - running on task 360 11
After - running on task 360 11
Before - running on task 403 15
After - running on task 403 15