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
  • you can call an eventhandler from inside the async method no problem? – Ewan Jun 25 '16 at 10:44
  • @Ewan: I'm not sure I follow you. Could you elaborate? I'm not familiar with how to use Async/Await or how to convert BackgroundWorker code; I'm looking for some suggestions. – InteXX Jun 25 '16 at 10:45
  • 1
    Note that async/await is not equivalent to BackgroundWorker. For one thing, it doesn't automagically create new threads for you; you have to do that yourself. See here, here and here. – Robert Harvey Jun 25 '16 at 14:15
  • @Ewan: Thank you for the example. I've edited my question to provide my workup; it's still not working. Could you have a look and maybe offer a critique? Thanks. – InteXX Jun 26 '16 at 7:20
  • 1
    urg vb :( ok, you are awaiting everything in the click event, your download.getchunk only gets one chunk per file and reports progress once. I was expecting multiple chunks per file. Still I would expect this to download all the files at the same time. Whats service.client maybe that is locking and forcing one at a time? – Ewan Jun 26 '16 at 9:06
up vote 1 down vote accepted

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.

  • This won't work. You have no await in GetData, so there is no place for GetData to return while work is being done. It will therefore run synchronously. – Robert Harvey Jun 25 '16 at 13:58
  • Im assuming the download saves to disk rather than returning the data. GetData will run On all the download objects at the same time because you dont await the call. If you need the return value you can add the tasks to an array and do Task.WaitAll – Ewan Jun 25 '16 at 14:20
  • The async keyword doesn't do anything, by itself. All it does is enable use of the await keyword within a method so decorated. From here: "The method runs synchronously until it reaches its first await expression, at which point the method is suspended until the awaited task is complete. In the meantime, control returns to the caller of the method." – Robert Harvey Jun 25 '16 at 14:23
  • exactly, you could await each download to finsih before starting the next, but you dont want to, so you dont await it – Ewan Jun 25 '16 at 14:30
  • 1
    Dude, listen to what I am saying. Your edit might work, but not for the reasons you think it does. – Robert Harvey Jun 25 '16 at 14:36

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.