3

I'm trying to get my head around the new async features in C#, and so far the strangest thing I've noticed is that every example for async features has a function that awaits another async function defined in the framework, but none of them have custom code.

For example, what I want to is create an object from each line in a text file, but asynchronously, so that the UI thread doesn't freeze:

async Task Read()
{
    string[] subjectStrings = File.ReadAllLines(filePath);
    for (int i = 0; i < subjectStrings.Length; i++)
    {
        Task<Subject> function = new Task<Subject>(code => new Subject((string)code), subjectStrings[i]);
        try
        {
            Subject subject = await function;
            subjects.Add(subject);
        }
        catch (Exception ex)
        {
            debugWriter.Write("Error in subject " + subjectStrings[i]);
            continue;
        }
    }
}

As you can see, I define a task that creates a new Subject object based on a line in the text file, and then await this task. If I do this, the debugger gets up to the await line, and then just stops. No more code is run, as far as I know.

If I was using the old async features, I'd just use Task.ContinueWith() and add a callback lambda that adds the subject to the list, and be on my way.

So my questions are:

  1. Why does this code not work? How are you supposed to make a custom async method that doesn't use any async methods itself?
  2. How are you supposed to use async methods? You can't use await unless you're in an async function, and you're not supposed to call an async method without await, so how do you first call that method from a synchronous method?
Ondrej Janacek
  • 12,486
  • 14
  • 59
  • 93
Migwell
  • 18,631
  • 21
  • 91
  • 160

5 Answers5

5

You're not starting the task - so it will never finish.

Use Task.Run instead of new Task and it will create and start the task for you.

Note that you're still reading the file synchronously, which isn't ideal... and if your Subject constructor really takes that long to complete, I'd question whether it should be a constructor.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    @Luaan the last point was that constructors shouldn't do expensive operations, not that they should do them asynchronously. – Steven Liekens Feb 21 '14 at 12:42
  • @StevenLiekens No misunderstanding there. That's the point I was trying to reinforce - if you need to run constructors parallely, you're doing something terribly wrong. A lot of things most likely. – Luaan Feb 21 '14 at 12:45
  • It's not so much that each constructor takes a long time as there are several thousand of these constructors that I want to run without blocking the UI thread. I could use a separate .Read() function but surely that's less clear than a constructor? – Migwell Feb 21 '14 at 12:48
  • @Miguel: It sounds like you should consider performing the *whole* "read file, call constructors" block within a single task. I'm assuming that `subjects.Add(subject)` affects the UI though? (If so, do that with all the subjects at the end.) – Jon Skeet Feb 21 '14 at 12:49
  • Yes I could (and probably should) do that. And yes, subjects is an ObservableCollection that's bound to a WPF ListBox. – Migwell Feb 21 '14 at 12:51
1

Why does this code not work? How are you supposed to make a custom async method that doesn't use any async methods itself?

Use await Task.Run or await Task.Factory.StartNew to create and run the task. Calling new Task would create a task which isn't started yet. In most cases, this is unnecessary, but you can call Start on tasks created this way.

How are you supposed to use async methods? You can't use await unless you're in an async function, and you're not supposed to call an async method without await, so how do you first call that method from a synchronous method?

Appropriate "root" async call depends on the type of application:

  • In console application: Wait on returned Task.

  • In GUI application: use async void event handler.

  • In ASP.NET MVC: controllers can return Task.

Athari
  • 33,702
  • 16
  • 105
  • 146
  • Can you eleborate on how you'd do this in a console app? What do you mean by wait on a returned task? – Migwell Feb 21 '14 at 12:42
  • `Main` method in Console app can't be asynchronous. So if you need to call `async Task f()` method, you call it `f().Wait()` instead of `await f()`. This blocks the calling thread until the call completes. – Athari Feb 21 '14 at 12:44
1

How are you supposed to make a custom async method that doesn't use any async methods itself?

You don't. If the method has no asynchronous work to do, it should be synchronous; it should not be async.

At the core, all async methods come down to one of two approaches. They either queue work to the thread pool via something like Task.Run (not recommended for library code), or they perform true asynchronous work via TaskCompletionSource<T> or a shortcut such as Task.Factory.FromAsync.

How are you supposed to use async methods? You can't use await unless you're in an async function, and you're not supposed to call an async method without await, so how do you first call that method from a synchronous method?

You don't. Ideally, you should be async all the way. Console applications are an exception to this rule; they have to have a synchronous Main. But you should use async all the way for WinForms, WPF, Silverlight, Windows Store, ASP.NET MVC, WebAPI, SignalR, iOS, Android, and Windows Phone applications, as well as unit tests.

