我如何等待.Net中列表中第一个完成的异步任务?

Eya*_*yal 0 .net vb.net async-await

我的输入是位于Amazon S3服务器上的一长串文件.我想下载文件的元数据,计算本地文件的哈希值,并将元数据哈希值与本地文件的哈希值进行比较.

目前,我使用循环来异步启动所有元数据下载,然后在每个完成时,根据需要在本地文件上计算MD5并进行比较.这是代码(只是相关的行):

Dim s3client As New AmazonS3Client(KeyId.Text, keySecret.Text)
Dim responseTasks As New List(Of System.Tuple(Of ListViewItem, Task(Of GetObjectMetadataResponse)))
For Each lvi As ListViewItem In lvStatus.Items
    Dim gomr As New Amazon.S3.Model.GetObjectMetadataRequest
    gomr.BucketName = S3FileDialog.GetBucketName(lvi.SubItems(2).Text)
    gomr.Key = S3FileDialog.GetPrefix(lvi.SubItems(2).Text)
    responseTasks.Add(New System.Tuple(Of ListViewItem, Task(Of GetObjectMetadataResponse))(lvi, s3client.GetObjectMetadataAsync(gomr)))
Next
For Each t As System.Tuple(Of ListViewItem, Task(Of GetObjectMetadataResponse)) In responseTasks
    Dim response As GetObjectMetadataResponse = Await t.Item2
    If response.ETag.Trim(""""c) = MD5CalcFile(lvi.SubItems(1).Text) Then
        lvi.SubItems(3).Text = "Match"
        UpdateLvi(lvi)
    End If
Next
Run Code Online (Sandbox Code Playgroud)

我有两个问题:

  1. 我按照我制作它的顺序等待响应.我宁愿按照他们完成的顺序处理它们,以便让它们更快.

  2. MD5计算是长且同步的.我尝试将其设为异步,但进程已锁定.我认为MD5任务被添加到.Net的任务列表的末尾,并且在完成所有下载之前它没有运行.

理想情况下,我会在响应到达时处理响应,而不是按顺序处理,并且MD5是异步的但有机会运行.

编辑:

合并WhenAll,它现在看起来像这样:

Dim s3client As New Amazon.S3.AmazonS3Client(KeyId.Text, keySecret.Text)
Dim responseTasks As New Dictionary(Of Task(Of GetObjectMetadataResponse), ListViewItem)
    For Each lvi As ListViewItem In lvStatus.Items
        Dim gomr As New Amazon.S3.Model.GetObjectMetadataRequest
        gomr.BucketName = S3FileDialog.GetBucketName(lvi.SubItems(2).Text)
        gomr.Key = S3FileDialog.GetPrefix(lvi.SubItems(2).Text)
        responseTasks.Add(s3client.GetObjectMetadataAsync(gomr), lvi)
    Next
    Dim startTime As DateTimeOffset = DateTimeOffset.Now
    Do While responseTasks.Count > 0
        Dim currentTask As Task(Of GetObjectMetadataResponse) = Await Task.WhenAny(responseTasks.Keys)
        Dim response As GetObjectMetadataResponse = Await currentTask
        If response.ETag.Trim(""""c) = MD5CalcFile(lvi.SubItems(1).Text) Then
            lvi.SubItems(3).Text = "Match"
            UpdateLvi(lvi)
        End If
    Loop
    MsgBox((DateTimeOffset.Now - startTime).ToString)
Run Code Online (Sandbox Code Playgroud)

只要MDSCalcFile完成,UI就会暂时锁定.整个循环大约需要45秒,第一个文件的MD5结果在启动后的1秒内发生.

如果我将行更改为:

        If response.ETag.Trim(""""c) = Await Task.Run(Function () MD5CalcFile(lvi.SubItems(1).Text)) Then
Run Code Online (Sandbox Code Playgroud)

完成MD5CalcFile后,UI不会锁定.整个循环大约需要75秒,从45秒开始,第一个文件的MD5结果在等待40秒后发生.

EDIT2:

我发现了一个适合我的解决方案.问题出在我的GetObjectMetadataAsync中.我写错了.注释中的错误版本的正确版本如下:

<System.Runtime.CompilerServices.Extension>
Function GetObjectMetadataAsync(a As AmazonS3Client, l As GetObjectMetadataRequest) As Task(Of GetObjectMetadataResponse)
    Return Task.Factory.FromAsync(AddressOf a.BeginGetObjectMetadata, AddressOf a.EndGetObjectMetadata, l, Nothing)
    'Return Task.Run(Function()
    '                    Try
    '                        Return a.GetObjectMetadata(l)
    '                    Catch ex As Amazon.S3.AmazonS3Exception
    '                        If ex.ErrorCode = "NoSuchKey" Then
    '                            Return Nothing
    '                        Else
    '                            Throw ex
    '                        End If
    '                    End Try
    '                End Function)
End Function
Run Code Online (Sandbox Code Playgroud)

我不知道为什么将一个同步版本放入一个线程或使用FromAsync,这很重要,但显然后者看起来更好,测试显示它要快得多.

Ste*_*ary 7

您可以WhenAny在完成任务结果时使用它们来处理:

while (responseTasks.Length > 0)
{
  var completedTask = await Task.WhenAny(responseTasks);
  responseTasks.Remove(completedTask);
  var response = await completedTask;
  ...
}
Run Code Online (Sandbox Code Playgroud)

(对不起,对于C#来说,我的VB语法是正确的太长了).

有关该主题的完整讨论,请参阅Stephen Toub关于该主题的帖子.

另一个选项是TPL Dataflow,它允许您为数据构建"网格".对于此示例,Dataflow可能过度,但如果您的实际处理更复杂,它将非常有用.

就MD5而言,使其异步应该不是问题.基于异步I/O的任务(例如返回的任务GetObjectMetadataAsync)不使用线程池线程.我会尝试其他一些场景(比如自己异步运行MD5)然后发布另一个问题,如果没有明显的问题.