C# async task completes before it's finished

拥有回忆 提交于 2020-08-20 05:56:26

问题


I'm developing a network application that receives data from a websocket, modifies it, and uploads it to a data service. Uploading the data takes some time and I'd like to hide that latency, uploading several messages at once. When an upload is complete I need to send an acknowledgement back through the websocket.

I've created a work queue to hold all of the outstanding tasks. This seems to be working well, so I haven't included it. But my upload task appears to be finishing before it's actually finished. Here's a trimmed down sample.

private async Task UploadDataAsync(string data, CancellationToken cancellationToken)
{
    Task uploadTask = new Task(async () =>
    {
        // Simulates uploading data
        await Task.Delay(5000, cancellationToken);
    });

    _ = uploadTask.ContinueWith(async (t1) =>
    {
        // Clean up the task
        await RemoveTask(t1);

        if (t1.IsCompletedSuccessfully)
            await SendAck(this, data, cancellationToken);
        else if (t1.IsFaulted)
            logger.LogError(t1.Exception, $"An error occurred while uploading {data}");
        else if (t1.IsCanceled)
            logger.LogWarning($"An upload task {data} was canceled");

    }, TaskScheduler.Default);

    await AddTask(uploadTask);
    uploadTask.Start();
}

In the preceding code the acknowledgement is sent before the data is uploaded. Surprisingly this seems to be because my Task uses an async lambda. For a reason I don't understand uploadTask completes when the upload is awaited. So I changed it to something like this:

private async Task UploadDataAsync(string data, CancellationToken cancellationToken)
{
    Task uploadTask = new Task(() =>
    {
        // Simulates uploading data
        Task.Delay(5000, cancellationToken).Wait();
    });

    _ = uploadTask.ContinueWith((t1) =>
    {
        // Clean up the task
        RemoveTask(t1).Wait();

        if (t1.IsCompletedSuccessfully)
            SendAck(this, data, cancellationToken).Wait();
        else if (t1.IsFaulted)
            logger.LogError(t1.Exception, $"An error occurred while uploading {data}");
        else if (t1.IsCanceled)
            logger.LogWarning($"An upload task {data} was canceled");

    }, TaskScheduler.Default);

    await AddTask(uploadTask);
    uploadTask.Start();
}

Now everything executes in the correct order, except when things go wrong or the operation is canceled (e.g. the server is shutdown). Now I'm dealing with AggregateExceptions and TaskCanceledExceptions.

This seems like it should be easier than I'm making it. Am I doing this wrong?

Edit Adding pseudocode that calls UploadDataAsync for context.

protected override async Task DoConnection(CancellationToken cancellationToken)
{
    while (_websocket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
    {
        // Simulates getting websocket data
        string result = await _websocket.ReceiveAsync();

        // This should only asynchronously wait for the upload task to get
        // created. It should not wait for the upload to complete.
        await OnDataReceived(result, cancellationToken);
    }
}

回答1:


The problem with the Task constructor having an async delegate is that the signature of the delegate is resolved to async void instead of async Task, so the asynchronous operation cannot be awaited (async void is useful mainly for event handlers, and harmful in most other cases). This happens because the Task constructor does not understand async delegates, meaning that it has no overload that accepts a Func<Task> argument.

There is a way to solve this problem without removing the Task constructor from your solution, although its usage is frowned upon by the experts. Instead of a non-generic Task you can use a generic Task<TResult>, with the TResult being a Task. In other words you can use a nested Task<Task>. The job of the outer task is to create the inner task, by executing the supplied async delegate. This is a CPU-bound operation that in most cases will have a very small duration. Essentially the outer task completes as soon as the code hits the first await¹ of the async delegate, and the rest of the work (that includes the high latency I/O operation) is represented by the inner task.

Task<Task> uploadTaskFactory = new Task<Task>(async () =>
{
    await Task.Delay(5000, cancellationToken); // Simulates an I/O operation
});

//...
uploadTaskFactory.Start();
Task uploadTask = await uploadTaskFactory; // takes essentially zero time

//...
await uploadTask; // takes 5 seconds

As you see using the Task<Task> constructor with an async delegate becomes rather awkward, so in general any alternative method should be preferred (especially when writing application code, for libraries it's okayish IMHO). Alternatives include the Task.Run that does understand async delegates, or, if you don't want to start the task immediately, to pass around Func<Task>s and invoke them at the right moment.

¹ To be precise: the first await of a non-completed awaitable.




回答2:


Right now, you call uploadTask.Start() and just continue without ever waiting for it to finish. (Even AddTask happens before you call uploadTask.Start()). You should await uploadTask rather than just launching it and moving on right away.



来源:https://stackoverflow.com/questions/60659668/c-sharp-async-task-completes-before-its-finished

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