C# async/await strange behavior in console app

我们两清 提交于 2019-12-19 16:33:11

问题


I built some async/await demo console app and get strange result. Code:

class Program
{
    public static void BeginLongIO(Action act)
    {
        Console.WriteLine("In BeginLongIO start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(1000);
        act();
        Console.WriteLine("In BeginLongIO end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
    }

    public static Int32 EndLongIO()
    {
        Console.WriteLine("In EndLongIO start... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(500);
        Console.WriteLine("In EndLongIO end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        return 42;
    }

    public static Task<Int32> LongIOAsync()
    {
        Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        var tcs = new TaskCompletionSource<Int32>();
        BeginLongIO(() =>
        {
            try { tcs.TrySetResult(EndLongIO()); }
            catch (Exception exc) { tcs.TrySetException(exc); }
        });
        Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        return tcs.Task;
    }

    public async static Task<Int32> DoAsync()
    {
        Console.WriteLine("In DoAsync start... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        var res = await LongIOAsync();
        Thread.Sleep(1000);
        Console.WriteLine("In DoAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        return res;
    }

    static void Main(String[] args)
    {
        ticks = DateTime.Now.Ticks;
        Console.WriteLine("In Main start... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        DoAsync();
        Console.WriteLine("In Main exec... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(3000);
        Console.WriteLine("In Main end... \t\t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
    }

    private static Int64 ticks;
}

The result bellow:

Maybe I do not fully understand what exactly makes await. I thought if the execution comes to await then the execution returns to the caller method and task for awaiting runs in another thread. In my example all operations execute in one thread and the execution doesn't returns to the caller method after await keyword. Where is the truth?


回答1:


This isn't how async-await works.

Marking a method as async doesn't create any background threads. When you call an async method it runs synchronously until an asynchronous point and only then returns to the caller.

That asynchronous point is when you await a task that haven't completed yet. When it does complete the rest of the method is scheduled to be executed. This task should represent an actual asynchronous operation (like I/O, or Task.Delay).

In your code there is no asynchronous point, there's no point in which the calling thread is returned. The thread just goes deeper and deeper and blocks on Thread.Sleep until these methods are completed and DoAsync returns.

Take this simple example:

public static void Main()
{
    MainAsync().Wait();
}

public async Task MainAsync()
{
    // calling thread
    await Task.Delay(1000);
    // different ThreadPool thread
}

Here we have an actual asynchronous point (Task.Delay) the calling thread returns to Main and then blocks synchronously on the task. After a second the Task.Delay task is completed and the rest of the method is executed on a different ThreadPool thread.

If instead of Task.Delay we would have used Thread.Sleep then it will all run on the same calling thread.




回答2:


To really understand this behaviour, you need to first understand what Task is and what async and await actually do to your code.

Task is the CLR representation of "an activity". It could be a method executing on a worker-pool thread. It could be an operation to retrieve some data from a database over a network. Its generic nature allows it to encapsulate many different implementations, but fundamentally you need to understand that it just means "an activity".

The Task class gives you ways to examine the state of the activity: whether it has completed, whether it has yet to start, whether it generated an error, etc. This modelling of an activity allows us to more easily compose programs which are built as a sequence of activities, rather than a sequence of method calls.

Consider this trivial code:

public void FooBar()
{
    Foo();
    Bar();
}

What this means is "execute method Foo, then execute method Bar. If we consider an implementation that returns Task from Foo and Bar, the composition of these calls is different:

public void FooBar()
{
    Foo().Wait();
    Bar().Wait();
}

The meaning is now "Start a task using the method Foo and wait for it to finish, then start a task using the method Bar and wait for it to finish." Calling Wait() on a Task is rarely correct - it causes the current thread to block until the Task completes and can cause deadlocks under some commonly-used threading models - so instead we can use async and await to achieve a similar effect without this dangerous call.

public async Task FooBar()
{
    await Foo();
    await Bar();
}

The async keyword causes execution of your method to be broken down into chunks: each time you write await, it takes the following code and generates a "continuation": a method to be executed as a Task after the awaited task completes.

This is different from Wait(), because a Task is not linked to any particular execution model. If the Task returned from Foo() represents a call over the network, there is no thread blocked, waiting for the result - there is a Task waiting for the operation to complete. When the operation completes, the Task is scheduled for execution - this scheduling process allows a separation between the definition of the activity and the method by which it is executed, and is the power in the use of tasks.

So, the method can be summarised as:

  • start the task Foo()
  • when that task completes start the task Bar
  • when that task completes indicate the method task has completed

In your console app, you aren't awaiting any Task that represents the pending IO operation, which is why you see a blocked thread - there is never an opportunity to set up a continuation which would execute asynchronously.

We can fix your LongIOAsync method to simulate your long IO in an asynchronous fashion by using the Task.Delay() method. This method returns a Task that completes after a specified period. This gives us the opportunity for an asynchronous continuation.

public static async Task<Int32> LongIOAsync()
{
    Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);

    await Task.Delay(1000);       

    Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
}



回答3:


Short answer is that LongIOAsync() is blocking. If you run this in a GUI program you will actually see the GUI freezes briefly - not the way async/await is supposed to work. Therefore the entire thing falls apart.

You need to wrap all the long running operations in a Task then directly await on that Task. Nothing should block during that.




回答4:


the line that actually runs something on a background thread is

   Task.Run( () => {  } ); 

In your example you are not awaiting that Task but that of a TaskCompletionSource

   public static Task<int> LongIOAsync()
   {
        var tcs = new TaskCompletionSource<Int32>();

        Task.Run ( () => BeginLongIO(() =>
        {
            try { tcs.TrySetResult(EndLongIO()); }
            catch (Exception exc) { tcs.TrySetException(exc); }
        }));

        return tcs.Task;    
   }

When awaiting LongIOAsync you are awaiting the the task from tcs witch is set from a background thread in a delegate given to Task.Run()

apply this change :

    public static Task<Int32> LongIOAsync()
    {
        Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        var tcs = new TaskCompletionSource<Int32>();

        Task.Run ( () => BeginLongIO(() =>
        {
            try { tcs.TrySetResult(EndLongIO()); }
            catch (Exception exc) { tcs.TrySetException(exc); }
        }));

        Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId);
        return tcs.Task;
    }

Alternately in this case you could of just awaited the returned from Task.Run() , TaskCompletionSource is for situations where you want to pass around the ability to set your Task as complete or other wise.



来源:https://stackoverflow.com/questions/31564204/c-sharp-async-await-strange-behavior-in-console-app

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!