Handling Reentrancy in Async Apps (C# and Visual Basic)
When you include asynchronous code in your app, you should consider and possibly prevent reentrancy, which refers to reentering an asynchronous operation before it has completed. If you don't identify and handle possibilities for reentrancy, it can cause unexpected results.
In this topic
Note
The instructions in Reviewing and Running the Example App show you how to run the code either as a Windows Presentation Foundation (WPF) app or as a Windows Store app.
To run the example as a WPF app, you must have Visual Studio 2012, Visual Studio 2013, Visual Studio Express 2012 for Windows Desktop, Visual Studio Express 2013 for Windows, or the .NET Framework 4.5 or 4.5.1 installed on your computer.
To run the example as a Windows Store app, you must have Windows 8 installed on your computer. In addition, if you want to run the example from Visual Studio, you must also have Visual Studio 2012, Visual Studio 2013, Visual Studio Express 2012 for Windows Desktop, or Visual Studio Express 2013 for Windows installed.
Recognizing Reentrancy
In the example in this topic, users choose a Start button to initiate an asynchronous app that downloads a series of websites and calculates the total number of bytes that are downloaded. A synchronous version of the example would respond the same way regardless of how many times a user chooses the button because, after the first time, the UI thread ignores those events until the app finishes running. In an asynchronous app, however, the UI thread continues to respond, and you might reenter the asynchronous operation before it has completed.
The following example shows the expected output if the user chooses the Start button only once. A list of the downloaded websites appears with the size, in bytes, of each site. The total number of bytes appears at the end.
1. msdn.microsoft.com/en-us/library/hh191443.aspx 83732
2. msdn.microsoft.com/en-us/library/aa578028.aspx 205273
3. msdn.microsoft.com/en-us/library/jj155761.aspx 29019
4. msdn.microsoft.com/en-us/library/hh290140.aspx 117152
5. msdn.microsoft.com/en-us/library/hh524395.aspx 68959
6. msdn.microsoft.com/en-us/library/ms404677.aspx 197325
7. msdn.microsoft.com 42972
8. msdn.microsoft.com/en-us/library/ff730837.aspx 146159
TOTAL bytes returned: 890591
However, if the user chooses the button more than once, the event handler is invoked repeatedly, and the download process is reentered each time. As a result, several asynchronous operations are running at the same time, the output interleaves the results, and the total number of bytes is confusing.
1. msdn.microsoft.com/en-us/library/hh191443.aspx 83732
2. msdn.microsoft.com/en-us/library/aa578028.aspx 205273
3. msdn.microsoft.com/en-us/library/jj155761.aspx 29019
4. msdn.microsoft.com/en-us/library/hh290140.aspx 117152
5. msdn.microsoft.com/en-us/library/hh524395.aspx 68959
1. msdn.microsoft.com/en-us/library/hh191443.aspx 83732
2. msdn.microsoft.com/en-us/library/aa578028.aspx 205273
6. msdn.microsoft.com/en-us/library/ms404677.aspx 197325
3. msdn.microsoft.com/en-us/library/jj155761.aspx 29019
7. msdn.microsoft.com 42972
4. msdn.microsoft.com/en-us/library/hh290140.aspx 117152
8. msdn.microsoft.com/en-us/library/ff730837.aspx 146159
TOTAL bytes returned: 890591
5. msdn.microsoft.com/en-us/library/hh524395.aspx 68959
1. msdn.microsoft.com/en-us/library/hh191443.aspx 83732
2. msdn.microsoft.com/en-us/library/aa578028.aspx 205273
6. msdn.microsoft.com/en-us/library/ms404677.aspx 197325
3. msdn.microsoft.com/en-us/library/jj155761.aspx 29019
4. msdn.microsoft.com/en-us/library/hh290140.aspx 117152
7. msdn.microsoft.com 42972
5. msdn.microsoft.com/en-us/library/hh524395.aspx 68959
8. msdn.microsoft.com/en-us/library/ff730837.aspx 146159
TOTAL bytes returned: 890591
6. msdn.microsoft.com/en-us/library/ms404677.aspx 197325
7. msdn.microsoft.com 42972
8. msdn.microsoft.com/en-us/library/ff730837.aspx 146159
TOTAL bytes returned: 890591
You can review the code that produces this output by scrolling to the end of this topic. You can experiment with the code by downloading the solution to your local computer and then running the WebsiteDownload project or by using the code at the end of this topic to create your own project For more information and instructions, see Reviewing and Running the Example App.
Handling Reentrancy
You can handle reentrancy in a variety of ways, depending on what you want your app to do. This topic presents the following examples:
-
Disable the Start button while the operation is running so that the user can't interrupt it.
Cancel and Restart the Operation
Cancel any operation that is still running when the user chooses the Start button again, and then let the most recently requested operation continue.
Run Multiple Operations and Queue the Output
Allow all requested operations to run asynchronously, but coordinate the display of output so that the results from each operation appear together and in order.
Disable the Start Button
You can block the Start button while an operation is running by disabling the button at the top of the StartButton_Click event handler. You can then reenable the button from within a finally block when the operation finishes so that users can run the app again.
The following code shows these changes, which are marked with asterisks. You can add the changes to the code at the end of this topic, or you can download the finished app from Async Samples: Reentrancy in .NET Desktop Apps or Async Samples: Reentrancy in Windows Store Apps. The project name is DisableStartButton.
Private Async Sub StartButton_Click(sender As Object, e As RoutedEventArgs)
' This line is commented out to make the results clearer in the output.
'ResultsTextBox.Text = ""
' ***Disable the Start button until the downloads are complete.
StartButton.IsEnabled = False
Try
Await AccessTheWebAsync()
Catch ex As Exception
ResultsTextBox.Text &= vbCrLf & "Downloads failed."
' ***Enable the Start button in case you want to run the program again.
Finally
StartButton.IsEnabled = True
End Try
End Sub
private async void StartButton_Click(object sender, RoutedEventArgs e)
{
// This line is commented out to make the results clearer in the output.
//ResultsTextBox.Text = "";
// ***Disable the Start button until the downloads are complete.
StartButton.IsEnabled = false;
try
{
await AccessTheWebAsync();
}
catch (Exception)
{
ResultsTextBox.Text += "\r\nDownloads failed.";
}
// ***Enable the Start button in case you want to run the program again.
finally
{
StartButton.IsEnabled = true;
}
}
As a result of the changes, the button doesn't respond while AccessTheWebAsync is downloading the websites, so the process can’t be reentered.
Cancel and Restart the Operation
Instead of disabling the Start button, you can keep the button active but, if the user chooses that button again, cancel the operation that's already running and let the most recently started operation continue.
For more information about cancellation, see Fine Tuning Your Async Application.
To set up this scenario, make the following changes to the basic code that is provided in Reviewing and Running the Example App. You also can download the finished app from Async Samples: Reentrancy in .NET Desktop Apps or Async Samples: Reentrancy in Windows Store Apps. The name of this project is CancelAndRestart.
Declare a CancellationTokenSource variable, cts, that’s in scope for all methods.
Class MainWindow // Or Class MainPage ' *** Declare a System.Threading.CancellationTokenSource. Dim cts As CancellationTokenSource
public partial class MainWindow : Window // Or class MainPage { // *** Declare a System.Threading.CancellationTokenSource. CancellationTokenSource cts;
In StartButton_Click, determine whether an operation is already underway. If the value of cts is null (Nothing in Visual Basic), no operation is already active. If the value isn't null, the operation that is already running is canceled.
' *** If a download process is already underway, cancel it. If cts IsNot Nothing Then cts.Cancel() End If
// *** If a download process is already underway, cancel it. if (cts != null) { cts.Cancel(); }
Set cts to a different value that represents the current process.
' *** Now set cts to cancel the current process if the button is chosen again. Dim newCTS As CancellationTokenSource = New CancellationTokenSource() cts = newCTS
// *** Now set cts to a new value that you can use to cancel the current process // if the button is chosen again. CancellationTokenSource newCTS = new CancellationTokenSource(); cts = newCTS;
At the end of StartButton_Click, the current process is complete, so set the value of cts back to null.
' *** When the process completes, signal that another process can proceed. If cts Is newCTS Then cts = Nothing End If
// *** When the process is complete, signal that another process can begin. if (cts == newCTS) cts = null;
The following code shows all the changes in StartButton_Click. The additions are marked with asterisks.
Private Async Sub StartButton_Click(sender As Object, e As RoutedEventArgs)
' This line is commented out to make the results clearer.
'ResultsTextBox.Text = ""
' *** If a download process is underway, cancel it.
If cts IsNot Nothing Then
cts.Cancel()
End If
' *** Now set cts to cancel the current process if the button is chosen again.
Dim newCTS As CancellationTokenSource = New CancellationTokenSource()
cts = newCTS
Try
' *** Send a token to carry the message if the operation is canceled.
Await AccessTheWebAsync(cts.Token)
Catch ex As OperationCanceledException
ResultsTextBox.Text &= vbCrLf & "Download canceled." & vbCrLf
Catch ex As Exception
ResultsTextBox.Text &= vbCrLf & "Downloads failed."
End Try
' *** When the process is complete, signal that another process can proceed.
If cts Is newCTS Then
cts = Nothing
End If
End Sub
private async void StartButton_Click(object sender, RoutedEventArgs e)
{
// This line is commented out to make the results clearer in the output.
//ResultsTextBox.Clear();
// *** If a download process is already underway, cancel it.
if (cts != null)
{
cts.Cancel();
}
// *** Now set cts to cancel the current process if the button is chosen again.
CancellationTokenSource newCTS = new CancellationTokenSource();
cts = newCTS;
try
{
// ***Send cts.Token to carry the message if there is a cancellation request.
await AccessTheWebAsync(cts.Token);
}
// *** Catch cancellations separately.
catch (OperationCanceledException)
{
ResultsTextBox.Text += "\r\nDownloads canceled.\r\n";
}
catch (Exception)
{
ResultsTextBox.Text += "\r\nDownloads failed.\r\n";
}
// *** When the process is complete, signal that another process can proceed.
if (cts == newCTS)
cts = null;
}
In AccessTheWebAsync, make the following changes.
Add a parameter to accept the cancellation token from StartButton_Click.
Use the GetAsync method to download the websites because GetAsync accepts a CancellationToken argument.
Before calling DisplayResults to display the results for each downloaded website, check ct to verify that the current operation hasn’t been canceled.
The following code shows these changes, which are marked with asterisks.
' *** Provide a parameter for the CancellationToken from StartButton_Click.
Private Async Function AccessTheWebAsync(ct As CancellationToken) As Task
' Declare an HttpClient object.
Dim client = New HttpClient()
' Make a list of web addresses.
Dim urlList As List(Of String) = SetUpURLList()
Dim total = 0
Dim position = 0
For Each url In urlList
' *** Use the HttpClient.GetAsync method because it accepts a
' cancellation token.
Dim response As HttpResponseMessage = Await client.GetAsync(url, ct)
' *** Retrieve the website contents from the HttpResponseMessage.
Dim urlContents As Byte() = Await response.Content.ReadAsByteArrayAsync()
' *** Check for cancellations before displaying information about the
' latest site.
ct.ThrowIfCancellationRequested()
position += 1
DisplayResults(url, urlContents, position)
' Update the total.
total += urlContents.Length
Next
' Display the total count for all of the websites.
ResultsTextBox.Text &=
String.Format(vbCrLf & vbCrLf & "TOTAL bytes returned: " & total & vbCrLf)
End Function
// *** Provide a parameter for the CancellationToken from StartButton_Click.
async Task AccessTheWebAsync(CancellationToken ct)
{
// Declare an HttpClient object.
HttpClient client = new HttpClient();
// Make a list of web addresses.
List<string> urlList = SetUpURLList();
var total = 0;
var position = 0;
foreach (var url in urlList)
{
// *** Use the HttpClient.GetAsync method because it accepts a
// cancellation token.
HttpResponseMessage response = await client.GetAsync(url, ct);
// *** Retrieve the website contents from the HttpResponseMessage.
byte[] urlContents = await response.Content.ReadAsByteArrayAsync();
// *** Check for cancellations before displaying information about the
// latest site.
ct.ThrowIfCancellationRequested();
DisplayResults(url, urlContents, ++position);
// Update the total.
total += urlContents.Length;
}
// Display the total count for all of the websites.
ResultsTextBox.Text +=
string.Format("\r\n\r\nTOTAL bytes returned: {0}\r\n", total);
}
If you choose the Start button several times while this app is running, it should produce results that resemble the following output.
1. msdn.microsoft.com/en-us/library/hh191443.aspx 83732
2. msdn.microsoft.com/en-us/library/aa578028.aspx 205273
3. msdn.microsoft.com/en-us/library/jj155761.aspx 29019
4. msdn.microsoft.com/en-us/library/hh290140.aspx 122505
5. msdn.microsoft.com/en-us/library/hh524395.aspx 68959
6. msdn.microsoft.com/en-us/library/ms404677.aspx 197325
Download canceled.
1. msdn.microsoft.com/en-us/library/hh191443.aspx 83732
2. msdn.microsoft.com/en-us/library/aa578028.aspx 205273
3. msdn.microsoft.com/en-us/library/jj155761.aspx 29019
Download canceled.
1. msdn.microsoft.com/en-us/library/hh191443.aspx 83732
2. msdn.microsoft.com/en-us/library/aa578028.aspx 205273
3. msdn.microsoft.com/en-us/library/jj155761.aspx 29019
4. msdn.microsoft.com/en-us/library/hh290140.aspx 117152
5. msdn.microsoft.com/en-us/library/hh524395.aspx 68959
6. msdn.microsoft.com/en-us/library/ms404677.aspx 197325
7. msdn.microsoft.com 42972
8. msdn.microsoft.com/en-us/library/ff730837.aspx 146159
TOTAL bytes returned: 890591
To eliminate the partial lists, uncomment the first line of code in StartButton_Click to clear the text box each time the user restarts the operation.
Run Multiple Operations and Queue the Output
This third example is the most complicated in that the app starts another asynchronous operation each time that the user chooses the Start button, and all the operations run to completion. All the requested operations download websites from the list asynchronously, but the output from the operations is presented sequentially. That is, the actual downloading activity is interleaved, as the output in Recognizing Reentrancy shows, but the list of results for each group is presented separately.
The operations share a global Task, pendingWork, which serves as a gatekeeper for the display process.
You can run this example by pasting the changes into the code in Building the App, or you can follow the instructions in Downloading the App to download the sample and then run the QueueResults project.
The following output shows the result if the user chooses the Start button only once. The letter label, A, indicates that the result is from the first time the Start button is chosen. The numbers show the order of the URLs in the list of download targets.
#Starting group A.
#Task assigned for group A.
A-1. msdn.microsoft.com/en-us/library/hh191443.aspx 87389
A-2. msdn.microsoft.com/en-us/library/aa578028.aspx 209858
A-3. msdn.microsoft.com/en-us/library/jj155761.aspx 30870
A-4. msdn.microsoft.com/en-us/library/hh290140.aspx 119027
A-5. msdn.microsoft.com/en-us/library/hh524395.aspx 71260
A-6. msdn.microsoft.com/en-us/library/ms404677.aspx 199186
A-7. msdn.microsoft.com 53266
A-8. msdn.microsoft.com/en-us/library/ff730837.aspx 148020
TOTAL bytes returned: 918876
#Group A is complete.
If the user chooses the Start button three times, the app produces output that resembles the following lines. The information lines that start with a pound sign (#) trace the progress of the application.
#Starting group A.
#Task assigned for group A.
A-1. msdn.microsoft.com/en-us/library/hh191443.aspx 87389
A-2. msdn.microsoft.com/en-us/library/aa578028.aspx 207089
A-3. msdn.microsoft.com/en-us/library/jj155761.aspx 30870
A-4. msdn.microsoft.com/en-us/library/hh290140.aspx 119027
A-5. msdn.microsoft.com/en-us/library/hh524395.aspx 71259
A-6. msdn.microsoft.com/en-us/library/ms404677.aspx 199185
#Starting group B.
#Task assigned for group B.
A-7. msdn.microsoft.com 53266
#Starting group C.
#Task assigned for group C.
A-8. msdn.microsoft.com/en-us/library/ff730837.aspx 148010
TOTAL bytes returned: 916095
B-1. msdn.microsoft.com/en-us/library/hh191443.aspx 87389
B-2. msdn.microsoft.com/en-us/library/aa578028.aspx 207089
B-3. msdn.microsoft.com/en-us/library/jj155761.aspx 30870
B-4. msdn.microsoft.com/en-us/library/hh290140.aspx 119027
B-5. msdn.microsoft.com/en-us/library/hh524395.aspx 71260
B-6. msdn.microsoft.com/en-us/library/ms404677.aspx 199186
#Group A is complete.
B-7. msdn.microsoft.com 53266
B-8. msdn.microsoft.com/en-us/library/ff730837.aspx 148010
TOTAL bytes returned: 916097
C-1. msdn.microsoft.com/en-us/library/hh191443.aspx 87389
C-2. msdn.microsoft.com/en-us/library/aa578028.aspx 207089
#Group B is complete.
C-3. msdn.microsoft.com/en-us/library/jj155761.aspx 30870
C-4. msdn.microsoft.com/en-us/library/hh290140.aspx 119027
C-5. msdn.microsoft.com/en-us/library/hh524395.aspx 72765
C-6. msdn.microsoft.com/en-us/library/ms404677.aspx 199186
C-7. msdn.microsoft.com 56190
C-8. msdn.microsoft.com/en-us/library/ff730837.aspx 148010
TOTAL bytes returned: 920526
#Group C is complete.
Groups B and C start before group A has finished, but the output for the each group appears separately. All the output for group A appears first, followed by all the output for group B, and then all the output for group C. The app always displays the groups in order and, for each group, always displays the information about the individual websites in the order that the URLs appear in the list of URLs.
However, you can't predict the order in which the downloads actually happen. After multiple groups have been started, the download tasks that they generate are all active. You can't assume that A-1 will be downloaded before B-1, and you can't assume that A-1 will be downloaded before A-2.
Global Definitions
The sample code contains the following two global declarations that are visible from all methods.
Class MainWindow ' Class MainPage in Windows Store app.
' ***Declare the following variables where all methods can access them.
Private pendingWork As Task = Nothing
Private group As Char = ChrW(AscW("A") - 1)
public partial class MainWindow : Window // Class MainPage in Windows Store app.
{
// ***Declare the following variables where all methods can access them.
private Task pendingWork = null;
private char group = (char)('A' - 1);
The Task variable, pendingWork, oversees the display process and prevents any group from interrupting another group's display operation. The character variable, group, labels the output from different groups to verify that results appear in the expected order.
The Click Event Handler
The event handler, StartButton_Click, increments the group letter each time the user chooses the Start button. Then the handler calls AccessTheWebAsync to run the downloading operation.
Private Async Sub StartButton_Click(sender As Object, e As RoutedEventArgs)
' ***Verify that each group's results are displayed together, and that
' the groups display in order, by marking each group with a letter.
group = ChrW(AscW(group) + 1)
ResultsTextBox.Text &= String.Format(vbCrLf & vbCrLf & "#Starting group {0}.", group)
Try
' *** Pass the group value to AccessTheWebAsync.
Dim finishedGroup As Char = Await AccessTheWebAsync(group)
' The following line verifies a successful return from the download and
' display procedures.
ResultsTextBox.Text &= String.Format(vbCrLf & vbCrLf & "#Group {0} is complete." & vbCrLf, finishedGroup)
Catch ex As Exception
ResultsTextBox.Text &= vbCrLf & "Downloads failed."
End Try
End Sub
private async void StartButton_Click(object sender, RoutedEventArgs e)
{
// ***Verify that each group's results are displayed together, and that
// the groups display in order, by marking each group with a letter.
group = (char)(group + 1);
ResultsTextBox.Text += string.Format("\r\n\r\n#Starting group {0}.", group);
try
{
// *** Pass the group value to AccessTheWebAsync.
char finishedGroup = await AccessTheWebAsync(group);
// The following line verifies a successful return from the download and
// display procedures.
ResultsTextBox.Text += string.Format("\r\n\r\n#Group {0} is complete.\r\n", finishedGroup);
}
catch (Exception)
{
ResultsTextBox.Text += "\r\nDownloads failed.";
}
}
The AccessTheWebAsync Method
This example splits AccessTheWebAsync into two methods. The first method, AccessTheWebAsync, starts all the download tasks for a group and sets up pendingWork to control the display process. The method uses a Language Integrated Query (LINQ query) and ToArray<TSource> to start all the download tasks at the same time.
AccessTheWebAsync then calls FinishOneGroupAsync to await the completion of each download and display its length.
FinishOneGroupAsync returns a task that's assigned to pendingWork in AccessTheWebAsync. That value prevents interruption by another operation before the task is complete.
Private Async Function AccessTheWebAsync(grp As Char) As Task(Of Char)
Dim client = New HttpClient()
' Make a list of the web addresses to download.
Dim urlList As List(Of String) = SetUpURLList()
' ***Kick off the downloads. The application of ToArray activates all the download tasks.
Dim getContentTasks As Task(Of Byte())() =
urlList.Select(Function(addr) client.GetByteArrayAsync(addr)).ToArray()
' ***Call the method that awaits the downloads and displays the results.
' Assign the Task that FinishOneGroupAsync returns to the gatekeeper task, pendingWork.
pendingWork = FinishOneGroupAsync(urlList, getContentTasks, grp)
ResultsTextBox.Text &=
String.Format(vbCrLf & "#Task assigned for group {0}. Download tasks are active." & vbCrLf, grp)
' ***This task is complete when a group has finished downloading and displaying.
Await pendingWork
' You can do other work here or just return.
Return grp
End Function
private async Task<char> AccessTheWebAsync(char grp)
{
HttpClient client = new HttpClient();
// Make a list of the web addresses to download.
List<string> urlList = SetUpURLList();
// ***Kick off the downloads. The application of ToArray activates all the download tasks.
Task<byte[]>[] getContentTasks = urlList.Select(url => client.GetByteArrayAsync(url)).ToArray();
// ***Call the method that awaits the downloads and displays the results.
// Assign the Task that FinishOneGroupAsync returns to the gatekeeper task, pendingWork.
pendingWork = FinishOneGroupAsync(urlList, getContentTasks, grp);
ResultsTextBox.Text += string.Format("\r\n#Task assigned for group {0}. Download tasks are active.\r\n", grp);
// ***This task is complete when a group has finished downloading and displaying.
await pendingWork;
// You can do other work here or just return.
return grp;
}
The FinishOneGroupAsync Method
This method cycles through the download tasks in a group, awaiting each one, displaying the length of the downloaded website, and adding the length to the total.
The first statement in FinishOneGroupAsync uses pendingWork to make sure that entering the method doesn't interfere with an operation that is already in the display process or that's already waiting. If such an operation is in progress, the entering operation must wait its turn.
Private Async Function FinishOneGroupAsync(urls As List(Of String), contentTasks As Task(Of Byte())(), grp As Char) As Task
' Wait for the previous group to finish displaying results.
If pendingWork IsNot Nothing Then
Await pendingWork
End If
Dim total = 0
' contentTasks is the array of Tasks that was created in AccessTheWebAsync.
For i As Integer = 0 To contentTasks.Length - 1
' Await the download of a particular URL, and then display the URL and
' its length.
Dim content As Byte() = Await contentTasks(i)
DisplayResults(urls(i), content, i, grp)
total += content.Length
Next
' Display the total count for all of the websites.
ResultsTextBox.Text &=
String.Format(vbCrLf & vbCrLf & "TOTAL bytes returned: " & total & vbCrLf)
End Function
private async Task FinishOneGroupAsync(List<string> urls, Task<byte[]>[] contentTasks, char grp)
{
// ***Wait for the previous group to finish displaying results.
if (pendingWork != null) await pendingWork;
int total = 0;
// contentTasks is the array of Tasks that was created in AccessTheWebAsync.
for (int i = 0; i < contentTasks.Length; i++)
{
// Await the download of a particular URL, and then display the URL and
// its length.
byte[] content = await contentTasks[i];
DisplayResults(urls[i], content, i, grp);
total += content.Length;
}
// Display the total count for all of the websites.
ResultsTextBox.Text +=
string.Format("\r\n\r\nTOTAL bytes returned: {0}\r\n", total);
}
You can run this example by pasting the changes into the code in Building the App, or you can follow the instructions in Downloading the App to download the sample, and then run the QueueResults project.
Points of Interest
The information lines that start with a pound sign (#) in the output clarify how this example works.
The output shows the following patterns.
A group can be started while a previous group is displaying its output, but the display of the previous group's output isn't interrupted.
#Starting group A. #Task assigned for group A. Download tasks are active. A-1. msdn.microsoft.com/en-us/library/hh191443.aspx 87389 A-2. msdn.microsoft.com/en-us/library/aa578028.aspx 207089 A-3. msdn.microsoft.com/en-us/library/jj155761.aspx 30870 A-4. msdn.microsoft.com/en-us/library/hh290140.aspx 119037 A-5. msdn.microsoft.com/en-us/library/hh524395.aspx 71260 #Starting group B. #Task assigned for group B. Download tasks are active. A-6. msdn.microsoft.com/en-us/library/ms404677.aspx 199186 A-7. msdn.microsoft.com 53078 A-8. msdn.microsoft.com/en-us/library/ff730837.aspx 148010 TOTAL bytes returned: 915919 B-1. msdn.microsoft.com/en-us/library/hh191443.aspx 87388 B-2. msdn.microsoft.com/en-us/library/aa578028.aspx 207089 B-3. msdn.microsoft.com/en-us/library/jj155761.aspx 30870 #Group A is complete. B-4. msdn.microsoft.com/en-us/library/hh290140.aspx 119027 B-5. msdn.microsoft.com/en-us/library/hh524395.aspx 71260 B-6. msdn.microsoft.com/en-us/library/ms404677.aspx 199186 B-7. msdn.microsoft.com 53078 B-8. msdn.microsoft.com/en-us/library/ff730837.aspx 148010 TOTAL bytes returned: 915908
The pendingWork task is null (Nothing in Visual Basic) at the start of FinishOneGroupAsync only for group A, which started first. Group A hasn’t yet completed an await expression when it reaches FinishOneGroupAsync. Therefore, control hasn't returned to AccessTheWebAsync, and the first assignment to pendingWork hasn't occurred.
The following two lines always appear together in the output. The code is never interrupted between starting a group's operation in StartButton_Click and assigning a task for the group to pendingWork.
#Starting group B. #Task assigned for group B. Download tasks are active.
After a group enters StartButton_Click, the operation doesn't complete an await expression until the operation enters FinishOneGroupAsync. Therefore, no other operation can gain control during that segment of code.
Reviewing and Running the Example App
To better understand the example app, you can download it, build it yourself, or review the code at the end of this topic without implementing the app.
Note
To run the example as a Windows Presentation Foundation (WPF) desktop app, you must have Visual Studio 2012, Visual Studio 2013, Visual Studio Express 2012 for Windows Desktop, Visual Studio Express 2013 for Windows, or the .NET Framework 4.5 or 4.5.1 installed on your computer.
To run the example as a Windows Store app, you must have Windows 8 installed on your computer. In addition, if you want to run the example from Visual Studio, you must also have Visual Studio 2012, Visual Studio 2013, Visual Studio Express 2012 for Windows 8, or Visual Studio Express 2013 for Windows installed. Visual Studio 2010 can't load projects that are targeted for the .NET Framework 4.5.
Downloading the App
Download the compressed file from Async Samples: Reentrancy in .NET Desktop Apps or Async Samples: Reentrancy in Windows Store Apps.
Decompress the file that you downloaded, and then start Visual Studio.
On the menu bar, choose File, Open, Project/Solution.
Navigate to the folder that holds the decompressed sample code, and then open the solution (.sln) file.
In Solution Explorer, open the shortcut menu for the project that you want to run, and then choose Set as StartUpProject.
Choose the CTRL+F5 keys to build and run the project.
Building the App
The following sections provide the code to build the example as a WPF app or as a Windows Store app.
To build a WPF app
Start Visual Studio.
On the menu bar, choose File, New, Project.
The New Project dialog box opens.
In the Installed Templates pane, expand Visual Basic or Visual C#, and then expand Windows.
In the list of project types, choose WPF Application.
Name the project WebsiteDownloadWPF, and then choose the OK button.
The new project appears in Solution Explorer.
In the Visual Studio Code Editor, choose the MainWindow.xaml tab.
If the tab isn’t visible, open the shortcut menu for MainWindow.xaml in Solution Explorer, and then choose View Code.
In the XAML view of MainWindow.xaml, replace the code with the following code.
<Window x:Class="MainWindow" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:WebsiteDownloadWPF" xmlns:d="https://schemas.microsoft.com/expression/blend/2008" xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Width="517" Height="360"> <Button x:Name="StartButton" Content="Start" HorizontalAlignment="Left" Margin="-1,0,0,0" VerticalAlignment="Top" Click="StartButton_Click" Height="53" Background="#FFA89B9B" FontSize="36" Width="518" /> <TextBox x:Name="ResultsTextBox" HorizontalAlignment="Left" Margin="-1,53,0,-36" TextWrapping="Wrap" VerticalAlignment="Top" Height="343" FontSize="10" ScrollViewer.VerticalScrollBarVisibility="Visible" Width="518" FontFamily="Lucida Console" /> </Grid> </Window>
<Window x:Class="WebsiteDownloadWPF.MainWindow" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:WebsiteDownloadWPF" xmlns:d="https://schemas.microsoft.com/expression/blend/2008" xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Width="517" Height="360"> <Button x:Name="StartButton" Content="Start" HorizontalAlignment="Left" Margin="-1,0,0,0" VerticalAlignment="Top" Click="StartButton_Click" Height="53" Background="#FFA89B9B" FontSize="36" Width="518" /> <TextBox x:Name="ResultsTextBox" HorizontalAlignment="Left" Margin="-1,53,0,-36" TextWrapping="Wrap" VerticalAlignment="Top" Height="343" FontSize="10" ScrollViewer.VerticalScrollBarVisibility="Visible" Width="518" FontFamily="Lucida Console" /> </Grid> </Window>
A simple window that contains a text box and a button appears in the Design view of MainWindow.xaml.
Add a reference for System.Net.Http.
In Solution Explorer, open the shortcut menu for MainWindow.xaml.vb or MainWindow.xaml.cs, and then choose View Code.
In MainWindow.xaml.vb or MainWindow.xaml.cs, replace the code with the following code.
' Add the following Imports statements, and add a reference for System.Net.Http. Imports System.Net.Http Imports System.Threading Class MainWindow Private Async Sub StartButton_Click(sender As Object, e As RoutedEventArgs) ' This line is commented out to make the results clearer in the output. 'ResultsTextBox.Text = "" Try Await AccessTheWebAsync() Catch ex As Exception ResultsTextBox.Text &= vbCrLf & "Downloads failed." End Try End Sub Private Async Function AccessTheWebAsync() As Task ' Declare an HttpClient object. Dim client = New HttpClient() ' Make a list of web addresses. Dim urlList As List(Of String) = SetUpURLList() Dim total = 0 Dim position = 0 For Each url In urlList ' GetByteArrayAsync returns a task. At completion, the task ' produces a byte array. Dim urlContents As Byte() = Await client.GetByteArrayAsync(url) position += 1 DisplayResults(url, urlContents, position) ' Update the total. total += urlContents.Length Next ' Display the total count for all of the websites. ResultsTextBox.Text &= String.Format(vbCrLf & vbCrLf & "TOTAL bytes returned: " & total & vbCrLf) End Function Private Function SetUpURLList() As List(Of String) Dim urls = New List(Of String) From { "https://msdn.microsoft.com/en-us/library/hh191443.aspx", "https://msdn.microsoft.com/en-us/library/aa578028.aspx", "https://msdn.microsoft.com/en-us/library/jj155761.aspx", "https://msdn.microsoft.com/en-us/library/hh290140.aspx", "https://msdn.microsoft.com/en-us/library/hh524395.aspx", "https://msdn.microsoft.com/en-us/library/ms404677.aspx", "https://msdn.microsoft.com", "https://msdn.microsoft.com/en-us/library/ff730837.aspx" } Return urls End Function Private Sub DisplayResults(url As String, content As Byte(), pos As Integer) ' Display the length of each website. The string format is designed ' to be used with a monospaced font, such as Lucida Console or ' Global Monospace. ' Strip off the "http:'". Dim displayURL = url.Replace("http://", "") ' Display position in the URL list, the URL, and the number of bytes. ResultsTextBox.Text &= String.Format(vbCrLf & "{0}. {1,-58} {2,8}", pos, displayURL, content.Length) End Sub End Class
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; // Add the following using directives, and add a reference for System.Net.Http. using System.Net.Http; using System.Threading; namespace WebsiteDownloadWPF { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private async void StartButton_Click(object sender, RoutedEventArgs e) { // This line is commented out to make the results clearer in the output. //ResultsTextBox.Text = ""; try { await AccessTheWebAsync(); } catch (Exception) { ResultsTextBox.Text += "\r\nDownloads failed."; } } private async Task AccessTheWebAsync() { // Declare an HttpClient object. HttpClient client = new HttpClient(); // Make a list of web addresses. List<string> urlList = SetUpURLList(); var total = 0; var position = 0; foreach (var url in urlList) { // GetByteArrayAsync returns a task. At completion, the task // produces a byte array. byte[] urlContents = await client.GetByteArrayAsync(url); DisplayResults(url, urlContents, ++position); // Update the total. total += urlContents.Length; } // Display the total count for all of the websites. ResultsTextBox.Text += string.Format("\r\n\r\nTOTAL bytes returned: {0}\r\n", total); } private List<string> SetUpURLList() { List<string> urls = new List<string> { "https://msdn.microsoft.com/en-us/library/hh191443.aspx", "https://msdn.microsoft.com/en-us/library/aa578028.aspx", "https://msdn.microsoft.com/en-us/library/jj155761.aspx", "https://msdn.microsoft.com/en-us/library/hh290140.aspx", "https://msdn.microsoft.com/en-us/library/hh524395.aspx", "https://msdn.microsoft.com/en-us/library/ms404677.aspx", "https://msdn.microsoft.com", "https://msdn.microsoft.com/en-us/library/ff730837.aspx" }; return urls; } private void DisplayResults(string url, byte[] content, int pos) { // Display the length of each website. The string format is designed // to be used with a monospaced font, such as Lucida Console or // Global Monospace. // Strip off the "http://". var displayURL = url.Replace("http://", ""); // Display position in the URL list, the URL, and the number of bytes. ResultsTextBox.Text += string.Format("\n{0}. {1,-58} {2,8}", pos, displayURL, content.Length); } } }
Choose the CTRL+F5 keys to run the program, and then choose the Start button several times.
Make the changes from Disable the Start Button, Cancel and Restart the Operation, or Run Multiple Operations and Queue the Output to handle the reentrancy.
To build a Windows Store app
Start Visual Studio.
On the menu bar, choose File, New, Project.
The New Project dialog box opens.
In the Installed, Templates category, expand Visual Basic or Visual C#, and then expand Windows Store.
In the list of project types, choose Blank App (XAML).
Name the project WebsiteDownloadWin, and then choose the OK button.
The new project appears in Solution Explorer.
In Solution Explorer, open the shortcut menu for MainPage.xaml, and then choose Open.
In the XAML window of MainPage.xaml, replace the code with the following code.
<Page x:Class="WebsiteDownloadWin.MainPage" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:WebsiteDownloadWin" xmlns:d="https://schemas.microsoft.com/expression/blend/2008" xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" FontSize="12"> <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"> <Button x:Name="StartButton" Content="Start" HorizontalAlignment="Left" Margin="325,77,0,0" VerticalAlignment="Top" Click="StartButton_Click" Height="145" Background="#FFA89B9B" FontSize="36" Width="711" /> <TextBox x:Name="ResultsTextBox" HorizontalAlignment="Left" Margin="325,222,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Height="546" FontSize="10" ScrollViewer.VerticalScrollBarVisibility="Visible" Width="711" FontFamily="Lucida Console" /> </Grid> </Page>
A simple window that contains a text box and a Start button appears in the Design window of MainPage.xaml.
In Solution Explorer, open the shortcut menu for MainPage.xaml.vb or MainPage.xaml.cs, and then choose View Code.
Replace the code in MainPage.xaml.vb or MainPage.xaml.cs with the following code.
' Add the following Imports statements. Imports System.Threading.Tasks Imports System.Threading Imports System.Net.Http Public NotInheritable Class MainPage Inherits Page Protected Overrides Sub OnNavigatedTo(e As Navigation.NavigationEventArgs) End Sub Private Async Sub StartButton_Click(sender As Object, e As RoutedEventArgs) ' This line is commented out to make the results clearer in the output. 'ResultsTextBox.Text = "" Try Await AccessTheWebAsync() Catch ex As Exception ResultsTextBox.Text &= vbCrLf & "Downloads failed." End Try End Sub Private Async Function AccessTheWebAsync() As Task ' Declare an HttpClient object. Dim client = New HttpClient() ' Make a list of web addresses. Dim urlList As List(Of String) = SetUpURLList() Dim total = 0 Dim position = 0 For Each url In urlList ' GetByteArrayAsync returns a task. At completion, the task ' produces a byte array. Dim urlContents As Byte() = Await client.GetByteArrayAsync(url) position += 1 DisplayResults(url, urlContents, position) ' Update the total. total += urlContents.Length Next ' Display the total count for all of the websites. ResultsTextBox.Text &= String.Format(vbCrLf & vbCrLf & "TOTAL bytes returned: " & total & vbCrLf) End Function Private Function SetUpURLList() As List(Of String) Dim urls = New List(Of String) From { "https://msdn.microsoft.com/en-us/library/hh191443.aspx", "https://msdn.microsoft.com/en-us/library/aa578028.aspx", "https://msdn.microsoft.com/en-us/library/jj155761.aspx", "https://msdn.microsoft.com/en-us/library/hh290140.aspx", "https://msdn.microsoft.com/en-us/library/hh524395.aspx", "https://msdn.microsoft.com/en-us/library/ms404677.aspx", "https://msdn.microsoft.com", "https://msdn.microsoft.com/en-us/library/ff730837.aspx" } Return urls End Function Private Sub DisplayResults(url As String, content As Byte(), pos As Integer) ' Display the length of each website. The string format is designed ' to be used with a monospaced font, such as Lucida Console or ' Global Monospace. ' Strip off the "http:'". Dim displayURL = url.Replace("http://", "") ' Display position in the URL list, the URL, and the number of bytes. ResultsTextBox.Text &= String.Format(vbCrLf & "{0}. {1,-58} {2,8}", pos, displayURL, content.Length) End Sub End Class
using System; using System.Collections.Generic; using System.IO; using System.Linq; using Windows.Foundation; using Windows.Foundation.Collections; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls.Primitives; using Windows.UI.Xaml.Data; using Windows.UI.Xaml.Input; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Navigation; // Add the following using directives. using System.Threading.Tasks; using System.Threading; using System.Net.Http; namespace WebsiteDownloadWin { public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); } private async void StartButton_Click(object sender, RoutedEventArgs e) { // This line is commented out to make the results clearer in the output. //ResultsTextBox.Text = ""; try { await AccessTheWebAsync(); } catch (Exception) { ResultsTextBox.Text += "\r\nDownloads failed."; } } private async Task AccessTheWebAsync() { // Declare an HttpClient object. HttpClient client = new HttpClient(); // Make a list of web addresses. List<string> urlList = SetUpURLList(); var total = 0; var position = 0; foreach (var url in urlList) { // GetByteArrayAsync returns a task. At completion, the task // produces a byte array. byte[] urlContents = await client.GetByteArrayAsync(url); DisplayResults(url, urlContents, ++position); // Update the total. total += urlContents.Length; } // Display the total count for all of the websites. ResultsTextBox.Text += string.Format("\r\n\r\nTOTAL bytes returned: {0}\r\n", total); } private List<string> SetUpURLList() { List<string> urls = new List<string> { "https://msdn.microsoft.com/en-us/library/hh191443.aspx", "https://msdn.microsoft.com/en-us/library/aa578028.aspx", "https://msdn.microsoft.com/en-us/library/jj155761.aspx", "https://msdn.microsoft.com/en-us/library/hh290140.aspx", "https://msdn.microsoft.com/en-us/library/hh524395.aspx", "https://msdn.microsoft.com/en-us/library/ms404677.aspx", "https://msdn.microsoft.com", "https://msdn.microsoft.com/en-us/library/ff730837.aspx" }; return urls; } private void DisplayResults(string url, byte[] content, int pos) { // Display the length of each website. The string format is designed // to be used with a monospaced font, such as Lucida Console or // Global Monospace. // Strip off the "http://". var displayURL = url.Replace("http://", ""); // Display position in the URL list, the URL, and the number of bytes. ResultsTextBox.Text += string.Format("\n{0}. {1,-58} {2,8}", pos, displayURL, content.Length); } } }
Choose the CTRL+F5 keys to run the program, and then choose the Start button several times.
Make the changes from Disable the Start Button, Cancel and Restart the Operation, or Run Multiple Operations and Queue the Output to handle the reentrancy.
See Also
Tasks
Walkthrough: Accessing the Web by Using Async and Await (C# and Visual Basic)
Concepts
Asynchronous Programming with Async and Await (C# and Visual Basic)