Reputation: 1205
I am trying to do async programming for the first time but it is not working as I expect it. I have a button which loads a collection of urls (this is ommitted from the code snippets)
private async void btnLoad_Click(object sender, EventArgs e)
{
foreach (var item in myCollectionOfUrls)
{
Uri tempUri = new Uri(item);
Uri = tempUri; // Uri is a property
string htmlCode = await LoadHtmlCodeAsync(Uri);
LoadAllChaptersAsync(htmlCode, Path.GetFileNameWithoutExtension(item));
}
}
LoadHtmlCodeAsync(Uri) works as intended:
private string LoadHtmlCode(string url)
{
using (WebClient client = new WebClient())
{
try
{
System.Threading.Thread.Sleep(0);
client.Encoding = Encoding.UTF8;
client.Proxy = null;
return client.DownloadString(url);
}
catch (Exception ex)
{
Logger.Log(ex.Message);
throw;
}
}
}
But LoadAllChaptersAsync
throws an error "this async method lacks await operators..."
private async void LoadAllChaptersAsync(string htmlCode, string mangaName)
{
HtmlAgilityPack.HtmlDocument htmlDoc = new HtmlAgilityPack.HtmlDocument();
htmlDoc.LoadHtml(htmlCode);
var chapterLink = htmlDoc.DocumentNode.SelectNodes(@"//div[@id='chapterlist']//a/@href");
var chapterName = htmlDoc.DocumentNode.SelectNodes(@"//div[@id='chapterlist']//a/@href/following-sibling::text()[1]").Reverse().ToList();
for (int i = 0; i < chapterLink.Count; i++)
{
var link = "http://" + Uri.Host + chapterLink[i].GetAttributeValue("href", "not found");
var chName = chapterName[i].OuterHtml.Replace(" : ", "");
var chapterNumber = chapterLink[i].InnerText;
Chapters.Add(new Tuple<string, string, string, string>(link, chName, chapterNumber, mangaName));
}
}
My expected result is that Chapters (a property of type List containing a Tuple) gets populated after I am done with extracting the information I need from the html source code. I want to do this asynchronously because for larger amounts of urls this process could take a while and I don't want to block the UI thread (it is a windows form app).
What did I do wrong?
Upvotes: 0
Views: 770
Reputation: 13224
But
LoadAllChaptersAsync
throws an error:this async method lacks await operators...
That is because your LoadAllChaptersAsync
method does not perform any asynchronous operations, and does not await
any.
A common misconception is that using the async
(or await
) keyword in a method somehow magically creates a new task on a different thread. It does not.
I want to do this asynchronously because for larger amounts of urls this process could take a while and I don't want to block the UI thread (it is a windows form app).
You could change your method to return a new Task
that performs work in the background, and that will return a new list with all of the newly created "chapters" on task completion. As in:
private Task<List<Tuple<string, string, string, string>>>
LoadAllChaptersAsync(string htmlCode, string mangaName)
{
return Task.Run(() {
var newChapters = new List<Tuple<string, string, string, string>>();
// ...
return newChapters;
});
}
This task can be then awaited, there is no need to mark your method that does not do anything async as async
.
var newChapters = await LoadAllChaptersAsync(htmlCode, Path.GetFileNameWithoutExtension(item));
Chapters.AddRange(newChapters);
Additional improvements
There are two improvements that we could make to the above solution. We can incorporate a few best practices for tasks that are primarily CPU bound and whose implementation does not include async/awaits.
CancellationToken
connected to a CancellationTokenSource
.For your code you might want to supply a "Stop Loading" button in the UI, and when clicked, use the following to cancel the work performed in the LoadAllChaptersAsync
method:
private async void btnStopLoading_Click(object sender, EventArgs e)
{
if (_loadChaptersCancelSource != null)
_loadChaptersCancelSource.Cancel();
}
Then your original code could be changed to:
private async void btnLoad_Click(object sender, EventArgs e)
{
if (_loadChaptersCancelSource == null)
{
var wasCancelled = false;
_loadChaptersCancelSource = new CancellationTokenSource();
try
{
var token = _loadChaptersCancelSource.Token;
foreach (var item in myCollectionOfUrls)
{
// stop if cancellation was requested.
token.ThrowIfCancellationRequested();
Uri tempUri = new Uri(item);
Uri = tempUri; // Uri is a property
// also modified to be cancellable.
string htmlCode = await LoadHtmlCodeAsync(Uri, token);
// client decides to run as a background task
var newChapters = await Task.Run(() =>
LoadAllChapters(htmlCode, Path.GetFileNameWithoutExtension(item), token),
token);
Chapters.AddRange(newChapters);
}
}
catch (OperationCanceledException)
{
wasCancelled = true;
}
catch (AggregateException ex)
{
if (!ex.InnerExceptions.Any(e => typeof(OperationCanceledException).IsAssignableFrom(e.GetType())))
throw; // not cancelled, different error.
wasCancelled = true;
}
finally
{
var cts = _loadChaptersCancelSource;
_loadChaptersCancelSource = null;
cts.Dispose();
}
if (wasCancelled)
; // Show a message ?
}
}
And your LoadAllChapters
could be a regular synchronous method, that allows for cooperative cancellation:
private List<Tuple<string, string, string, string>>
LoadAllChapters(string htmlCode, string mangaName, CancellationToken cancelToken)
{
HtmlAgilityPack.HtmlDocument htmlDoc = new HtmlAgilityPack.HtmlDocument();
htmlDoc.LoadHtml(htmlCode);
// Don't continue if cancelation is requested
cancelToken.ThrowIfCancellationRequested();
var chapterLink = htmlDoc.DocumentNode.SelectNodes(@"//div[@id='chapterlist']//a/@href");
var chapterName = htmlDoc.DocumentNode.SelectNodes(@"//div[@id='chapterlist']//a/@href/following-sibling::text()[1]").Reverse().ToList();
var newChapters = new List<Tuple<string, string, string, string>>();
for (int i = 0; i < chapterLink.Count; i++)
{
// Stop the loop if cancellation is requested.
cancelToken.ThrowIfCancellationRequested();
var link = "http://" + Uri.Host + chapterLink[i].GetAttributeValue("href", "not found");
var chName = chapterName[i].OuterHtml.Replace(" : ", "");
var chapterNumber = chapterLink[i].InnerText;
newChapters.Add(new Tuple<string, string, string, string>(link, chName, chapterNumber, mangaName));
}
return newChapters;
}
A very similar approach (that does involve async operations) with some additional explanations can be found here: "Async Cancellation: Bridging between the .NET Framework and the Windows Runtime".
Upvotes: 3
Reputation: 10708
When you use async
, you're not making a method which immediately returns a Task
representing its own work - instead, an async
method will return a Task
representing the rest of its work when you use an await
operator. As mentioned in the answer from Alex, this can be done via Task.Run
, but it can also be done from within a method by await
ing the Task.Yield()
function, which returns immediately.
Note that in UI applications, you will usually have your SynchronizationContext
set up to only use the one thread - it may be necessary to use ConfigureAwait
to ensure you're on another thread. Thus:
await Task.Yield().ConfigureAwait(false);
This is only a possibility, though - it's best to test by making calls to Thread.CurrentThread
and checking the ManagedThreadId
to ensure you're on a a particular thread or another.
Upvotes: 1