Question

Given the following code in the DoWork() event of a BackgroundWorker object, how can the concept be converted to the Async/Await model?

I wish to execute multiple downloads simultaneously in order to maximize bandwidth. I also will need to retain the ability to increment a progress bar on the dialog.

Dim oChunks As SortedDictionary(Of String, Byte())
Dim oFiles As List(Of FileInfo)
Dim iChunk As Integer

oChunks = New SortedDictionary(Of String, Byte())
oFiles = Service.Client.GetChunks(Me.Upload.UploadId, Me.Target)

oFiles.ForEach(Sub(File As FileInfo)
                 Worker.ReportProgress((iChunk / Me.Upload.Chunks) * 100, "Downloading...")
                 oChunks.Add(File.Name, Service.Client.GetChunk(File, Me.Target))
                 iChunk += 1
               End Sub)

A C# answer is welcome; I can translate.

EDIT

Based on Ewan's sample code, I came up with the revision below.

But it's not working correctly:

  1. The downloads still run sequentially, not in parallel
  2. The UI blocks during execution
  3. The Download.Progress event doesn't fire

What am I missing?

Public Class Main
  Private Async Sub cmdSave_Click(Sender As Button, e As EventArgs) Handles cmdSave.Click
    Dim oChunks As SortedDictionary(Of String, Byte())
    Dim oFiles As List(Of FileInfo)
    Dim aData As Byte()

    Me.Upload = dgvUploads.CurrentRow.DataBoundItem

    pnlTarget.Enabled = False
    dlgSave.FileName = "{0}.zip".ToFormat(Me.Upload.UploadName)
    txtFile.Text = String.Empty

    If dlgSave.ShowDialog = DialogResult.OK Then
      Me.SetControlsEnabled(False)

      'bgwWorker.RunWorkerAsync(JobTypes.Save)

      oFiles = Await Me.GetFilesAsync(Me.Upload.UploadId, Me.Target)
      oChunks = Await Me.GetChunksAsync(oFiles)
      aData = Await Me.MergeChunksAsync(oChunks)

      File.WriteAllBytes(dlgSave.FileName, aData)

      Me.SetControlsEnabled(True)

      MsgBox("Download complete.", MsgBoxStyle.Information, Me.Text)
    End If
  End Sub



  Public Async Function GetFilesAsync(UploadId As Integer, Target As Targets) As Task(Of List(Of FileInfo))
    Dim oArgs As ProgressEventArgs

    oArgs = New ProgressEventArgs(0, "Initializing...", ProgressBarStyle.Marquee)
    Me.ReportProgress(Nothing, oArgs)

    Return Await Service.Client.GetFilesAsync(UploadId, Target)
  End Function



  Public Async Function GetChunksAsync(Files As List(Of FileInfo)) As Task(Of SortedDictionary(Of String, Byte()))
    Dim oDownload As Download
    Dim oChunks As SortedDictionary(Of String, Byte())
    Dim aChunks As Chunk()
    Dim oTasks As List(Of Task(Of Chunk))
    Dim oArgs As ProgressEventArgs

    prgProgress.Value = 0

    oArgs = New ProgressEventArgs(Files.Count, "Downloading...", ProgressBarStyle.Continuous)
    oTasks = New List(Of Task(Of Chunk))

    Files.ForEach(Sub(File)
                    oDownload = New Download

                    AddHandler oDownload.Progress, AddressOf ReportProgress

                    oTasks.Add(oDownload.GetChunkAsync(File, Me.Target, oArgs))
                  End Sub)

    aChunks = Await Task.WhenAll(oTasks.ToArray)
    oChunks = New SortedDictionary(Of String, Byte())

    aChunks.ToList.ForEach(Sub(Chunk)
                             oChunks.Add(Chunk.Name, Chunk.Data)
                           End Sub)

    Return oChunks
  End Function



  Public Async Function MergeChunksAsync(Chunks As SortedDictionary(Of String, Byte())) As Task(Of Byte())
    Dim oChunks As List(Of Byte())
    Dim iOffset As Integer
    Dim oArgs As ProgressEventArgs
    Dim aData As Byte()

    prgProgress.Value = 0

    oArgs = New ProgressEventArgs(Chunks.Count, "Saving...", ProgressBarStyle.Continuous)

    aData = Await Task.Run(Function()
                             oChunks = Chunks.Select(Function(Pair) Pair.Value).ToList

                             aData = New Byte(oChunks.Sum(Function(Chunk) Chunk.Length) - 1) {}

                             oChunks.ForEach(Sub(Chunk)
                                               Me.ReportProgress(Nothing, oArgs)

                                               Buffer.BlockCopy(Chunk, 0, aData, iOffset, Chunk.Length)

                                               iOffset += Chunk.Length
                                             End Sub)
                             Return aData
                           End Function)

    Return aData
  End Function



  Public Sub ReportProgress(Sender As Download, e As ProgressEventArgs)
    prgProgress.Maximum = e.Maximum
    prgProgress.Style = e.Style

    If e.Style = ProgressBarStyle.Marquee Then
      prgProgress.Value = 0
    Else
      prgProgress.Increment(1)
    End If

    lblStatus.Text = e.Status
  End Sub