You can use async methods via await and combinators such as Task.WhenAll and Task.WhenAny. This is the most common way of using async methods, but not the only one; e.g., you can call an async method and consume it as an IObservable<T>.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • "If the method has no asynchronous work to do, it should be synchronous" I do have asynchronous work. I want to read this file into an array, *while* allowing the user to do whatever else they want in the UI. How should that be synchronous? – Migwell Feb 21 '14 at 12:55
  • And if every method ever should be async, why isn't async functionality automatically implemented in WPF or Winforms? – Migwell Feb 21 '14 at 13:03
  • @Miguel: If you're doing file I/O, then that's asynchronous, and you should make your method asynchronous. The point I was making is that *not* every method should be async; it should only be asynchronous if it has asynchronous work to do. – Stephen Cleary Feb 21 '14 at 13:14
  • But it shouldn't matter if I'm doing I/O or not. Even if I was doing arbitrary math equations, *alongside* running the UI, shouldn't that be asynchronous? – Migwell Feb 21 '14 at 13:19
  • @Miguel math equations are probably the best example of procedures that can benefit from multiple processor cores, depending on their complexity. – Steven Liekens Feb 21 '14 at 13:25
  • 1
    @Miguel: Math is an example of synchronous work. If you want to offload that from your UI, then you could use `Task.Run` to treat it as asynchronous. But you don't want to create a library that exposes an asynchronous API to do math, because that work isn't naturally asynchronous. [I explain why on my blog](http://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html). – Stephen Cleary Feb 21 '14 at 13:27
  • I don't quite follow. Shouldn't *all* processing that's not UI related ideally be done on a separate thread? Why would you *ever* want to block the UI thread? – Migwell Feb 21 '14 at 13:52
  • @Miguel one reason is because a free background thread might not always be immediately available. This still won't freeze your UI, but overall it will take your program longer to finish executing the code. /EDIT: actually that might be a misunderstanding on my part. *blush* – Steven Liekens Feb 21 '14 at 13:58
  • @Miguel: For example, if you're already on a background thread, then you don't want to call `Task.Run` to execute math code; you'd just want to execute it directly. As I explain in the blog post I linked to, this is much more of a problem on ASP.NET. – Stephen Cleary Feb 21 '14 at 13:58
0

You're confusing waiting and working. Waiting uses async/await, working does not. This still means you can await a CPU-bound task, but you have to run it manually, eg.:

var result = await Task.Run(YourLongOperation);

A distinction that helps me understand this on a gut level is, that waiting is cooperative - I voluntarily give up my share of CPU time, because I don't actually need it. Working on the other hand has to run parallel.

In a normal scenario, using only inherently asynchronous async/awaits, there doesn't have to be more than the single thread you started with. It is usually a bad idea to combine CPU-bound operations with I/O-bound ones (CPU-bound will block, unless you explicitly run the task parallely).

Luaan
  • 62,244
  • 7
  • 97
  • 116
0

I won't bore you with Yet Another Technical Explanation™, instead let me show you a practical example based on your code sample.

Starting with a synchronous version that does all the work on the calling thread.

class SubjectFactory
{
    public IEnumerable<Subject> Read(string filePath)
    {
        string[] subjectStrings = File.ReadAllLines(filePath);

        return Parse(subjectStrings);
    }

    private IEnumerable<Subject> Parse(IEnumerable<string> subjects)
    {
        string code = "XYZ";

        foreach ( var subject in subjects )
        {
            yield return new Subject(code, subject);
        }
    }
}

Assuming that the constructor in Subject is lightweight, the biggest bottleneck is File.ReadAllLines. Why? Because disk I/O is inherently slow.

So how do you go about wrapping that in a task?

If the framework had a File.ReadAllLinesAsync() method, you could await that and be done with it.

public async Task<IEnumerable<Subject>> ReadAsync(string filePath)
{ // Doesn't exist!
    string[] subjectStrings = await File.ReadAllLinesAsync(filePath);

    return this.Parse(subjectStrings);
}

Unfortunately, life is difficult, and so is parallel programming. It's looking like you'll have to reinvent the wheel.

private async Task<string[]> ReadAllLinesAsync(string filePath)
{
    ArrayList allLines = new ArrayList();

    using ( var streamReader = new StreamReader(File.OpenRead(filePath)) )
    {
        string line = await streamReader.ReadLineAsync();

        allLines.Add(line);
    }

    return (string[]) allLines.ToArray(typeof(string));
}

Now you can do the same thing as before, but use your custom method ReadAllLinesAsync().

public async Task<IEnumerable<Subject>> ReadAsync(string filePath)
{
    // call with 'this' instead of 'File'
    string[] subjectStrings = await this.ReadAllLinesAsync(filePath);

    return Parse(subjectStrings);
}

With all that in place, in your WPF app all you got to do is this:

var filePath             = @"X:\subjects\";
var subjectFactory       = new SubjectFactory();
var subjectsCollection   = await subjectFactory.ReadAsync(filePath);
var observableCollection = new ObservableCollection<Subject>(subjectsCollection);
Steven Liekens
  • 13,266
  • 8
  • 59
  • 85