Using Async for File Access

by Alan Berman

The new Async feature in Visual Studio makes it easy to code asynchronous method calls. To make synchronous code asynchronous, you can simply call an asynchronous method instead of a synchronous method and add a few keywords to the code, as shown in the examples below.  You no longer need to define continuations to capture what happens when the asynchronous operation finishes, which can otherwise complicate the code.

Even with the simplicity, why make file access calls asynchronous?  Aren't they fast enough already?  Here are reasons to consider:

  • Asynchrony makes UI applications more responsive. It allows the UI thread that launches the operation to be used for other things. If the UI thread is blocked by executing code that takes a long time (i.e. more than, say, 50 milliseconds), the UI may freeze until the I/O is complete and the UI thread is again able to process keyboard and mouse input and other events.
  • Asynchrony improves the scalability of ASP.NET and other server-based applications by reducing the need for threads.  If file operations are synchronous and a hundred file access operations are happening at once, then a hundred threads are needed.  Asynchronous operations often do not require use of a thread during the wait.  They use the existing I/O completion thread briefly at the end.
  • Even though a file access operation has very low latency now, it's possible that latency may greatly increase in the future. For example, a file may later be moved to a server that's across the world.
  • The added overhead of using the Async feature is small.
  • Asynchronous tasks can easily be run in parallel.

Running the Examples

Note: The examples in this blog do not apply to Metro style apps, which are Windows 8 applications that are tailored for touch interaction and are full screen.  For information on using Async for file access in Metro style apps, see .NET for Metro style apps overview and File and Stream I/O.  For some good examples of Metro file I/O, you can download the File Access Sample provided on the Dev Center.

The examples below run in either Visual Studio 11 Developer Preview or the Async CTP for Visual Studio 2010 SP1.  Both can be accessed from Visual Studio Asynchronous Programming. If you use the Async CTP for Visual Studio 2010, add a reference to AsyncCtpLibrary.dll, which is in My Documents\Microsoft Visual Studio Async CTP\Samples.

Include the following using statements in the console examples below.

 using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;

Use of the FileStream Class

The examples below use the FileStream class, which has an option that causes asynchronous I/O to occur at the operating system level.  In many cases, this will avoid blocking a ThreadPool thread.  To enable this option, you must specify the useAsync=true or options=FileOptions.Asynchronous argument in the constructor call.

StreamReader and StreamWriter do not have this option if you open them directly by specifying a file path. StreamReader/Writer do have this option if you provide them a Stream that was opened by the FileStream class. Note that asynchrony provides a responsiveness advantage in UI apps even if a thread pool thread is blocked, since the UI thread is not blocked during the wait.

Writing Text

The following example writes text to a file.  At each await statement, the method immediately exits.  When the file I/O is complete, the method resumes at the statement following the await statement.  Note that the async modifier is in the definition of methods that use the await statement.

 static void Main(string[] args)
{
    ProcessWrite().Wait();
    Console.Write("Done ");
    Console.ReadKey();
}

static Task ProcessWrite()
{
    string filePath = @"c:\temp2\temp2.txt";
    string text = "Hello World\r\n";

    return WriteTextAsync(filePath, text);
}

static async Task WriteTextAsync(string filePath, string text)
{
    byte[] encodedText = Encoding.Unicode.GetBytes(text);

    using (FileStream sourceStream = new FileStream(filePath,
        FileMode.Append, FileAccess.Write, FileShare.None,
        bufferSize: 4096, useAsync: true))
    {
        await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
    };
}

Reading Text

The following example reads text from a file.  The text is buffered and, in this case, placed into a StringBuilder.  Unlike in the previous example, the evaluation of the await produces a value.  The ReadAsync method returns a Task<Int32>, so the evaluation of the await produces an Int32 value (numRead) that is returned after the operation completes..

 static void Main(string[] args)
{
    ProcessRead().Wait();
    Console.Write("Done ");
    Console.ReadKey();
}

