Pemrograman asinkron

Gunakan metode asinkron berbasis tugas untuk menghindari pemblokiran thread saat Microsoft.Data.SqlClient menunggu koneksi dan I/O jaringan. Eksekusi asinkron tidak membuat perintah atau pembaca aman untuk penggunaan secara bersamaan, dan tidak mengurangi waktu eksekusi query di server.

Contoh yang melakukan query tabel aplikasi menggunakan database contoh AdventureWorks. Contoh C# membutuhkan using Microsoft.Data.SqlClient;.

Konversi alur pemanggilan menjadi kode asinkron

Konversi satu jalur panggilan lengkap pada satu waktu:

  1. Buat metode induk menjadi asinkron.
  2. Ganti panggilan koneksi, perintah, reader, dan transaksi dengan padanan berbasis tugasnya.
  3. await selesaikan setiap operasi sebelum menggunakan objek yang sama lagi.
  4. Teruskan token pembatalan penelepon melalui setiap operasi yang dapat dibatalkan oleh penelpon.
  5. Tetapkan batas waktu perintah yang terbatas sebagai batas operasi database.

Gunakan await sejak pembukaan koneksi hingga pemrosesan hasil. Kutipan berikut mengasumsikan bahwa connectionString dan sql sudah didefinisikan. Ini membuat CancellationTokenSource dengan batas waktu terbatas untuk operasi:

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 mengembalikan saat hasil pertama tersedia, bukan setelah semua baris tiba. Setiap pemanggilan ReadAsync memajukan satu baris, sehingga loop memproses baris saat tiba dan membebaskan thread hanya saat menunggu data tambahan dari server. Token 30 detik membatasi seluruh operasi, termasuk loop baca.

Hanya satu eksekusi asinkron yang dapat tertunda pada SqlCommand. Hanya satu operasi bacaan asinkron, transisi hasil asinkron, atau operasi field asinkron yang dapat tertunda pada sebuah SqlDataReader. Tunggu setiap operasi sebelum memulai operasi berikutnya pada objek yang sama.

Fitur pemrograman asinkron

Metode sinkron berikut memiliki padanan berbasis tugas yang menerima token pembatalan:

Metode sinkron Metode asinkron Contoh
SqlConnection.Open SqlConnection.OpenAsync Konversi metode sinkron
SqlCommand.ExecuteNonQuery SqlCommand.ExecuteNonQueryAsync Jalankan perintah secara asinkron
SqlCommand.ExecuteScalar SqlCommand.ExecuteScalarAsync Jalankan perintah secara asinkron
SqlCommand.ExecuteXmlReader SqlCommand.ExecuteXmlReaderAsync Jalankan perintah secara asinkron
SqlCommand.ExecuteReader SqlCommand.ExecuteReaderAsync Ubah alur panggilan menjadi kode asinkron
SqlDataReader.Read SqlDataReader.ReadAsync Konversi alur panggilan menjadi kode asinkron
SqlDataReader.NextResult SqlDataReader.NextResultAsync Baca hasil secara asinkron
SqlDataReader.IsDBNull SqlDataReader.IsDBNullAsync Baca hasil secara asinkron
SqlDataReader.GetFieldValue<T> SqlDataReader.GetFieldValueAsync<T> Baca hasil secara asinkron
SqlBatch.ExecuteNonQuery, ExecuteScalar, dan ExecuteReader SqlBatch.ExecuteNonQueryAsync, ExecuteScalarAsync, dan ExecuteReaderAsync Jalankan batch
SqlBulkCopy.WriteToServer SqlBulkCopy.WriteToServerAsync Gunakan operasi asinkron dengan SqlBulkCopy

Gunakan pembuangan sinkron ketika sebuah objek hanya mengimplementasikan IDisposable. Gunakan await using untuk objek yang mengimplementasikan IAsyncDisposable. Buang reader sebelum menggunakan kembali koneksinya.

Jalankan perintah secara asinkron

Contoh berikut mengasumsikan bahwa connection itu terbuka. Gunakan ExecuteScalarAsync untuk satu nilai, dan berikan token pembatalan milik penelepon:

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

Gunakan ExecuteNonQueryAsync untuk pernyataan yang mengubah data. Gunakan ExecuteXmlReaderAsync untuk sebuah FOR XML kueri. Lepaskan pembaca XML sebelum Anda menjalankan perintah lain pada koneksi yang sama. Tanpa Multiple Active Result Sets (MARS), sebuah koneksi hanya mengizinkan satu reader terbuka.

Baca hasil secara asinkron

