Need Async/Await equivalent for BackgroundWorker
https://softwareengineering.stackexchange.com/questions/323239
-
19-12-2020 - |
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:
- The downloads still run sequentially, not in parallel
- The UI blocks during execution
- 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
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.