Asynchrone Programmierung

Verwenden Sie aufgabenbasierte asynchrone Methoden, um zu vermeiden, dass ein Thread blockiert wird, während Microsoft.Data.SqlClient auf den Verbindungsaufbau und Netzwerk-I/O wartet. Asynchrone Ausführung macht einen Befehl oder Reader nicht für gleichzeitige Nutzung sicher und verkürzt die Abfrageausführungszeit auf dem Server nicht.

Die Beispiele, die Anwendungstabellen abfragen, verwenden die AdventureWorks-Beispieldatenbank. Die C#-Beispiele erfordern using Microsoft.Data.SqlClient;.

Eine Aufrufkette zu asynchronem Code konvertieren

Konvertiere jeweils einen vollständigen Aufrufpfad:

  1. Mach die enthaltende Methode asynchron.
  2. Ersetzen Sie Verbindungs-, Befehls-, Reader- und Transaktionsaufrufe durch ihre aufgabenbasierten Gegenstücke.
  3. await Schließen Sie jede Operation, bevor dasselbe Objekt erneut verwendet wird.
  4. Geben Sie das Abbruchtoken des Anrufers durch jede Operation weiter, die der Anrufer abbrechen kann.
  5. Legen Sie ein endliches Befehls-Timeout als Grenze für Datenbankvorgänge fest.

Verwenden Sie await ab dem Öffnen der Verbindung bis zur Ergebnisverarbeitung. Der folgende Auszug setzt voraus, dass connectionString und sql bereits definiert sind. Es erzeugt ein CancellationTokenSource mit einem endlichen Timeout für die Operation:

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 kehrt zurück, wenn das erste Ergebnis verfügbar ist, nicht nachdem alle Zeilen angekommen sind. Jeder Aufruf von ReadAsync rückt eine Zeile voran, sodass die Schleife Zeilen verarbeitet, sobald sie eintreffen, und den Thread nur freigibt, während sie auf weitere Daten vom Server wartet. Der 30-Sekunden-Token begrenzt den gesamten Vorgang, einschließlich der Leseschleife.

Es kann nur eine asynchrone Ausführung für ein SqlCommand ausstehend sein. Es kann nur eine asynchrone Lese-, Ergebnisübergangs- oder Feldoperation für ein SqlDataReader ausstehend sein. Warte auf jede Operation, bevor du die nächste Operation am selben Objekt startest.

Asynchrone Programmierungsfeatures

Die folgenden synchronen Methoden haben aufgabenbasierte Gegenstücke, die ein Abbruchtoken akzeptieren:

Synchrone Methode Asynchrone Methode Example
SqlConnection.Open SqlConnection.OpenAsync Konvertiere eine synchrone Methode
SqlCommand.ExecuteNonQuery SqlCommand.ExecuteNonQueryAsync Befehle asynchron ausführen
SqlCommand.ExecuteScalar SqlCommand.ExecuteScalarAsync Befehle asynchron ausführen
SqlCommand.ExecuteXmlReader SqlCommand.ExecuteXmlReaderAsync Befehle asynchron ausführen
SqlCommand.ExecuteReader SqlCommand.ExecuteReaderAsync Eine Aufrufkette in asynchronen Code konvertieren
SqlDataReader.Read SqlDataReader.ReadAsync Einen Aufrufpfad in asynchronen Code konvertieren
SqlDataReader.NextResult SqlDataReader.NextResultAsync Ergebnisse asynchron lesen
SqlDataReader.IsDBNull SqlDataReader.IsDBNullAsync Ergebnisse asynchron abrufen
SqlDataReader.GetFieldValue<T> SqlDataReader.GetFieldValueAsync<T> Ergebnisse asynchron auslesen
SqlBatch.ExecuteNonQuery, ExecuteScalar und ExecuteReader SqlBatch.ExecuteNonQueryAsync, ExecuteScalarAsync und ExecuteReaderAsync Batch ausführen
SqlBulkCopy.WriteToServer SqlBulkCopy.WriteToServerAsync Verwenden Sie asynchrone Operationen mit SqlBulkCopy