Contoh berikut meneruskan token pembatalan yang sama ke masing-masing panggilan asinkron ini:

  • ExecuteReaderAsync menjalankan kueri.
  • ReadAsync membaca setiap baris.
  • GetFieldValueAsync membaca nilai setiap kolom.
  • NextResultAsync bergerak ke set hasil berikutnya.
  • IsDBNullAsync memeriksa 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 bergerak ke set hasil berikutnya. GetFieldValueAsync dan IsDBNullAsync bacalah satu kolom dari baris saat ini. Saat Anda menggunakan default CommandBehavior, baris sudah di-buffer, jadi panggilan ini biasanya selesai tanpa menunggu. Saat Anda menggunakan CommandBehavior.SequentialAccess, proses dapat menunggu data kolom dari jaringan. Untuk informasi lebih lanjut tentang akses berurutan, lihat dukungan streaming SqlClient.

Konversi metode sinkron

Metode sinkron ini membuka koneksi, menjalankan perintah, dan mengembalikan jumlah baris yang terpengaruh dalam 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()!;
}

Untuk mengonversi metode ke kode asinkron, lakukan perubahan berikut:

  • Ubah tipe pengembalian menjadi Task<int>.
  • Tambahkan parameter CancellationToken .
  • Ganti Open dengan OpenAsync, dan ganti ExecuteScalar dengan ExecuteScalarAsync.
  • Lepaskan koneksi dengan await using.

Metode yang dikonversi terlihat seperti ini:

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

Pemanggil membuat token dan menunggu hasilnya:

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

Kedua metode mengembalikan jumlah yang sama. Output tergantung pada database yang Anda hubungkan ke.

Connect Timeout masih membatasi pembuatan koneksi dan waktu tunggu pool. Membatalkan token dapat mengembalikan kendali ke pemanggil lebih cepat.

Tambahkan panggilan asinkron ke aplikasi yang sudah ada

Konversi satu jalur panggilan lengkap dalam satu waktu. Hindari memanggil .Result, .Wait(), atau GetAwaiter().GetResult() pada tugas database karena panggilan tersebut memblokir dan dapat melakukan deadlock di lingkungan dengan konteks sinkronisasi.

Jangan memulai perintah asinkron lalu menggunakan perintah atau reader yang sama secara sinkron sebelum tugas selesai. Tanpa MARS, jangan gunakan koneksi yang sama secara sinkron sebelum tugas selesai.

Tambahkan pembatalan dan batas waktu

Membatalkan operasi asinkron

Teruskan token pembatalan yang sama dari pembukaan koneksi hingga eksekusi perintah:

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

Pembatalan diupayakan semaksimal mungkin. Operasi dapat selesai sebelum pembatalan berlaku, dan pengecualian yang diamati bergantung pada saat pembatalan terjadi. Jangan berasumsi bahwa menerima permintaan pembatalan berarti server melakukan rollback pada pekerjaan yang sudah selesai. Ketika serangkaian perubahan harus berhasil atau gagal sebagai satu unit, gunakan transaksi eksplisit. Untuk informasi lebih lanjut, lihat Transaksi lokal.

Cancel meminta pembatalan perintah saat ini. Ini tidak menggantikan token pembatalan milik penelepon. Operasi dapat selesai sebelum permintaan pembatalan berlaku.

Setelah pembatalan atau timeout, tunggu operasi dan lepaskan reader serta command. Periksa status koneksi sebelum digunakan kembali. Kegagalan yang memutus koneksi memerlukan koneksi baru.

Mengonfigurasi batas waktu

Timeout koneksi dan perintah mencakup berbagai fase:

Setting Mencakup
Connect Timeout Pembentukan koneksi dan menunggu koneksi pool.
CommandTimeout Eksekusi perintah dan waktu tunggu pembacaan jaringan kumulatif.
Token pembatalan Permintaan yang dikendalikan oleh penelepon untuk berhenti menunggu.

CommandTimeout diukur dalam hitungan detik. 0 Artinya tidak ada batas waktu perintah. Nilai default adalah nilai koneksi Command Timeout jika dikonfigurasi, jika tidak, 30 detik.

Untuk pembaca, timeout perintah berlaku untuk waktu tunggu jaringan kumulatif untuk setiap operasi baca penyedia. Itu tidak termasuk waktu yang dihabiskan untuk memproses baris dalam kode aplikasi. Timeout dapat terjadi sebelum baris pertama atau saat mengambil baris berikutnya.

Gabungkan pembatalan dan timeout perintah

Gunakan timeout perintah terbatas dan pembatalan pemanggil secara bersamaan. Contoh ini mengasumsikan bahwa connection terbuka:

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

Jika penelepon membatalkan terlebih dahulu, panggilan yang ditunggu akan memunculkan pengecualian. Jenis pengecualian tergantung pada di bagian mana pembatalan terjadi. Jika timeout perintah berakhir terlebih dahulu, penyedia melaporkan timeout. Dalam kedua kasus, perintah mungkin berhasil selesai sebelum pembatalan atau timeout berlaku.

