Programowanie asynchroniczne

Stosuj asynchroniczne metody zadaniowe, aby uniknąć blokowania wątku, gdy biblioteka Microsoft.Data.SqlClient oczekuje na połączenie i sieciowe operacje wejścia/wyjścia. Asynchroniczne wykonywanie nie czyni polecenia ani czytnika bezpiecznym do jednoczesnego użycia i nie skraca czasu wykonania zapytań na serwerze.

Przykłady zapytujące tabele aplikacji wykorzystują przykładową bazę danych AdventureWorks. Przykłady w języku C# wymagają using Microsoft.Data.SqlClient;.

Przekonwertowanie ścieżki wywołań na kod asynchroniczny

Konwertuj jedną pełną ścieżkę połączenia na raz:

  1. Uczyń metodę otaczającą asynchroniczną.
  2. Zamień połączenia, polecenia, czytnik i wywołania transakcji ich odpowiednikami opartymi na zadaniach.
  3. await Zakończ każdą operację przed ponownym użyciem tego samego obiektu.
  4. Przekaż token anulowania wywołującego do każdej operacji, którą wywołujący może anulować.
  5. Utrzymuj skończony limit czasu polecenia jako limit operacji bazy danych.

Używaj await od otwarcia połączenia aż do przetwarzania wyników. Poniższy fragment zakłada, że connectionString i sql są już zdefiniowane. Tworzy CancellationTokenSource z określonym limitem czasu dla operacji:

using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));

await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(cancellation.Token);

using var command = new SqlCommand(sql, connection);
await using SqlDataReader reader =
    await command.ExecuteReaderAsync(cancellation.Token);

while (await reader.ReadAsync(cancellation.Token))
{
    Console.WriteLine(reader.GetValue(0));
}

ExecuteReaderAsync zwraca, gdy pierwszy wynik jest dostępny, a nie po pojawieniu się wszystkich wierszy. Każde ReadAsync wywołanie powoduje przejście do następnego wiersza, więc pętla przetwarza wiersze w miarę ich pojawiania się i zwalnia wątek tylko podczas oczekiwania na kolejne dane z serwera. Token 30-sekundowy ogranicza całą operację, włącznie z pętlą odczytu.

Tylko jedno asynchroniczne wykonanie może oczekiwać na obiekcie SqlCommand. Tylko jeden asynchroniczny odczyt, przejście wyniku lub operacja pola mogą oczekiwać na elemencie SqlDataReader. Czekaj na każdą operację przed rozpoczęciem kolejnej operacji na tym samym obiekcie.

Funkcje programowania asynchronicznego

Następujące metody synchroniczne mają odpowiedniki oparte na zadaniach, które akceptują token anulowania:

Metoda synchroniczna Asynchroniczna metoda Przykład
SqlConnection.Open SqlConnection.OpenAsync Przekonwertowanie metody synchronicznej
SqlCommand.ExecuteNonQuery SqlCommand.ExecuteNonQueryAsync Uruchamiaj polecenia asynchronicznie
SqlCommand.ExecuteScalar SqlCommand.ExecuteScalarAsync Uruchamiaj polecenia asynchronicznie
SqlCommand.ExecuteXmlReader SqlCommand.ExecuteXmlReaderAsync Uruchamiaj polecenia asynchronicznie
SqlCommand.ExecuteReader SqlCommand.ExecuteReaderAsync Konwertowanie ścieżki wywołania na kod asynchroniczny
SqlDataReader.Read SqlDataReader.ReadAsync Przekonwertowanie ścieżki wywołań na kod asynchroniczny
SqlDataReader.NextResult SqlDataReader.NextResultAsync Odczytuj wyniki asynchronicznie
SqlDataReader.IsDBNull SqlDataReader.IsDBNullAsync Asynchroniczny odczyt wyników
SqlDataReader.GetFieldValue<T> SqlDataReader.GetFieldValueAsync<T> Odczytuj wyniki asynchronicznie
SqlBatch.ExecuteNonQuery, ExecuteScalari ExecuteReader SqlBatch.ExecuteNonQueryAsync, ExecuteScalarAsynci ExecuteReaderAsync Wykonaj zadanie wsadowe
SqlBulkCopy.WriteToServer SqlBulkCopy.WriteToServerAsync Używaj operacji asynchronicznych z SqlBulkCopy

Używaj synchronicznego zwalniania zasobów, gdy obiekt implementuje tylko IDisposable. Użyj await using dla obiektów implementujących IAsyncDisposable. Zwolnij czytnik przed ponownym użyciem jego połączenia.

Uruchamiaj polecenia asynchronicznie

Poniższe przykłady zakładają, że connection jest otwarte. Użyj ExecuteScalarAsync dla pojedynczej wartości i przekaż token anulowania należący do wywołującego:

using var cancellation = new CancellationTokenSource(
    TimeSpan.FromSeconds(30));
CancellationToken token = cancellation.Token;

using var count = new SqlCommand(
    "SELECT COUNT(*) FROM Production.Product;",
    connection);
int total = (int)(await count.ExecuteScalarAsync(token))!;
Console.WriteLine($"Row count: {total}");

Użyj ExecuteNonQueryAsync w instrukcjach zmieniających dane. Użyj ExecuteXmlReaderAsync, aby wykonać zapytanie FOR XML. Zamknij czytnik XML przed wykonaniem kolejnego polecenia na tym samym połączeniu. Bez Multiple Active Result Sets (MARS) połączenie pozwala na otwarcie tylko jednego czytnika danych.

Odczytuj wyniki asynchronicznie

Poniższy przykład przekazuje ten sam token anulowania do każdego z tych asynchronicznych wywołań:

  • ExecuteReaderAsync uruchamia zapytanie.
  • ReadAsync czyta każdy wiersz.
  • GetFieldValueAsync odczytuje każdą wartość kolumny.
  • NextResultAsync przechodzi do następnego zbioru wyników.
  • IsDBNullAsyncSprawdza NULL.
using var cancellation = new CancellationTokenSource(
    TimeSpan.FromSeconds(30));
CancellationToken token = cancellation.Token;

using var query = new SqlCommand("""
    SELECT TOP (3) ProductID, Name
    FROM Production.Product
    ORDER BY ProductID;

    SELECT TOP (1) ProductID, Color
    FROM Production.Product
    WHERE Color IS NULL;
    """, connection);
await using SqlDataReader reader = await query.ExecuteReaderAsync(token);

while (await reader.ReadAsync(token))
{
    int id = await reader.GetFieldValueAsync<int>(0, token);
    string name = await reader.GetFieldValueAsync<string>(1, token);
    Console.WriteLine($"{id}: {name}");
}

if (await reader.NextResultAsync(token) && await reader.ReadAsync(token))
{
    int productId = await reader.GetFieldValueAsync<int>(0, token);
    bool colorIsNull = await reader.IsDBNullAsync(1, token);
    Console.WriteLine($"{productId}: Color is NULL: {colorIsNull}");
}

NextResultAsync przechodzi do następnego zbioru wyników. GetFieldValueAsync i IsDBNullAsync odczytują jedną kolumnę bieżącego wiersza. Gdy używasz domyślnego CommandBehavior, wiersz jest już buforowany, więc te wywołania zwykle kończą się bez czekania. Gdy używasz CommandBehavior.SequentialAccess, mogą czekać na dane kolumnowe z sieci. Więcej informacji o dostępie sekwencyjnym można znaleźć w artykule SqlClient streaming support.

Przekonwertowanie metody synchronicznej

Ta metoda synchroniczna otwiera połączenie, wykonuje polecenie i zwraca liczbę wierszy w sys.objects:

static int CountObjects(string connectionString)
{
    using var connection = new SqlConnection(connectionString);
    connection.Open();

    using var command = new SqlCommand("SELECT COUNT(*) FROM sys.objects;", connection);
    return (int)command.ExecuteScalar()!;
}

Aby przekonwertować metodę na kod asynchroniczny, wprowadź następujące zmiany:

  • Zmień typ zwrotu na Task<int>.
  • Dodaj CancellationToken parametr.
  • Zastąp Open przez OpenAsync, a zastąp ExecuteScalar przez ExecuteScalarAsync.
  • Usuń połączenie z await using.

Metoda konwersji wygląda następująco:

static async Task<int> CountObjectsAsync(
    string connectionString,
    CancellationToken cancellationToken)
{
    await using var connection = new SqlConnection(connectionString);
    await connection.OpenAsync(cancellationToken);

    using var command = new SqlCommand("SELECT COUNT(*) FROM sys.objects;", connection);
    return (int)(await command.ExecuteScalarAsync(cancellationToken))!;
}

Wywołujący tworzy token i czeka na wynik:

using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
int objectCount = await CountObjectsAsync(connectionString, cancellation.Token);
Console.WriteLine($"Objects: {objectCount}");

Obie metody dają tę samą liczbę. Wyniki zależą od bazy danych, do której się łączysz.

Connect Timeout nadal ogranicza ustanawianie połączeń i oczekiwanie na połączenie z puli. Anulowanie tokena może szybciej przywrócić kontrolę wywołującemu.

Dodaj wywołania asynchroniczne do istniejącej aplikacji