End Class



Public Class Download
  Public Event Progress As EventHandler(Of ProgressEventArgs)

  Public Async Function GetChunkAsync(File As FileInfo, Target As Targets, Args As ProgressEventArgs) As Task(Of Chunk)
    Dim oChunk As Chunk

    oChunk = New Chunk
    oChunk.Name = File.Name
    oChunk.Data = Await Service.Client.GetChunkAsync(File, Target)

    RaiseEvent Progress(Me, Args)

    Return oChunk
  End Function
End Class



Public Class Chunk
  Public Property Name As String
  Public Property Data As Byte()
End Class



Public Class ProgressEventArgs
  Inherits EventArgs

  Public Sub New(Maximum As Integer, Status As String, Style As ProgressBarStyle)
    _Maximum = Maximum
    _Status = Status
    _Style = Style
  End Sub



  Public ReadOnly Property Maximum As Integer
  Public ReadOnly Property Status As String
  Public ReadOnly Property Style As ProgressBarStyle
End Class
Était-ce utile?

La solution

Maybe something like this:

public class Download
    {
        public EventHandler<int> Progress;
        public async Task GetData(string url)
        {
            int percentDone = 0;
            foreach (var chunk in chunks)
            {
                //get data
                //write data to disk?
                if(Progress != null)
                {
                    Progress(this, percentDone);
                }
            }
        }
    }

    public class ManyDownloads
    {
        public void ReportProgress(object sender, int progress)
        {
            //update the UI
        }
        public List<Download> Downloads { get; set; }
        public void DownloadAll(List<string> Urls)
        {
            this.Downloads = new List<Download>();
            foreach(var url in Urls)
            {
                var d = new Download();
                d.Progress += ReportProgress;
                d.GetData(url); //dont await will run async
                this.Downloads.Add(d); //keep hold of the download object so you can refernce it in ReportProgress if needed
            }

        }
    }

if you need the return value of the function you can add all the tasks to an array and use await Task.WhenAll or Task.WaitAll

public async Task DownloadAll(List<string> Urls)
{
    List<Task<string>> tasks = new List<Task<string>>();

    this.Downloads = new List<Download>();
    foreach(var url in Urls)
    {
        var d = new Download();
        d.Progress += ReportProgress;
        tasks.Add(d.GetData(url)); //dont await will run async
    }
    //tasks are all running in threads at this point
    await Task.WhenAll<string>(tasks.ToArray()); //wait for all the tasks to complete before continuing

    foreach(var t in tasks)
    {
        //save data
        File.WriteAllText(t.Result, "filename.txt"); //todo::check for exceptions
    }
}

Update re comments:

The confusion is that the async keyword on the GetData function adds no functionality to the pseudo code. without an await within that function the code in that function executes synchronously. (I added the async because I assumed that the actual download chunk code will include an await.)

However! we don't particularly care in this case about the code in the function. We want to run the whole function multiple times in parallel. Calling a Task function without awaiting it in the DownloadAll method creates and runs a new Task for each download.

You have to be careful not to dispose of these Tasks before they are completed though. If you just ran the first set of code in a console app the app would start all the downloads and then finish and exit before they completed.

Licencié sous: CC-BY-SA avec attribution
scroll top