Asynkron programmering

Använd uppgiftsbaserade asynkrona metoder för att undvika att blockera en tråd medan Microsoft. Data.SqlClient väntar på anslutning och nätverks-I/O. Asynkron exekvering gör inte ett kommando eller en läsare säker för samtidigt användning, och det minskar inte tiden för frågeexekvering på servern.

Exemplen som gör frågor mot applikationstabeller använder AdventureWorks exempeldatabas. C#-exemplen kräver using Microsoft.Data.SqlClient;.

Konvertera en anropsväg till asynkron kod

Konvertera en komplett anropssökväg åt gången:

  1. Gör den omslutande metoden asynkron.
  2. Ersätt anslutnings-, kommando-, läsar- och transaktionsanrop med deras uppgiftsbaserade motsvarigheter.
  3. await Kontrollera före varje operation innan samma objekt används igen.
  4. Skicka anroparens annulleringstoken genom varje operation som anroparen kan avbryta.
  5. Behåll en ändlig kommandotimeout som databasoperationsgräns.

Använd await från att anslutningen öppnas till resultathantering. Följande utdrag antar att connectionString och sql redan är definierade. Den skapar en CancellationTokenSource med en ändlig timeout för operationen:

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 returnerar när det första resultatet är tillgängligt, inte efter att alla rader har anlänt. Varje ReadAsync anrop flyttar en rad framåt, så loopen bearbetar rader när de anländer och släpper tråden endast medan den väntar på mer data från servern. 30-sekunders-tokenen begränsar hela operationen, inklusive läsloopen.

Endast en asynkron exekvering kan vara väntande på en SqlCommand. Endast en asynkron läsning, resultatövergång eller fältoperation kan vara väntande på en SqlDataReader. Vänta på varje operation innan du påbörjar nästa operation på samma objekt.

Asynkrona programmeringsfunktioner

Följande synkrona metoder har uppgiftsbaserade motsvarigheter som accepterar en annulleringstoken:

synkron metod Asynkron metod Example
SqlConnection.Open SqlConnection.OpenAsync Konvertera en synkron metod
SqlCommand.ExecuteNonQuery SqlCommand.ExecuteNonQueryAsync Kör kommandon asynkront
SqlCommand.ExecuteScalar SqlCommand.ExecuteScalarAsync Kör kommandon asynkront
SqlCommand.ExecuteXmlReader SqlCommand.ExecuteXmlReaderAsync Kör kommandon asynkront
SqlCommand.ExecuteReader SqlCommand.ExecuteReaderAsync Konvertera en anropsväg till asynkron kod
SqlDataReader.Read SqlDataReader.ReadAsync Konvertera en anropsväg till asynkron kod
SqlDataReader.NextResult SqlDataReader.NextResultAsync Läs resultaten asynkront
SqlDataReader.IsDBNull SqlDataReader.IsDBNullAsync Läs resultaten asynkront
SqlDataReader.GetFieldValue<T> SqlDataReader.GetFieldValueAsync<T> Läs resultaten asynkront
SqlBatch.ExecuteNonQuery, ExecuteScalar och ExecuteReader SqlBatch.ExecuteNonQueryAsync, ExecuteScalarAsync och ExecuteReaderAsync Kör en batch
SqlBulkCopy.WriteToServer SqlBulkCopy.WriteToServerAsync Använd asynkrona operationer med SqlBulkCopy

Använd synkron avyttring när ett objekt endast implementerar IDisposable. Använd await using för objekt som implementerar IAsyncDisposable. Frigör läsaren innan du återanvänder dess anslutning.

Kör kommandon asynkront

Följande exempel antar att connection är öppen. Använd ExecuteScalarAsync för ett enda värde och skicka en anroparägd annulleringstoken:

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

Använd ExecuteNonQueryAsync för instruktioner som ändrar data. Använd ExecuteXmlReaderAsync för en FOR XML fråga. Frigör en XML-läsare innan du kör ett annat kommando på samma anslutning. Utan Multiple Active Result Sets (MARS) kan en anslutning bara ha en öppen läsare.

Läs resultaten asynkront