Konwertuj jedną pełną ścieżkę połączenia na raz. Unikaj wywoływania .Result, .Wait() lub GetAwaiter().GetResult() dla zadań bazy danych, ponieważ te wywołania blokują i mogą prowadzić do zakleszczenia w środowiskach z kontekstem synchronizacji.

Nie zaczynaj polecenia asynchronicznego i nie używaj tego samego polecenia lub czytnika synchronicznie, zanim zadanie się zakończy. Bez MARS nie używaj synchronicznie tego samego połączenia, dopóki zadanie się nie zakończy.

Dodaj limity anulowania i limity czasowe

Anulowanie operacji asynchronicznej

Przekaż ten sam token anulowania od otwarcia połączenia do wykonania polecenia:

using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(10));

await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(cancellation.Token);

using var command = new SqlCommand(
    "SELECT COUNT(*) FROM Production.Product;",
    connection);
await command.ExecuteScalarAsync(cancellation.Token);

Anulowanie jest realizowane w miarę możliwości. Operacja może zakończyć się, zanim anulowanie nastąpi, a zaobserwowany wyjątek zależy od tego, w którym momencie nastąpi anulowanie. Nie zakładaj, że otrzymanie prośby o anulowanie oznacza, że serwer cofnął ukończoną pracę. Gdy zestaw zmian musi zakończyć się powodzeniem lub niepowodzeniem jako całość, użyj jawnej transakcji. Więcej informacji można znaleźć w Transakcje lokalne.

Cancel prosi o anulowanie obecnej komendy. Nie zastępuje tokena anulowania należącego do wywołującego. Operacja może zakończyć się przed wejściem w życie prośby o anulowanie.

Po anulowaniu lub przekroczeniu limitu czasu oczekuj na zakończenie operacji i zwolnij czytnik oraz polecenie. Sprawdź stan połączenia przed ponownym użyciem. Awaria zrywająca połączenie wymaga nowego połączenia.

Konfigurowanie limitów czasu

Limity czasowe połączenia i poleceń obejmują różne fazy:

Setting Obejmuje
Connect Timeout Nawiązanie połączenia i oczekiwanie na połączenie z puli.
CommandTimeout Wykonanie poleceń i skumulowane oczekiwania na odczyt z sieci.
Token anulowania Prośba inicjowana przez dzwoniącego, by przestać czekać.

CommandTimeout jest mierzona w sekundach. 0 oznacza brak limitu czasu komendy. Domyślna wartość to wartość Command Timeout połączenia, gdy jest skonfigurowane, w przeciwnym razie 30 sekund.

Dla czytnika limit czasu polecenia dotyczy skumulowanego czasu oczekiwania na sieć dla każdej operacji odczytu dostawcy. Nie obejmuje czasu poświęconego na przetwarzanie wiersza w kodzie aplikacji. Przekroczenie limitu czasu może nastąpić przed pierwszym wierszem lub podczas pobierania kolejnego wiersza.

Połącz anulowanie i limit czasu polecenia

Używaj skończonego limitu czasu i anulowania przez wywołującego razem. Ten przykład zakłada, że connection jest otwarte:

using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));

using var command = new SqlCommand("""
    SELECT TOP (10) SalesOrderID, OrderDate
    FROM Sales.SalesOrderHeader
    ORDER BY SalesOrderID;
    """, connection)
{
    CommandTimeout = 15
};

await using SqlDataReader reader =
    await command.ExecuteReaderAsync(cancellation.Token);

while (await reader.ReadAsync(cancellation.Token))
{
    Console.WriteLine($"{reader.GetInt32(0)} | {reader.GetDateTime(1):O}");
}

Jeśli wywołujący anuluje jako pierwszy, wywołanie, na które oczekiwano, zgłasza wyjątek. Typ wyjątku zależy od tego, gdzie następuje anulowanie. Jeśli najpierw upłynie limit czasu polecenia, dostawca zgłasza przekroczenie limitu czasu. W obu przypadkach polecenie może zakończyć się pomyślnie, zanim anulowanie lub limit czasu zacznie obowiązywać.

Zaawansowane wzorce asynchroniczne

Używaj metod asynchronicznych za pośrednictwem typów bazowych dostawców

Kod niezależny od dostawcy może używać DbConnection, DbCommand, oraz DbDataReader metod asynchronicznych. Utwórz parametry za pomocą polecenia dostawcy i unikaj rzutowań w dół, chyba że aplikacja potrzebuje funkcji specyficznej dla Microsoft.Data.SqlClient.

Używaj metod transakcji SQL z użyciem zadań

Używaj lokalnych metod transakcji opartych na zadaniach, gdy docelowy framework je udostępnia. Poniższy przykład zakłada, że connection jest otwarte, sql zawiera tekst polecenia, a wywołujący przekazuje :cancellationToken