static async Task ProcessRead()
{
    string filePath = @"c:\temp2\temp2.txt";

    if (File.Exists(filePath) == false)
    {
        Console.WriteLine("file not found: " + filePath);
    }
    else {
        try {
            string text = await ReadTextAsync(filePath);
            Console.WriteLine(text);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

static async Task<string> ReadTextAsync(string filePath)
{
    using (FileStream sourceStream = new FileStream(filePath,
        FileMode.Open, FileAccess.Read, FileShare.Read,
        bufferSize: 4096, useAsync: true))
    {
        StringBuilder sb = new StringBuilder();

        byte[] buffer = new byte[0x1000];
        int numRead;
        while ((numRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
        {
            string text = Encoding.Unicode.GetString(buffer, 0, numRead);
            sb.Append(text);
        }

        return sb.ToString();
    }
}

Control Flow

While it's easy to make method calls asynchronous, it helps to know what actually happens when the program reaches an await statement.

The original example had the statement "await sourceStream.WriteAsync...".  That's a contraction of  these two statements:  "Task theTask = sourceStream.WriteAsync…" and "await theTask".  The first statement causes a task to be returned and file processing to start.  The second statement with the await causes the method to immediately exit (returning a different task).  When the file processing later completes, execution returns to the statement following the await.

The following example does the same thing as the "Writing Text" example, however it splits the await statement into two statements and adds console output to assist in understanding the sequence.

 static void Main(string[] args)
{
    ProcessWrite2().Wait();
    Console.Write("Done ");
    Console.ReadKey();
}

static Task ProcessWrite2()
{
    string filePath = @"c:\temp2\temp2.txt";
    string text = "Hello World\r\n";

    Task theTask = WriteTextAsync2(filePath, text);

    ShowInfo(theTask, "In ProcessWrite2 before wait");
    return theTask;
}

static async Task WriteTextAsync2(string filePath, string text)
{
    byte[] encodedText = Encoding.Unicode.GetBytes(text);

    using (FileStream sourceStream = new FileStream(filePath,
        FileMode.Append, FileAccess.Write, FileShare.None,
        bufferSize: 4096, useAsync: true))
    {
        Task theTask = sourceStream.WriteAsync(encodedText, 0, encodedText.Length);

        ShowInfo(theTask, "In WriteTextAsync2 before await");
        await theTask;
        ShowInfo(theTask, "In WriteTextAsync2 after await");
    };
}

static void ShowInfo(Task theTask, string message)
{
    Console.Write(message.PadRight(32) + " ");
    Console.Write("task " + theTask.Id.ToString() + " ");
    Console.Write("\n");
}

// Console output:
// In WriteTextAsync2 before await task 1
// In ProcessWrite2 before wait task 2
// In WriteTextAsync2 after await task 1
// Done

 

Parallel Asynchronous I/O

The following example demonstrates parallel processing by writing 10 text files.  For each file, the WriteAsync method returns a task.  Each task is added to a list of tasks.  The "await Task.WhenAll(tasks)" statement exits the method and resumes within the method when file processing is complete for all of the tasks.

For the Visual Studio 2010 Async CTP, use TaskEx.WhenAll instead of Task.WhenAll.

The example closes all FileStream instances in a finally block after the tasks are complete. If each FileStream was instead created in a using statement, the FileStream might be disposed of before the task was complete.

Note that any speedup is almost entirely from the parallel processing and not the asynchronous processing.  The advantages of asynchrony are that it does not tie up multiple threads, and that it does not tie up the user interface thread.

 static void Main(string[] args)
{
    ProcessWriteMult().Wait();
    Console.Write("Done ");
    Console.ReadKey();
}

static async Task ProcessWriteMult()
{
    string folder = @"c:\temp2\";
    List<Task> tasks = new List<Task>();
    List<FileStream> sourceStreams = new List<FileStream>();

    try {
        for (int index = 1; index <= 10; index++)
        {
            string text = "In file " + index.ToString() + "\r\n";

            string fileName = "thefile" + index.ToString("00") + ".txt";
            string filePath = folder + fileName;

            byte[] encodedText = Encoding.Unicode.GetBytes(text);

            FileStream sourceStream = new FileStream(filePath,
                FileMode.Append, FileAccess.Write, FileShare.None,
                bufferSize: 4096, useAsync: true);

            Task theTask = sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
            sourceStreams.Add(sourceStream);

            tasks.Add(theTask);
       }
    
        await Task.WhenAll(tasks);
    }

    finally {
        foreach (FileStream sourceStream in sourceStreams)
        {
            sourceStream.Close();
        }
    }
}

The WriteAsync and ReadAsync methods allow you to specify a CancellationToken, which can be used to cancel the operation mid-stream.  The Simultaneous Async Tasks blog has an example of cancellation.  Additional information is provided in the Cancellation topic.

Thanks to Stephen Toub for providing technical guidance.

See Also 

Resources