Verwenden Sie synchrone Freigabe, wenn ein Objekt nur IDisposable implementiert. Verwenden Sie await using für Objekte, die IAsyncDisposable implementieren. Entsorgen Sie den Reader, bevor Sie seine Connection wiederverwenden.

Befehle asynchron ausführen

Die folgenden Beispiele nehmen an, dass connection offen ist. Verwenden Sie ExecuteScalarAsync für einen einzelnen Wert und geben Sie ein aufrufereigenes Abbruchtoken weiter:

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

Verwenden Sie ExecuteNonQueryAsync für datenverändernde Anweisungen. Verwenden Sie ExecuteXmlReaderAsync für eine FOR XML Anfrage. Schließe den XML-Reader, bevor du einen weiteren Befehl auf derselben Verbindung ausführst. Ohne Multiple Active Result Sets (MARS) erlaubt eine Verbindung nur einen geöffneten Reader.

Ergebnisse asynchron auslesen

Das folgende Beispiel übergibt denselben Abbruchtoken an jeden dieser asynchronen Aufrufe:

  • ExecuteReaderAsync führt die Abfrage aus.
  • ReadAsync liest jede Zeile.
  • GetFieldValueAsync liest jeden Spaltenwert.
  • NextResultAsync wechselt zur nächsten Ergebnismenge.
  • IsDBNullAsync prüft, ob NULL vorliegt.
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 wechselt zur nächsten Ergebnismenge. GetFieldValueAsync und IsDBNullAsync lesen eine Spalte der aktuellen Zeile. Wenn Sie den Standard CommandBehaviorverwenden, ist die Zeile bereits gepuffert, sodass diese Aufrufe normalerweise ohne Wartezeit abgeschlossen werden. Wenn du CommandBehavior.SequentialAccess verwendest, können diese auf Spaltendaten aus dem Netzwerk warten. Weitere Informationen zum sequentiellen Zugriff finden Sie unter SqlClient Streaming-Unterstützung.

Konvertiere eine synchrone Methode

Diese synchrone Methode öffnet eine Verbindung, führt einen Befehl aus und gibt die Anzahl der Zeilen in sys.objectszurück:

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

Um die Methode in asynchronen Code umzuwandeln, nehmen Sie folgende Änderungen vor:

  • Ändere den Rückgabetyp auf Task<int>.
  • Füge einen Parameter CancellationToken hinzu.
  • Ersetzen Sie Open durch OpenAsync, und ersetzen Sie ExecuteScalar durch ExecuteScalarAsync.
  • Geben Sie die Verbindung mit await using frei.

Die umgewandelte Methode sieht so aus:

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

Der Aufrufer erstellt das Token und wartet auf das Ergebnis:

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

Beide Methoden liefern die gleiche Zahl. Das Ergebnis hängt von der Datenbank ab, mit der du dich verbindest.

Connect Timeout Weiterhin begrenzt den Verbindungsaufbau und die Wartezeiten im Pool. Das Abbrechen des Token kann die Ausführung schneller an den Aufrufer zurückgeben.

Fügen Sie asynchrone Aufrufe zu einer bestehenden Anwendung hinzu

Konvertieren Sie jeweils einen kompletten Aufrufpfad. Vermeiden Sie es, GetAwaiter().GetResult(), .Result oder .Wait() bei Datenbankvorgängen aufzurufen, da diese Aufrufe blockieren und in Umgebungen mit einem Synchronisationskontext zu Deadlocks führen können.

Starte keinen asynchronen Befehl und benutze dann denselben Befehl oder Reader synchron, bevor die Aufgabe abgeschlossen ist. Ohne MARS solltest du dieselbe Verbindung nicht synchron verwenden, solange die Aufgabe nicht abgeschlossen ist.

Fügen Sie Stornierung und Zeitbeschränkungen hinzu

Abbrechen eines asynchronen Vorgangs

Geben Sie denselben Abbruchtoken vom Öffnen der Verbindung bis zur Befehlsausführung weiter:

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

