2

I have a bunch of tasks that I want to be able to run in order. The thing is, they involve a lot of disk reading and I will need to do some disk reading/writing in between using each one, so I'd like to be able to create a bunch of tasks for reading from the disk (and returning the result), but not starting them until I'm ready to.

Because of this, I can't use Task.Run or Task.Factory.StartNew. It is my understanding that this is what the Task constructor was for.

Ex:

public async Task<IEnumerable<Task<byte[]>>> ReadAllFiles()
{
    var folder = await ApplicationData.Current.LocalFolder;
    var files = await folder.GetFilesAsync();
    var fileTasks = files.Select(
        file => new Task<Task<byte[]>>(
            async () => {
                return (await FileIO.ReadBufferAsync(file)).ToArray();
            }).Unwrap());
    return fileTasks;
}

Then, in my calling method I can go:

var readTasks = await ReadAllFiles();
foreach(var task in readTasks)
{
    task.Start(); // Throws exception
    var bytes = await task; // If the previous line is commented out, does not return
    // Do other stuff
}

Is there any way to do this? Right now task.Start() throws an exception: System.InvalidOperationException: Additional information: Start may not be called on a promise-style task..

Edit: It should be noted that there are some weirdnesses with how it looks currently. This is because in each step such as reading the file, I'm doing some additional logic processing on the data before returning it. This should not affect the code, so long as I can make asynchronous calls in the Task constructor method.

Edit2: It seems someone would like me to be clearer with what I am asking.

Is there a way that I can create a task with a return value, but not start it (likely using the Task<TResult> constructor) so that I may start it and await the value at another time. Currently I am receiving the exception included above.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Nate Diamond
  • 5,525
  • 2
  • 31
  • 57
  • Why are you trying to use `Start()` here in the first place? – svick Oct 12 '14 at 08:39
  • So, when you use `Task.Run` or `Task.Factory.StartNew`, it automatically starts the task that it creates. When you create a task with the `Task` constructor, it does not start it. This means that you have to call `Task.Start()` to start the task, otherwise if you await it, it will never return. – Nate Diamond Oct 12 '14 at 16:49
  • Yes, but your `ReadAllFiles()` returns a lazy sequence, so even if you used `Task.Run()`, the `Task`s would be started only when you iterate over them in your `foreach`. – svick Oct 12 '14 at 17:24
  • That I hadn't considered. I'll give it a try. So even if it returns a Task via Task.Run, it won't actually call the Select lambda until enumerated? – Nate Diamond Oct 12 '14 at 17:35
  • Exactly, that's how LINQ works in general. – svick Oct 12 '14 at 18:51

2 Answers2

1

I seem to have figured it out. Based on this answer.

It seems that I need to keep a copy of the outer Task and call Task.Start on it separately, then I can return the Unwrapped task as expected.

Example:

public async Task<IEnumerable<Task<byte[]>>> ReadAllFiles()
{
    var folder = await ApplicationData.Current.LocalFolder;
    var files = await folder.GetFilesAsync();
    var fileTasks = files.Select(
        file => {
            var wrappedTask = new Task<Task<byte[]>>(
            async () => {
                return (await FileIO.ReadBufferAsync(file)).ToArray();
            });
            
            var unwrappedTask = wrappedTask.Unwrap();
            wrappedTask.Start();
            return unwrappedTask;
        });
    return fileTasks;
}

This makes sure that the unwrapping has been completed and schedules the inner task (but does not start it).

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Nate Diamond
  • 5,525
  • 2
  • 31
  • 57
0

Task.Run, Task.Factory.StartNew, and Task.Task are all intended for CPU-bound tasks. Your tasks are I/O-bound.

Is there a way that I can create a task with a return value, but not start it ... so that I may start it and await the value at another time.

Certainly; what you want is actually a delegate with an async-compatible signature. In this case, a Func<Task<byte[]>>. I have more examples of async-compatible delegates on my blog.

So, your example could be:

public async Task<IEnumerable<Task<byte[]>>> ReadAllFiles()
{
  var folder = await ApplicationData.Current.LocalFolder;
  var files = await folder.GetFilesAsync();
  var fileReaders = files.Select(file => new Func<Task<byte[]>>(
      async () => await FileIO.ReadBufferAsync(file)).ToArray()));
  return fileReaders;
}

var readers = await ReadAllFiles();
foreach(var func in readers)
{
  var bytes = await func();
  // Do other stuff
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810