Background Tasks in Async Methods

In the next release of Halfwit RT (what, you hadn't heard of Halfwit RT?) you'll be able to start typing a username and have it auto-complete from a known list of users.

To populate this list initially, I wanted to get a list of all the people you follow as soon as you're authenticated. However, the act of fetching the users you follow takes some time, so I didn't want to "await" the call and prevent the authentication method from finishing until it was done.

The normal way to start a "fire and forget" background operation is to use Task.Factory.StartNew(), so at first I did something like this:

Task.Factory.StartNew(async () => /* fetch user names */);

This worked, but it raised a compiler warning:

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

This makes perfect sense - the compiler doesn't think you want to start a background task and not wait for it to finish inside an async method.

So how did I get around it? I got tricky. Maybe too tricky, as I'm sure there's a different (and possibly better) way to do this.

await Task.Factory.StartNew(() =>
{
    Task.Factory.StartNew(async () => { /* fetch user names */ });
});

So I'm awaiting a task that effectively finishes immediately, because all it does is start a background operation in another task.

It got a little trickier than that, though, because when the act of fetching the user names is complete, I need to add them to a List<String>, and I need that to happen on the primary thread to avoid issues. So I had to pass in the current TaskScheduler and make sure the continuation happened there.

Here's the code in its entirety, as called from my authentication method:

Task LoadFriendUserNamesAsync()
{
    return Task.Factory.StartNew(scheduler =>
    {
        Task.Factory.StartNew(async () =>
        {
            var friendsResponse = await Client.GetFriendIdsAsync();
            if (friendsResponse.StatusCode != System.Net.HttpStatusCode.OK) return new string[0];

            var usersResponse = await Client.LookupUsersAsync(friendsResponse.Result);
            if (usersResponse.StatusCode != System.Net.HttpStatusCode.OK) return new string[0];

            return usersResponse.Result.Select(u => u.ScreenName).ToArray();
        }).ContinueWith(t =>
        {
            if (t.IsFaulted || t.IsCanceled) return;

            var task = t.Result;
            if (task.IsFaulted || task.IsCanceled) return;

            foreach (var user in task.Result) UserNames.Add(user);

        }, (TaskScheduler)scheduler);
    }, TaskScheduler.Current);
}
c# .net async
Posted by: Matt Hamilton
Last revised: 15 Oct, 2024 10:34 PM History

Trackbacks

Comments

02 Apr, 2013 02:37 PM @ version 3

I usually like to use Task.Run() instead of Task.Factory.StartNew() (the difference is explained here by the all-knowing Stephen Toub), but it doesnt really matter in this case.

If you want to kick off a 'fire-and-forget' async task and avoid the compiler error, you can just set the return result to a variable like so:

SomeAsync();            // generates warning
var x = SomeAsync();    // no warning

If you have the most super strict code analysis rules enabled, that can result in a new warning: CA1804 Remove Unused locals (since 'x' is assigned but never used). If you are going to do x.ContinueWith(), then that avoids the issue, but I created a tiny helper method that takes a Task and returns void so I can avoid the compiler warning and the code analysis warning:

AsyncHelper.RunWithoutWaiting(SomeAsync());
02 Apr, 2013 09:07 PM @ version 3

This is why I create blog posts like this, Brian! Thanks for your solution!

I had no idea that simply assigning a task to a variable would suppress the warning. That's great to know.

I'll also do some reading on Task.Factory.StartNew vs Task.Run, so thanks for that too!

02 Apr, 2013 09:56 PM @ version 3

yeah, I guess the MS guys figure that if you are assigning the return value, then you must know what you are doing =) (it really does seem like a strange delineation to make, but who am I to question their wisdom?)

I like Task.Run() - if nothing else, it is shorter to type each time!

No new comments are allowed on this post.