Asynchroniczny dostęp do plików (C#)

Korzystając z funkcji asynchronicznej w celu uzyskania dostępu do plików, można wywołać metody asynchroniczne bez używania wywołań zwrotnych lub dzielenia kodu na wiele metod lub wyrażeń lambda. Aby uczynić kod synchroniczny asynchronicznym, użyj metody asynchronicznej zamiast metody synchronicznej i dodaj parę słów kluczowych do kodu.

Rozważ dodanie asynchronii do wywołań dostępu do plików z następujących powodów:

  • Asynchrony sprawia, że aplikacje interfejsu użytkownika są bardziej dynamiczne, ponieważ wątek interfejsu użytkownika uruchamiający operację może wykonywać inne zadania. Jeśli wątek interfejsu użytkownika musi wykonywać kod, który zajmuje dużo czasu (na przykład ponad 50 milisekund), interfejs użytkownika może zablokować się aż do momentu ukończenia operacji wejścia/wyjścia, a wątek interfejsu użytkownika będzie mógł ponownie przetwarzać dane wejściowe z klawiatury i myszy oraz inne zdarzenia.
  • Asynchronia zwiększa skalowalność ASP.NET i innych aplikacji opartych na serwerze, zmniejszając zapotrzebowanie na wątki. Jeśli aplikacja używa dedykowanego wątku na odpowiedź, a tysiąc żądań jest obsługiwanych jednocześnie, potrzebne są tysiące wątków. Operacje asynchroniczne często nie muszą używać wątku podczas oczekiwania. Używają oni istniejącego wątku zakończenia operacji I/O krótko pod koniec.
  • Opóźnienie operacji dostępu do pliku może być bardzo niskie w bieżących warunkach, ale opóźnienie może znacznie wzrosnąć w przyszłości. Na przykład plik może zostać przeniesiony na serwer na całym świecie.
  • Dodatkowe obciążenie związane z używaniem funkcji Async jest niewielkie.
  • Wiele asynchronicznych operacji I/O można uruchomić bez blokowania wątku wywołującego.

Używanie odpowiednich klas

W tym temacie w prostych przykładach pokazano File.WriteAllTextAsync i File.ReadAllTextAsync. Aby uzyskać precyzyjną kontrolę nad operacjami we/wy plików, użyj FileStream klasy , która ma opcję, która powoduje asynchroniczne operacje we/wy na poziomie systemu operacyjnego. Korzystając z tej opcji, można uniknąć blokowania wątku puli wątków w wielu przypadkach. Aby włączyć tę opcję, określ argument useAsync=true lub options=FileOptions.Asynchronous w wywołaniu konstruktora.

Nie można użyć tej opcji z opcją StreamReader i StreamWriter jeśli otworzysz je bezpośrednio, określając ścieżkę pliku. Można jednak użyć tej opcji, jeśli podasz im Stream, który otworzyła klasa FileStream. Wywołania asynchroniczne są szybsze w aplikacjach interfejsu użytkownika, nawet jeśli wątek puli wątków jest zablokowany, ponieważ wątek interfejsu użytkownika nie jest blokowany podczas oczekiwania.

Pisanie tekstu

W poniższych przykładach tekst jest zapisywany w pliku. Przy każdej instrukcji await metoda natychmiast przechodzi z działania. Po zakończeniu operacji I/O plików metoda zostanie wznowiona przy instrukcji, która następuje po instrukcji await. Modyfikator asynchroniczny jest w definicji metod korzystających z instrukcji await.

Prosty przykład

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

    await File.WriteAllTextAsync(filePath, text);
}

Przykład sterowania skończonego

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

Oryginalny przykład zawiera instrukcję await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);, która jest skurczem następujących dwóch instrukcji:

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

Pierwsza instrukcja zwraca zadanie i powoduje uruchomienie przetwarzania plików. Druga instrukcja z funkcją await powoduje natychmiastowe zakończenie metody i zwrócenie innego zadania. Po zakończeniu przetwarzania pliku wykonanie powróci do instrukcji, która następuje po await.

Odczytywanie tekstu

Poniższe przykłady odczytują tekst z pliku.

Prosty przykład

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

    Console.WriteLine(text);
}

Przykład sterowania skończonego

Tekst jest buforowany i w tym przypadku umieszczany w obiekcie StringBuilder. W przeciwieństwie do poprzedniego przykładu ocena oczekiwania generuje wartość. Metoda ReadAsync zwracaTask<Int32> wartość , więc ocena oczekiwania generuje Int32 wartość numRead po zakończeniu operacji. Aby uzyskać więcej informacji, zobacz Async Return Types (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();
}

Wiele asynchronicznych operacji wejścia/wyjścia

W poniższych przykładach rozpoczyna się wiele operacji zapisu asynchronicznego. Środowisko uruchomieniowe kolejkuje te operacje, a podstawowa implementacja może używać asynchronicznego I/O systemu operacyjnego lub wątków z puli wątków, w zależności od platformy i konfiguracji, zatem rzeczywista współbieżność zależy od systemu operacyjnego i sprzętu.

Prosty przykład

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

Przykład kontroli skończonej

Dla każdego pliku WriteAsync metoda zwraca zadanie dodane do listy zadań. Instrukcja await Task.WhenAll(tasks); powoduje zakończenie metody i ponowne jej uruchomienie po zakończeniu przetwarzania plików dla wszystkich zadań.

Przykład zamyka wszystkie FileStream wystąpienia w ramach finally bloku po zakończeniu zadań. Jeśli każda FileStream została utworzona w instrukcji using, FileStream może zostać usunięte przed ukończeniem zadania.

Podejście asynchroniczne pozwala uniknąć blokowania wątku wywołującego podczas oczekiwania na operacje we/wy. W wielu przypadkach ulepszenia przepływności zależą od systemu operacyjnego, sprzętu oraz, na niektórych platformach, od zachowania środowiska uruchomieniowego .NET, takich jak limity puli wątków i planowanie.

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

W przypadku używania metod WriteAsync i ReadAsync, można określić CancellationToken, aby anulować operację w połowie strumienia. Aby uzyskać więcej informacji, zobacz Anulowanie w zarządzanych wątkach.

Zobacz także