Pola asinkron tingkat lanjut

Gunakan metode asinkron melalui tipe penyedia dasar

Kode yang independen dari penyedia dapat menggunakan DbConnection, DbCommand, dan DbDataReader metode asinkron. Buat parameter melalui perintah provider dan hindari downcast kecuali aplikasi membutuhkan fitur khusus Microsoft.Data.SqlClient.

Gunakan metode transaksi SQL berbasis tugas

Gunakan metode transaksi lokal berbasis task saat kerangka kerja target mengekspos metode tersebut. Contoh berikut mengasumsikan bahwa connection terbuka, sql memegang teks perintah, dan pemanggil meneruskan 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;
}

Gunakan token pembersihan terpisah untuk rollback. Token operasi mungkin sudah dibatalkan. Tangani kegagalan rollback tanpa membuang pengecualian operasi asli. Contoh ini mempertahankan kedua pengecualian dalam sebuah AggregateException. Sebagai gantinya, sebuah aplikasi dapat mencatat pengecualian rollback dan melempar kembali pengecualian asli.

Metode Microsoft.Data.SqlClient ini menggunakan implementasi turunan DbConnection dan DbTransaction, yang memanggil metode transaksi sinkron dan mengembalikan Task yang sudah selesai. Menunggu mereka tidak membuka blokir thread pemanggil selama operasi transaksi berjalan. Pembatalan diperiksa sebelum operasi dimulai dan tidak mengganggu operasi yang sedang berlangsung. Eksekusi perintah di dalam transaksi masih asinkron.

Gunakan transaksi terdistribusi asinkron

Perintah asinkron dapat dijalankan di dalam konteks TransactionScope. Cakupan ini menetapkan Transaction.Current, yang oleh .NET disebut sebagai transaksi ambient. Koneksi yang terbuka di dalam lingkup secara otomatis mendaftar dalam transaksi tersebut, karena kata kunci koneksi Enlist secara default adalah true. Konfigurasikan TransactionScopeAsyncFlowOption.Enabled saat transaksi harus mengalir melintasi await. Transaksi terdistribusi memiliki persyaratan platform, server, dan koordinator. Untuk informasi lebih lanjut, lihat integrasi System.Transactions.

Gunakan operasi asinkron dengan SqlBulkCopy

Gunakan WriteToServerAsync untuk penyalinan massal asinkron. Jaga pembaca sumber atau koleksi baris tetap hidup hingga tugas yang dikembalikan selesai. Konfigurasikan ukuran batch, perilaku transaksi, dan timeout untuk operasi. Untuk informasi lebih lanjut, lihat Operasi salinan massal.

Secara asinkron menggunakan beberapa perintah dengan MARS

MARS memungkinkan lebih dari satu perintah yang tertunda pada satu koneksi fisik. Aktifkan ini dengan MultipleActiveResultSets=True hanya ketika aplikasi membutuhkan model berbagi koneksi tersebut.

MARS tidak diperlukan untuk pemrograman asinkron. Tanpa MARS, selesaikan atau buang reader aktif sebelum menjalankan perintah lain pada koneksi yang sama. Koneksi kedua yang terpisah seringkali lebih baik untuk pekerjaan yang benar-benar mandiri.

Untuk perilaku sesi dan pembatasan transaksi, lihat Enable Multiple Active Result Sets.

Membaca dan memperbarui data secara asinkron dengan MARS

Saat Anda mengaktifkan MARS, satu perintah dapat membaca baris sementara perintah lain dieksekusi pada koneksi yang sama. Tunggu setiap operasi perintah dan pembacaan secara terpisah. Operasi MARS tidak aman untuk thread, dan eksekusi server masih bisa berserialisasi pada titik yield.

Ketika operasi baca dan pembaruan harus bersifat atomik, tetapkan transaksi lokal yang sama ke setiap perintah dan uji perilaku MARS transaksi tersebut. Jangan melakukan commit saat operasi lain dalam transaksi tersebut masih tertunda.

Pemrograman asinkron lama

Microsoft.Data.SqlClient mempertahankan metode BeginExecuteNonQuery, BeginExecuteReader, dan BeginExecuteXmlReader Asynchronous Programming Model (APM) untuk kompatibilitas. Jangan gunakan dalam kode baru. CommandTimeout diabaikan oleh panggilan asinkron gaya lama ini dan dihormati oleh metode berbasis tugas seperti ExecuteReaderAsync.

Metode lama tidak memerlukan Asynchronous Processing=true. Microsoft. Data.SqlClient 4.0 dan versi yang lebih baru tidak mendukung Asynchronous Processing properti koneksi.