问题
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