비동기 파일 액세스(C#)

비동기 기능을 사용하여 파일에 액세스하면 콜백을 사용하거나 여러 메서드 또는 람다 식 간에 코드를 분할하지 않고 비동기 메서드를 호출할 수 있습니다. 동기 코드를 비동기로 만들려면 동기 메서드 대신 비동기 메서드를 호출하고 코드에 몇 가지 키워드를 추가합니다.

다음과 같은 이유로 파일 액세스 호출에 비동기를 추가하는 것이 좋습니다.

  • 비동기 기능을 사용하면 작업을 시작하는 UI 스레드가 다른 작업을 수행할 수 있으므로 UI 애플리케이션의 응답성이 향상됩니다. UI 스레드가 시간이 오래 걸리는 코드를 실행해야 하는 경우(예: 50밀리초 초과) I/O가 완료될 때까지 UI가 중지되고 UI 스레드가 키보드 및 마우스 입력 및 기타 이벤트를 다시 처리할 수 있습니다.
  • 비동기에서는 스레드의 필요성을 줄여 ASP.NET 및 기타 서버 기반 애플리케이션의 확장성을 향상시킵니다. 애플리케이션이 응답당 전용 스레드를 사용하고 천 개의 요청이 동시에 처리되는 경우 1,000개의 스레드가 필요합니다. 비동기 작업은 대기 중에 스레드를 사용할 필요가 없는 경우가 많습니다. 마지막에 기존 I/O 완성 스레드를 간략하게 사용합니다.
  • 파일 액세스 작업의 대기 시간은 현재 조건에서 매우 낮을 수 있지만 대기 시간은 나중에 크게 증가할 수 있습니다. 예를 들어 파일이 전 세계의 서버로 이동될 수 있습니다.
  • 비동기 기능 사용 시 추가된 오버헤드는 작습니다.
  • 호출 스레드를 차단하지 않고 여러 비동기 I/O 작업을 실행할 수 있습니다.

적절한 클래스 사용

이 항목의 간단한 예제에서는 File.WriteAllTextAsyncFile.ReadAllTextAsync를 보여줍니다. 파일 I/O 작업을 세부적으로 제어하려면 운영 체제 수준에서 비동기 I/O를 발생시키는 옵션이 있는 클래스를 사용합니다 FileStream . 이 옵션을 사용하면 대부분의 경우 스레드 풀 스레드의 차단을 방지할 수 있습니다. 이 옵션을 사용하려면 생성자 호출 시 useAsync=true 또는 options=FileOptions.Asynchronous 인수를 지정하십시오.

파일 경로를 지정하여 직접 여는 경우 이 옵션을 StreamReaderStreamWriter 사용할 수 없습니다. FileStream 클래스가 연 Stream을 제공하는 경우, 이 옵션을 사용할 수 있습니다. 대기하는 동안 UI 스레드가 차단되지 않으므로 스레드 풀 스레드가 차단된 경우에도 UI 앱에서 비동기 호출이 더 빠릅니다.

텍스트 쓰기

다음 예제에서는 파일에 텍스트를 씁니다. 각 await 문에서 메서드는 즉시 종료됩니다. 파일 I/O가 완료되면 await 문 뒤에 있는 문에서 메서드가 다시 시작됩니다. 비동기 한정자는 await 문을 사용하는 메서드 정의에 있습니다.

간단한 예제

public async Task SimpleWriteAsync()
{
    string filePath = "simple.txt";
    string text = $"Hello World";

    await File.WriteAllTextAsync(filePath, text);
}

유한 컨트롤 예제

public async Task ProcessWriteAsync()
{
    string filePath = "temp.txt";
    string text = $"Hello World{Environment.NewLine}";

    await WriteTextAsync(filePath, text);
}

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

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

    await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
}

원래 예제에는 다음 두 문의 축소인 문이 await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);있습니다.

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

첫 번째 문은 작업을 반환하고 파일 처리를 시작합니다. await가 있는 두 번째 문은 메서드를 즉시 종료하고 다른 작업을 반환합니다. 파일 처리가 완료되면, 제어 흐름은 await 다음 문으로 반환됩니다.