Die Stornierung erfolgt nach bestem Bemühen. Die Operation könnte abgeschlossen werden, bevor der Abbruch wirksam wird, und die beobachtete Ausnahme hängt davon ab, wo der Abbruch erfolgt. Gehe nicht davon aus, dass eine Stornierungsanfrage bedeutet, dass der Server abgeschlossene Arbeit zurückgesetzt hat. Wenn eine Menge von Änderungen als Einheit erfolgreich sein oder scheitern muss, verwenden Sie eine explizite Transaktion. Weitere Informationen finden Sie unter Lokale Transaktionen.

Cancel fordert die Aufhebung des aktuellen Befehls an. Es ersetzt keinen vom Aufrufer bereitgestellten Cancellation Token. Die Operation könnte abgeschlossen werden, bevor die Abbruchanfrage in Kraft tritt.

Nach Abbruch oder Zeitüberschreitung warten Sie den Vorgang ab und geben Sie den Reader und den Command frei. Überprüfen Sie den Verbindungszustand vor der Wiederverwendung. Ein Fehler, der die Verbindung unterbricht, erfordert eine neue Verbindung.

Konfigurieren von Timeouts

Verbindungs- und Kommando-Timeouts decken verschiedene Phasen ab:

Setting Abdeckungen
Connect Timeout Herstellen einer Verbindung und Warten auf eine gepoolte Verbindung
CommandTimeout Befehlsausführung und kumulierte Wartezeiten beim Netzwerklesen.
Abbruchtoken Eine vom Anrufer gesteuerte Anforderung, das Warten einzustellen.

CommandTimeoutin Sekunden gemessen. 0 Das heißt, es gibt kein Kommandozeitlimit. Standardmäßig ist der Command Timeout Wert der Verbindung, wenn konfiguriert, ansonsten 30 Sekunden.

Bei einem Datenleser gilt das Befehlszeitlimit für die kumulierte Netzwerkwartezeit jedes Lesevorgangs des Anbieters. Es beinhaltet nicht die Zeit, die mit der Verarbeitung einer Zeile im Anwendungscode verbracht wird. Eine Zeitüberschreitung kann vor der ersten Zeile oder beim Abrufen einer späteren Zeile auftreten.

Abbruch und Befehls-Timeout kombinieren

Verwenden Sie ein begrenztes Befehls-Timeout und den Abbruch durch den Aufrufer zusammen. Dieses Beispiel setzt voraus, dass connection offen ist:

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

Wenn der Aufrufer zuerst abbricht, löst der abgewartete Aufruf eine Ausnahme aus. Der Ausnahmetyp hängt davon ab, wo der Abbruch erfolgt. Wenn der Kommando-Timeout zuerst abläuft, meldet der Anbieter einen Timeout. In beiden Fällen könnte der Befehl erfolgreich abgeschlossen werden, bevor der Abbruch oder das Timeout in Kraft tritt.

Fortgeschrittene asynchrone Muster

Verwenden Sie asynchrone Methoden über Basisanbietertypen

Anbieterunabhängiger Code kann DbCommand, DbDataReader und DbConnection asynchrone Methoden verwenden. Erstelle Parameter über den Provider-Befehl und vermeide Downcasts, es sei denn, die Anwendung benötigt eine Microsoft.Data.SqlClient-spezifische Funktion.

Verwenden Sie aufgabenbasierte SQL-Transaktionsmethoden

Verwenden Sie die aufgabenbasierten lokalen Transaktionsmethoden, wenn das Ziel-Framework sie bereitstellt. Das folgende Beispiel geht davon aus, dass connection offen ist, sql den Befehlstext hält und der Aufrufer cancellationToken übergibt:

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

Verwende einen separaten Bereinigungs-Token für ein Rollback. Der Operationstoken könnte bereits storniert sein. Behandle einen Rollback-Fehlschlag, ohne die ursprüngliche Operation-Ausnahme zu verwerfen. Dieses Beispiel behält beide Ausnahmen in einem AggregateException bei. Eine Anwendung kann stattdessen die Rollback-Ausnahme protokollieren und die ursprüngliche Ausnahme neu werfen.

