비동기 파일 액세스(C#)
파일에 액세스하는 비동기 기능을 사용할 수 있습니다. 비동기 기능을 사용하면 콜백을 사용하거나 여러 메서드 또는 람다 식에서 코드를 분할하지 않고도 비동기 메서드를 호출할 수 있습니다. 동기 코드를 비동기로 만들려면 동기 메서드 대신 비동기 메서드를 호출하고 몇 가지 키워드를 코드에 추가하면 됩니다.
파일 액세스 호출에 비동기를 추가하는 이유로 다음을 고려할 수 있습니다.
- 비동기는 UI 애플리케이션의 응답성을 개선합니다. 작업을 시작하는 UI 스레드가 다른 작업을 수행할 수 있기 때문입니다. UI 스레드가 시간이 오래 걸리는(예: 50밀리초 이상) 코드를 실행해야 하는 경우, I/O가 완료되고 UI 스레드가 키보드와 마우스 입력 및 기타 이벤트를 다시 처리할 수 있을 때까지 UI가 정지할 수 있습니다.
- 비동기는 스레드의 필요성을 줄임으로써 ASP.NET 및 기타 서버 기반 애플리케이션의 확장성을 개선합니다. 애플리케이션이 각 응답에 전용 스레드를 사용하고 1,000개의 요청이 동시에 처리되는 경우 수천 개의 스레드가 필요합니다. 비동기 작업은 대기 중에 종종 스레드를 사용할 필요가 없습니다. 끝날 때 기존 I/O 완료 스레드를 잠시 사용합니다.
- 파일 액세스 작업의 대기 시간은 현재 조건에서 매우 낮을 수 있지만 나중에 대기 시간이 크게 늘어날 수 있습니다. 예를 들어 전 세계에 있는 서버로 파일을 이동할 수 있습니다.
- 비동기 기능을 사용할 경우에는 추가되는 오버헤드가 적습니다.
- 비동기 작업은 쉽게 병렬로 실행할 수 있습니다.
적절한 클래스 사용
이 항목의 간단한 예제는 File.WriteAllTextAsync 및 File.ReadAllTextAsync를 보여 줍니다. 파일 I/O 작업에 대한 한정된 제어를 위해 운영 체제 수준에서 비동기 I/O를 일으키는 옵션이 있는 FileStream 클래스를 사용합니다. 이 옵션을 사용하면 많은 경우 스레드 풀 스레드가 차단되는 것을 방지할 수 있습니다. 이 옵션을 사용하도록 설정하려면 생성자 호출에서 useAsync=true
또는 options=FileOptions.Asynchronous
인수를 지정합니다.
파일 경로를 지정하여 직접 여는 경우에는 StreamReader 및 StreamWriter와 함께 해당 옵션을 사용할 수 없습니다. 그러나 FileStream 클래스에서 열린 Stream을 제공하면 이 옵션을 사용할 수 있습니다. 대기 중에는 UI 스레드가 차단되지 않으므로 스레드 풀 스레드가 차단된 경우에도 UI 앱에서 비동기 호출이 더 빠릅니다.
텍스트 쓰기
다음 예제에서는 파일에 텍스트를 씁니다. 각 await 문에서 메서드가 즉시 종료됩니다. 파일 I/O가 완료되면 await 문 뒤에 오는 문에서 메서드가 다시 시작됩니다. async 한정자는 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의 계산에서 값이 생성됩니다. ReadAsync 메서드는 Task<Int32>를 반환하므로 작업이 완료된 후 await 평가에서 Int32
값 numRead
가 생성됩니다. 자세한 내용은 비동기 반환 형식(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
다음 예제에서는 10개의 텍스트 파일을 작성하여 병렬 처리를 보여 줍니다.
간단한 예
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);
문은 메서드를 종료하고, 파일 처리가 모든 작업에 대해 완료되면 메서드 내에서 다시 시작됩니다.
이 예제는 작업이 완료된 후 finally
블록에서 모든 FileStream 인스턴스를 닫습니다. 대신 using
문에 각 FileStream
이 만들어진 경우 작업이 완료되기 전에 FileStream
이 삭제될 수 있습니다.
성능 향상은 거의 대부분 비동기 처리가 아닌 병렬 처리에서 발생합니다. 비동기의 장점은 다중 스레드를 묶어 두지 않고 사용자 인터페이스 스레드를 묶어 두지 않는다는 것입니다.
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();
}
}
}
WriteAsync 및 ReadAsync 메서드를 사용하는 경우 중간에 작업을 취소하는 데 사용할 수 있는 CancellationToken을 지정할 수 있습니다. 자세한 내용은 관리형 스레드의 취소를 참조하세요.
참고 항목
.NET