await using SqlTransaction transaction =
    (SqlTransaction)await connection.BeginTransactionAsync(cancellationToken);

using var command = new SqlCommand(sql, connection, transaction);

try
{
    await command.ExecuteNonQueryAsync(cancellationToken);
    await transaction.CommitAsync(cancellationToken);
}
catch (Exception operationException)
{
    try
    {
        await transaction.RollbackAsync(CancellationToken.None);
    }
    catch (Exception rollbackException)
    {
        throw new AggregateException(
            "The operation and transaction rollback both failed.",
            operationException,
            rollbackException);
    }

    throw;
}

Do rollbacku używaj osobnego tokena czyszczenia. Token operacji mógł już zostać anulowany. Obsłuż niepowodzenie wycofania bez odrzucania pierwotnego wyjątku operacji. Ten przykład zachowuje oba wyjątki w elemencie AggregateException. Aplikacja może zamiast tego zarejestrować wyjątek rollback i ponownie wyrzucić oryginalny wyjątek.

Te metody Microsoft. Data.SqlClient wykorzystują odziedziczone DbConnection i DbTransaction implementacje, które wywołują synchroniczne metody transakcji i zwracają ukończone zadanie. Oczekiwanie na nie nie odblokowuje wątku wywołującego podczas wykonywania operacji transakcyjnej. Anulowanie jest sprawdzane przed rozpoczęciem operacji i nie przerywa operacji w toku. Wykonywanie poleceń w ramach transakcji pozostaje asynchroniczne.

Używaj asynchronicznych transakcji rozproszonych

Polecenia asynchroniczne mogą być wykonywane wewnątrz TransactionScope. Zakres ustawia Transaction.Current, co platforma .NET nazywa transakcją ambientową. Połączenie, które otwiera się wewnątrz zakresu, automatycznie rejestruje się w tej transakcji, ponieważ słowo kluczowe połączenia Enlist domyślnie ma wartość true. Skonfiguruj TransactionScopeAsyncFlowOption.Enabled, gdy transakcja musi przepływać przez await. Transakcje rozproszone mają wymagania dotyczące platformy, serwera i koordynatora. Więcej informacji można znaleźć w integracji System.Transactions.

Używaj operacji asynchronicznych z SqlBulkCopy

Użyj WriteToServerAsync do asynchronicznego kopiowania masowego. Utrzymuj czytnik źródłowy lub kolekcję wierszy przy życiu do czasu zakończenia zadania zwróconego przez metodę. Konfiguruj rozmiar partii, zachowanie transakcji i czas działania dla operacji. Więcej informacji można znaleźć w artykule Operacje kopiowania masowego.

Używaj wielu poleceń asynchronicznie za pomocą MARS

MARS pozwala na więcej niż jedno oczekujące polecenie na jednym fizycznym połączeniu. Włącz go MultipleActiveResultSets=True tylko wtedy, gdy aplikacja potrzebuje takiego modelu współdzielenia połączenia.

MARS nie jest wymagany do programowania asynchronicznego. Bez MARS zakończ lub usuń aktywny czytnik danych przed wykonaniem kolejnego polecenia na tym samym połączeniu. Drugie połączenie często jest bardziej przejrzyste przy pracy naprawdę samodzielnej.

Aby poznać zachowanie sesji i ograniczenia transakcji, zobacz Włączenie wielu aktywnych zestawów wyników.

Asynchronicznie odczytywanie i aktualizowanie danych za pomocą usługi MARS

Po włączeniu MARS jedno polecenie może odczytywać wiersze, podczas gdy wykonywane jest inne polecenie na tym samym połączeniu. Czekaj na każde polecenie i operację czytnika osobno. Operacje MARS nie są bezpieczne dla wątków, a wykonywanie serwera nadal może się serializować w punktach wstrzymania.

Gdy operacje odczytu i aktualizacji muszą być atomowe, przypisz tę samą lokalną transakcję każdemu poleceniu i przetestuj zachowanie transakcji w MARS. Nie zatwierdzaj, dopóki inna operacja w tej transakcji jest jeszcze w trakcie oczekiwania.

Starsze programowanie asynchroniczne

Microsoft.Data.SqlClient zachowuje metody asynchronicznego modelu programowania (APM) BeginExecuteNonQuery, BeginExecuteReader i BeginExecuteXmlReader dla zachowania zgodności. Nie używaj ich w nowym kodzie. CommandTimeout jest ignorowany przez te stare asynchroniczne wywołania i jest respektowany przez metody oparte na zadaniach, takie jak ExecuteReaderAsync.

Starsze metody nie wymagają Asynchronous Processing=true. Microsoft.Data.SqlClient 4.0 i nowsze wersje nie obsługują właściwości połączenia Asynchronous Processing.