Diese Microsoft.Data.SqlClient-Methoden verwenden die vererbten DbConnection- und DbTransaction-Implementierungen, die die synchronen Transaktionsmethoden aufrufen und eine abgeschlossene Task zurückgeben. Das Warten auf sie blockiert den aufrufenden Thread nicht, während die Transaktionsoperation läuft. Der Abbruch wird vor Beginn des Vorgangs überprüft und unterbricht keinen laufenden Vorgang. Die Befehlsausführung innerhalb der Transaktion ist weiterhin asynchron.

Verwenden Sie asynchrone verteilte Transaktionen

Asynchrone Befehle können innerhalb eines TransactionScope ausgeführt werden. Der Scope setzt Transaction.Current, was .NET als ambient-Transaktion bezeichnet. Eine Verbindung, die sich innerhalb des Gültigkeitsbereichs öffnet, meldet sich automatisch in diese Transaktion ein, da das Verbindungs-Schlüsselwort Enlist standardmäßig auf true. Konfigurieren TransactionScopeAsyncFlowOption.Enabled Sie, wenn die Transaktion über await fließen muss. Verteilte Transaktionen haben Anforderungen an Plattform, Server und Koordinator. Weitere Informationen finden Sie unter System.Transactions-Integration.

Verwenden Sie asynchrone Operationen mit SqlBulkCopy

Verwenden Sie WriteToServerAsync für asynchrone Massenkopien. Halte den Quellleser oder die Zeilensammlung am Leben, bis der zurückgegebene Task abgeschlossen ist. Konfigurieren Sie Batchgröße, Transaktionsverhalten und Timeout für die Operation. Weitere Informationen finden Sie unter Massenkopiervorgänge.

Asynchrones Verwenden mehrerer Befehle mit MARS

MARS erlaubt mehr als einen ausstehenden Befehl auf einer physischen Verbindung. Aktiviere es nur mit MultipleActiveResultSets=True, wenn die Anwendung dieses Verbindungs-Sharing-Modell benötigt.

MARS ist für asynchrone Programmierung nicht erforderlich. Ohne MARS beenden oder entsorgen Sie den aktiven Reader, bevor Sie einen weiteren Befehl auf derselben Verbindung ausführen. Eine zweite Verbindung ist bei wirklich unabhängiger Arbeit oft klarer.

Für Sitzungsverhalten und Transaktionsbeschränkungen siehe Enable Multiple Active Result Sets.

Asynchrones Lesen und Aktualisieren von Daten mit MARS

Wenn du MARS aktivierst, kann ein Befehl Zeilen lesen, während ein anderer Befehl auf derselben Verbindung ausgeführt werden kann. Warten Sie auf jeden Befehl und jeden Lesevorgang einzeln. MARS-Operationen sind nicht threadsicher, und die Serverausführung kann trotzdem an Yield Points serialisieren.

Wenn Lese- und Aktualisierungsoperationen atomar sein müssen, weisen Sie jedem Befehl dieselbe lokale Transaktion zu und testen Sie das MARS-Verhalten der Transaktion. Committe nicht, solange eine andere Operation in dieser Transaktion noch aussteht.

Herkömmliche asynchrone Programmierung

Microsoft.Data.SqlClient behält die Methoden BeginExecuteNonQuery, BeginExecuteReader und BeginExecuteXmlReader des asynchronen Programmiermodells (APM) aus Kompatibilitätsgründen bei. Verwende sie nicht in neuem Code. CommandTimeout wird von diesen altmodischen asynchronen Aufrufen ignoriert und von aufgabenbasierten Methoden wie ExecuteReaderAsync berücksichtigt wird.

Die Legacy-Methoden erfordern kein Asynchronous Processing=true. Microsoft. Data.SqlClient 4.0 und neuere Versionen unterstützen die Asynchronous Processing Verbindungseigenschaft nicht.