Följande exempel skickar samma avbokningstoken till var och en av dessa asynkrona anrop:

  • ExecuteReaderAsync kör frågan.
  • ReadAsync läser varje rad.
  • GetFieldValueAsync läser varje kolumnvärde.
  • NextResultAsync går vidare till nästa resultatuppsättning.
  • IsDBNullAsync Kontrollerar 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 går vidare till nästa resultatuppsättning. GetFieldValueAsync och IsDBNullAsync läser en kolumn från den aktuella raden. När du använder standarden CommandBehaviorär raden redan buffrad, så dessa anrop slutförs vanligtvis utan att vänta. När du använder CommandBehavior.SequentialAccess, kan det hända att man måste vänta på kolumndata från nätverket. För mer information om sekventiell åtkomst, se stöd för SqlClient-strömning.

Konvertera en synkron metod

Denna synkrona metod öppnar en anslutning, kör ett kommando och returnerar antalet rader i 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()!;
}

För att konvertera metoden till asynkron kod, gör dessa ändringar:

  • Ändra returtypen till Task<int>.
  • Lägg till en CancellationToken parameter.
  • Ersätt Open med OpenAsync, och ersätt ExecuteScalar med ExecuteScalarAsync.
  • Kassera anslutningen med await using.

Den konverterade metoden ser ut så här:

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

Anroparen skapar tokenen och väntar på resultatet:

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

Båda metoderna ger samma antal. Resultatet beror på vilken databas du ansluter till.

Connect Timeout begränsar fortfarande anslutningsetablering och poolväntetider. Att avbryta tokenen kan återlämna kontrollen till anroparen tidigare.

Lägg till asynkrona anrop till en befintlig applikation

Konvertera en komplett anropsväg åt gången. Undvik att anropa .Result, .Wait() eller GetAwaiter().GetResult() på databasåtgärder eftersom dessa anrop blockerar och kan fastna i miljöer med en synkroniseringskontext.

Starta inte ett asynkront kommando och använd sedan samma kommando eller läsare synkront innan uppgiften är klar. Utan MARS, använd inte samma anslutning synkront innan aktiviteten har slutförts.

Lägg till annullering och tidsgränser

Avbryt en asynkron åtgärd

Använd samma annulleringstoken från att anslutningen öppnas till kommandokörningen:

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

Avbokning sker i mån av möjlighet. Operationen kan avslutas innan annullering träder i kraft, och det observerade undantaget beror på var annulleringen sker. Anta inte att en avbokningsbegäran betyder att servern har rullat tillbaka slutfört arbete. När en uppsättning förändringar måste lyckas eller misslyckas som en enhet, använd en explicit transaktion. För mer information, se Lokala transaktioner.

Cancel begär att det nuvarande kommandot avbryts. Den ersätter inte en anroparägd avbokningstoken. Operationen kan vara klar innan avbokningsbegäran träder i kraft.

Efter avbokning eller timeout, invänta operationen och frigör läsaren och kommandot. Inspektera anslutningstillståndet innan du använder det igen. Ett anslutningsbrytande fel kräver en ny anslutning.

Konfigurera tidsgränser

Anslutnings- och kommandotidsgränser gäller olika faser:

Setting Täcker
Connect Timeout Upprättar anslutning och väntar på en poolad anslutning.
CommandTimeout Kommandokörning och ackumulerade väntetider för nätverksläsning.
Annulleringstoken En begäran som styrs av uppringaren om att sluta vänta.

CommandTimeout mäts i sekunder. 0 betyder att det inte finns någon tidsgräns för kommandon. Standardvärdet är anslutningens Command Timeout värde när det är konfigurerat, annars 30 sekunder.

För en läsare gäller kommandotimeout för den sammanlagda nätverksväntan för varje läsoperation från providern. Det inkluderar inte tid som läggs på att bearbeta en rad i applikationskoden. En timeout kan ske före första raden eller när man hämtar en senare rad.

Kombinera avbokning och kommandotidsgräns

Använd en begränsad kommandotimeout och anroparens annullering tillsammans. Detta exempel antar att connection är öppet:

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

Om den som ringer först avbryter, kastar det inväntade anropet ett undantag. Undantagstypen beror på var annullering sker. Om kommandotimeouten går ut först rapporterar leverantören timeout. I båda fallen kan kommandot slutföras framgångsrikt innan avbrytningen eller timeouten träder i kraft.