텍스트 읽기

다음 예제에서는 파일에서 텍스트를 읽습니다.

간단한 예제

public async Task SimpleReadAsync()
{
    string filePath = "simple.txt";
    string text = await File.ReadAllTextAsync(filePath);

    Console.WriteLine(text);
}

유한 컨트롤 예제

텍스트는 버퍼링되어 이 경우 StringBuilder에 저장됩니다. 이전 예제와 달리 await의 평가는 값을 생성합니다. 메서드는 ReadAsyncTask<Int32>를 반환하므로, 작업 완료 후 await 평가는 Int32numRead 값을 생성합니다. 자세한 내용은 비동기 반환 형식(C#)을 참조하세요.

public async Task ProcessReadAsync()
{
    try
    {
        string filePath = "temp.txt";
        if (File.Exists(filePath) != false)
        {
            string text = await ReadTextAsync(filePath);
            Console.WriteLine(text);
        }
        else
        {
            Console.WriteLine($"file not found: {filePath}");
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

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

    var 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();
}

여러 비동기 I/O 작업

다음 예제에서는 여러 비동기 쓰기 작업을 시작합니다. 런타임은 이러한 작업을 큐에 대기하고, 기본 구현은 플랫폼 및 구성에 따라 운영 체제(OS) 비동기 I/O 또는 스레드 풀 스레드를 사용할 수 있으므로 실제 동시성은 OS 및 하드웨어에 따라 달라집니다.

간단한 예제

public async Task SimpleParallelWriteAsync()
{
    string folder = Directory.CreateDirectory("tempfolder").Name;
    IList<Task> writeTaskList = new List<Task>();

    for (int index = 11; index <= 20; ++ index)
    {
        string fileName = $"file-{index:00}.txt";
        string filePath = $"{folder}/{fileName}";
        string text = $"In file {index}{Environment.NewLine}";

        writeTaskList.Add(File.WriteAllTextAsync(filePath, text));
    }

    await Task.WhenAll(writeTaskList);
}

유한 컨트롤 예제

각 파일에 대해 메서드는 WriteAsync 작업 목록에 추가된 작업을 반환합니다. 이 문은 await Task.WhenAll(tasks); 메서드를 종료하고 모든 작업에 대해 파일 처리가 완료되면 메서드 내에서 다시 시작됩니다.

이 예제에서는 작업이 완료된 후 블록의 FileStream 모든 finally 인스턴스를 닫습니다. 각 FileStreamusing 문에서 만들어졌다면, FileStream이 작업이 완료되기 전에 삭제될 수 있습니다.

비동기 접근 방식은 I/O가 보류 중인 동안 호출 스레드를 차단하지 않습니다. 대부분의 경우 처리량 향상은 OS, 하드웨어 및 일부 플랫폼에서 스레드 풀 제한 및 일정 예약과 같은 .NET 런타임 동작에 따라 달라집니다.

public async Task ProcessMultipleWritesAsync()
{
    IList<FileStream> sourceStreams = new List<FileStream>();

    try
    {
        string folder = Directory.CreateDirectory("tempfolder").Name;
        IList<Task> writeTaskList = new List<Task>();

        for (int index = 1; index <= 10; ++ index)
        {
            string fileName = $"file-{index:00}.txt";
            string filePath = $"{folder}/{fileName}";

            string text = $"In file {index}{Environment.NewLine}";
            byte[] encodedText = Encoding.Unicode.GetBytes(text);

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

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

            writeTaskList.Add(writeTask);
        }

        await Task.WhenAll(writeTaskList);
    }
    finally
    {
        foreach (FileStream sourceStream in sourceStreams)
        {
            sourceStream.Close();
        }
    }
}

WriteAsyncReadAsync 메서드를 사용할 때 작업을 중간에 취소하도록 지정할 수 있는 CancellationToken를 사용하십시오. 자세한 내용은 관리되는 스레드의 취소를 참조하세요.

참고하십시오