Avancerade asynkrona mönster

Använd asynkrona metoder genom providers basklasser

Leverantörsoberoende kod kan använda DbConnection, DbCommand, och DbDataReader asynkrona metoder. Skapa parametrar via provider-kommandot och undvik nedkastningar om inte applikationen behöver en Microsoft.Data.SqlClient-specifik funktion.

Använd taskbaserade SQL-transaktionsmetoder

Använd de uppgiftsbaserade lokala transaktionsmetoderna när målplattformen exponerar dem. Följande exempel antar att connection är öppen, sql håller kommandotexten och anroparen skickar in 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;
}

Använd en separat rensningstoken för rollback. Operationstoken kan redan vara avbrutet. Hantera ett rollback-fel utan att kassera det ursprungliga operationundantaget. Detta exempel bevarar båda undantagen i en AggregateException. En applikation kan istället logga rollback-undantaget och kasta det ursprungliga undantaget igen.

Dessa Microsoft.Data.SqlClient-metoder använder de ärvda implementationerna för DbConnection och DbTransaction, som anropar de synkrona transaktionsmetoderna och returnerar en slutförd Task. Att vänta på dem blockerar inte den anropande tråden medan transaktionsoperationen körs. Annullering kontrolleras innan operationen startar och avbryter inte en pågående operation. Kommandoexekveringen i transaktionen är fortfarande asynkron.

Använd asynkrona distribuerade transaktioner

Asynkrona kommandon kan utföras inuti en TransactionScope. Scopen sätter Transaction.Current, vilket .NET kallar ambient-transaktionen. En anslutning som öppnas inom scopen registrerar sig automatiskt i den transaktionen, eftersom nyckelordet för anslutningssträngen Enlist som standard är true. Konfigurera TransactionScopeAsyncFlowOption.Enabled när transaktionen måste överföras mellan await. Distribuerade transaktioner har plattforms-, server- och koordinatorkrav. För mer information, se System.Transactions-integration.

Använd asynkrona operationer med SqlBulkCopy

Använd WriteToServerAsync för asynkron bulkkopiering. Håll källläsaren eller radsamlingen vid liv tills den returnerade aktiviteten är klar. Konfigurera batchstorlek, transaktionsbeteende och timeout för operationen. För mer information, se åtgärder för masskopiering.

Asynkront använda flera kommandon med MARS

MARS tillåter mer än ett väntande kommando på en fysisk anslutning. Aktivera det MultipleActiveResultSets=True endast när applikationen behöver den anslutningsdelningsmodellen.

MARS krävs inte för asynkron programmering. Utan MARS, avsluta eller släng den aktiva läsaren innan du exekverar ett annat kommando på samma anslutning. En andra anslutning är ofta tydligare för verkligt självständigt arbete.

För sessionsbeteende och transaktionsbegränsningar, se Aktivera flera aktiva resultatuppsättningar.

Asynkront läsa och uppdatera data med MARS

När du aktiverar MARS kan ett kommando läsa rader medan ett annat kommando körs på samma anslutning. Vänta på varje kommando och läsaråtgärd separat. MARS-operationer är inte trådsäkra, och serverexekvering kan fortfarande serialisera vid yield points.

När läs- och uppdateringsoperationer måste vara atomära, tilldela samma lokala transaktion till varje kommando och testa transaktionens MARS-beteende. Sätt inte in medan en annan operation i den transaktionen fortfarande är under behandling.

Äldre asynkron programmering

Microsoft.Data.SqlClient behåller metoderna BeginExecuteNonQuery, BeginExecuteReader och BeginExecuteXmlReader i Asynchronous Programming Model (APM) av kompatibilitetsskäl. Använd dem inte i ny kod. CommandTimeout ignoreras av dessa gammaldags asynkrona anrop och respekteras av uppgiftsbaserade metoder såsom ExecuteReaderAsync.

De äldre metoderna kräver inte Asynchronous Processing=true. Microsoft.Data.SqlClient 4.0 och senare versioner stöder inte anslutningsegenskapen Asynchronous Processing.