Skenario pemrograman asinkron
Jika Anda memiliki kebutuhan terikat I/O (seperti meminta data dari jaringan, mengakses database, atau membaca dan menulis ke sistem file), Anda sebaiknya menggunakan pemrograman asinkron. Anda juga dapat memiliki kode terikat CPU, seperti melakukan perhitungan yang mahal, yang juga merupakan skenario yang baik untuk menulis kode asinkron.
C# memiliki model pemrograman asinkron tingkat bahasa, yang memungkinkan penulisan kode asinkron dengan mudah tanpa harus beralih panggilan balik atau menyesuaikan dengan pustaka yang mendukung asinkron. Model ini mengikuti apa yang dikenal sebagai Pola Asinkron Berbasis Tugas (TAP).
Gambaran umum model asinkron
Inti dari pemrograman asinkron adalah objek Task
dan Task<T>
, yang memodelkan operasi asinkron. Objek tersebut didukung oleh kata kunci async
dan await
. Model ini cukup sederhana dalam banyak kasus:
- Untuk kode terikat I/O, Anda menunggu operasi yang mengembalikan
Task
atauTask<T>
di dalam metodeasync
. - Untuk kode terikat CPU, Anda menunggu operasi yang dimulai pada utas latar belakang dengan metode Task.Run.
Kata kunci await
adalah tempat keajaiban terjadi. Ini menghasilkan kontrol kepada pemanggil metode yang melakukan await
, dan pada akhirnya memungkinkan antarmuka pengguna menjadi responsif atau layanan menjadi elastis. Meskipun ada cara lain untuk mendekati kode asinkron selain async
dan await
, artikel ini berfokus pada konstruksi tingkat bahasa.
Catatan
Dalam beberapa contoh System.Net.Http.HttpClient kelas berikut digunakan untuk mengunduh beberapa data dari layanan web.
Objek s_httpClient
yang digunakan dalam contoh ini adalah bidang Program
statis kelas (silakan periksa contoh lengkapnya):
private static readonly HttpClient s_httpClient = new();
Contoh terikat I/O: Pengunduhan data dari layanan web
Anda mungkin perlu mengunduh beberapa data dari layanan web saat tombol ditekan, tetapi tidak ingin memblokir utas antarmuka pengguna. Ini dapat dicapai seperti berikut ini:
s_downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await s_httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
Kode mengekspresikan niat (mengunduh data secara asinkron) tanpa terjebak dalam interaksi dengan objek Task
.
Contoh terikat CPU: Melakukan penghitungan untuk game
Katakanlah Anda sedang menulis game seluler di mana menekan tombol dapat menimbulkan kerusakan pada banyak musuh di layar. Melakukan kalkulasi kerusakan bisa mahal, dan melakukannya pada utas antarmuka pengguna akan membuat game tampak terjeda saat penghitungan dilakukan!
Cara terbaik untuk menangani hal ini adalah dengan memulai utas latar belakang, yang melakukan pekerjaan menggunakan Task.Run
, dan menunggu hasilnya menggunakan await
. Hal ini memungkinkan antarmuka pengguna untuk merasa lancar saat pekerjaan sedang dilakukan.
static DamageResult CalculateDamageDone()
{
return new DamageResult()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
};
}
s_calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
Kode ini dengan jelas mengungkapkan niat dari peristiwa klik tombol, tidak memerlukan pengelolaan utas latar belakang secara manual, dan melakukannya dengan cara yang tidak memblokir.
Apa yang sebenarnya terjadi
Di sisi C#, kompilator mengubah kode Anda menjadi mesin status yang melacak hal-hal seperti menghasilkan eksekusi ketika await
tercapai dan melanjutkan eksekusi ketika pekerjaan latar belakang telah selesai.
Untuk yang cenderung teoritis, ini adalah implementasi dari Promise Model asinkron.
Bagian-bagian kunci untuk dipahami
- Kode asinkron dapat digunakan untuk kode terikat I/O dan terikat CPU, tetapi berbeda untuk setiap skenario.
- Kode asinkron menggunakan
Task<T>
danTask
, yang merupakan konstruksi yang digunakan untuk memodelkan pekerjaan yang dilakukan di latar belakang. - Kata kunci
async
mengubah metode menjadi metode asinkron, yang memungkinkan Anda menggunakan kata kunciawait
dalam isinya. - Ketika kata kunci
await
diterapkan, kata kunci menangguhkan metode panggilan dan menghasilkan kontrol kembali ke pemanggilnya hingga tugas yang ditunggu selesai. await
hanya dapat digunakan di dalam metode asinkron.
Mengenali pekerjaan yang terikat CPU dan terikat I/O
Dua contoh pertama dari panduan ini menunjukkan bagaimana Anda dapat menggunakan async
dan await
untuk pekerjaan terikat I/O dan terikat CPU. Ini adalah kunci di mana Anda dapat mengidentifikasi ketika pekerjaan yang perlu Anda lakukan adalah terikat I/O atau terikat CPU karena dapat sangat memengaruhi performa kode Anda dan berpotensi menyebabkan penyalahgunaan konstruksi tertentu.
Berikut adalah dua pertanyaan yang harus Anda ajukan sebelum menulis kode apa pun:
Apakah kode Anda akan "menunggu" sesuatu, seperti data dari database?
Jika jawaban Anda adalah "ya", maka pekerjaan Anda terikat I/O.
Apakah kode Anda akan melakukan komputasi yang mahal?
Jika Anda menjawab "ya", maka pekerjaan Anda terikat CPU.
Jika pekerjaan yang Anda miliki terikat I/O, gunakan async
dan await
tanpa Task.Run
. Anda tidak boleh menggunakan Task Parallel Library.
Jika pekerjaan yang Anda miliki terikat CPU dan Anda peduli tentang responsivitas, gunakan async
dan await
, tetapi melahirkan pekerjaan di utas lain dengan Task.Run
. Jika pekerjaan sesuai untuk konkurensi dan paralelisme, pertimbangkan juga untuk menggunakan Task Parallel Library.
Selain itu, Anda harus selalu mengukur eksekusi kode Anda. Misalnya, Anda mungkin menemukan diri Anda dalam situasi di mana pekerjaan terikat CPU Anda tidak cukup mahal dibandingkan dengan overhead pengalihan konteks saat membuat beberapa utas. Setiap pilihan memiliki barter, dan Anda harus memilih barter yang tepat untuk situasi Anda.
Contoh lainnya
Contoh berikut menunjukkan berbagai cara Anda dapat menulis kode asinkron di C#. Mereka mencakup beberapa skenario berbeda yang mungkin Anda temui.
Mengekstrak data dari jaringan
Cuplikan ini mengunduh HTML dari URL yang diberikan dan menghitung berapa kali string ".NET" terjadi dalam HTML. Ini menggunakan ASP.NET untuk menentukan metode pengontrol API Web, yang melakukan tugas ini dan mengembalikan angka.
Catatan
Jika Anda berencana melakukan penguraian HTML dalam kode produksi, jangan gunakan ekspresi reguler. Gunakan pustaka penguraian sebagai gantinya.
[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
// Suspends GetDotNetCount() to allow the caller (the web server)
// to accept another request, rather than blocking on this one.
var html = await s_httpClient.GetStringAsync(URL);
return Regex.Matches(html, @"\.NET").Count;
}
Berikut adalah skenario yang sama yang ditulis untuk Aplikasi Universal Windows, yang melakukan tugas yang sama saat Tombol ditekan:
private readonly HttpClient _httpClient = new HttpClient();
private async void OnSeeTheDotNetsButtonClick(object sender, RoutedEventArgs e)
{
// Capture the task handle here so we can await the background task later.
var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://dotnetfoundation.org");
// Any other work on the UI thread can be done here, such as enabling a Progress Bar.
// This is important to do here, before the "await" call, so that the user
// sees the progress bar before execution of this method is yielded.
NetworkProgressBar.IsEnabled = true;
NetworkProgressBar.Visibility = Visibility.Visible;
// The await operator suspends OnSeeTheDotNetsButtonClick(), returning control to its caller.
// This is what allows the app to be responsive and not block the UI thread.
var html = await getDotNetFoundationHtmlTask;
int count = Regex.Matches(html, @"\.NET").Count;
DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";
NetworkProgressBar.IsEnabled = false;
NetworkProgressBar.Visibility = Visibility.Collapsed;
}
Tunggu hingga beberapa tugas selesai
Anda mungkin menemukan diri Anda dalam situasi di mana Anda perlu mengambil beberapa bagian data secara bersamaan. API Task
berisi dua metode, Task.WhenAll dan Task.WhenAny, yang memungkinkan Anda menulis kode asinkron yang menunggu tanpa pemblokiran pada beberapa pekerjaan latar belakang.
Contoh ini menunjukkan bagaimana Anda dapat mengambil data User
untuk satu set userId
.
private static async Task<User> GetUserAsync(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
return await Task.FromResult(new User() { id = userId });
}
private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
}
return await Task.WhenAll(getUserTasks);
}
Berikut adalah cara lain untuk menulis ini dengan lebih ringkas menggunakan LINQ:
private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
Meskipun kodenya lebih sedikit, berhati-hatilah saat mencampur LINQ dengan kode asinkron. Karena LINQ menggunakan eksekusi yang ditangguhkan (malas), panggilan asinkron tidak akan segera terjadi seperti yang terjadi dalam perulangan foreach
kecuali Anda memaksa urutan yang dihasilkan untuk melakukan beralih dengan panggilan ke .ToList()
atau .ToArray()
. Contoh di atas menggunakan Enumerable.ToArray untuk melakukan kueri dengan bersemangat dan menyimpan hasilnya dalam array. Itu memaksa kode id => GetUserAsync(id)
untuk menjalankan dan memulai tugas.
Info dan saran penting
Dengan pemrograman asinkron, ada beberapa detail yang perlu diingat yang dapat mencegah perilaku tak terduga.
async
metode harus memilikiawait
kata kunci dalam tubuh mereka atau mereka tidak akan pernah menghasilkan!Penting untuk mengingat ini. Jika
await
tidak digunakan dalam isi metodeasync
, kompiler C# mengeluarkan peringatan, tetapi kode mengompilasi dan berjalan seolah-olah itu adalah metode normal. Ini sangat tidak efisien, karena mesin status yang dihasilkan oleh kompiler C# untuk metode asinkron tidak mencapai apa pun.Tambahkan "Asinkron" sebagai akhiran dari setiap nama metode asinkron yang Anda tulis.
Ini adalah konvensi yang digunakan dalam .NET untuk lebih mudah membedakan metode sinkron dan asinkron. Metode tertentu yang tidak secara eksplisit dipanggil oleh kode Anda (seperti penanganan aktivitas atau metode pengontrol web) tidak selalu berlaku. Karena mereka tidak secara eksplisit dipanggil oleh kode Anda, menjadi eksplisit tentang penamaan mereka tidak begitu penting.
async void
hanya boleh digunakan untuk penanganan aktivitas.async void
adalah satu-satunya cara untuk memungkinkan penanganan aktivitas asinkron berfungsi karena peristiwa tidak memiliki jenis pengembalian (sehingga tidak dapat menggunakanTask
danTask<T>
). Penggunaan lainasync void
tidak mengikuti model TAP dan dapat menjadi tantangan untuk digunakan, seperti:- Pengecualian yang dilemparkan dalam metode
async void
tidak dapat ditangkap di luar metode itu. - Metode
async void
sulit diuji. - Metode
async void
dapat menyebabkan efek samping yang buruk jika pemanggil tidak mengharapkannya menjadi asinkron.
- Pengecualian yang dilemparkan dalam metode
Hati-hati saat menggunakan lambda asinkron dalam ekspresi LINQ
Ekspresi Lambda dalam LINQ menggunakan eksekusi yang ditangguhkan, yang berarti kode dapat berakhir dieksekusi pada saat Anda tidak mengharapkannya. Pengenalan tugas pemblokiran ke dalamnya dapat dengan mudah mengakibatkan kebuntuan jika tidak ditulis dengan benar. Selain itu, bersarangnya kode asinkron seperti ini juga dapat mempersulit pertimbangan eksekusi kode. Asinkron dan LINQ sangat kuat, tetapi harus digunakan bersama-sama dengan hati-hati dan sejelas mungkin.
Menulis kode yang menunggu Tugas dengan cara yang tidak memblokir
Memblokir utas saat ini sebagai sarana untuk menunggu
Task
selesai dapat mengakibatkan kebuntuan dan utas konteks yang diblokir dan dapat memerlukan penanganan kesalahan yang lebih kompleks. Tabel berikut ini menyediakan panduan tentang cara menangani menunggu tugas dengan cara yang tidak memblokir:Gunakan ini... Alih-alih ini... Ketika ingin melakukan ini... await
Task.Wait
atauTask.Result
Mengambil hasil proses di latar belakang await Task.WhenAny
Task.WaitAny
Menunggu tugas apa pun selesai await Task.WhenAll
Task.WaitAll
Menunggu semua tugas selesai await Task.Delay
Thread.Sleep
Menunggu jangka waktu tertentu Pertimbangkan untuk menggunakan
ValueTask
jika memungkinkanMengembalikan objek
Task
dari metode asinkron dapat menyebabkan kemacetan performa di jalur tertentu.Task
adalah jenis referensi, jadi menggunakannya berarti mengalokasikan objek. Dalam kasus di mana metode yang dinyatakan dengan pengubahasync
mengembalikan hasil yang di-cache atau diselesaikan secara sinkron, alokasi tambahan dapat menjadi biaya waktu yang signifikan dalam bagian kode yang kritis performa. Ini bisa menjadi mahal jika alokasi tersebut terjadi dalam perulangan yang ketat. Untuk informasi selengkapnya, lihat jenis pengembalian asinkron umum.Pertimbangkan untuk menggunakan
ConfigureAwait(false)
Pertanyaan umumnya adalah, "kapan saya harus menggunakan metode Task.ConfigureAwait(Boolean) ini?". Metode ini memungkinkan instans
Task
untuk mengonfigurasi awaiter-nya. Ini adalah pertimbangan penting dan pengaturan yang salah berpotensi memiliki implikasi performa dan bahkan kebuntuan. Untuk informasi selengkapnya tentangConfigureAwait
, lihat ConfigureAwait FAQ.Menulis kode yang kurang stateful
Jangan bergantung pada status objek global atau eksekusi metode tertentu. Sebaliknya, hanya bergantung pada nilai metode yang dikembalikan. Mengapa?
- Kode akan lebih mudah untuk dipikirkan.
- Kode akan lebih mudah diuji.
- Mencampur kode asinkron dan sinkron jauh lebih sederhana.
- Kondisi race biasanya dapat dihindari sama sekali.
- Bergantung pada nilai pengembalian membuat koordinasi kode asinkron menjadi sederhana.
- (Bonus) ini bekerja sangat baik dengan injeksi dependensi.
Tujuan yang disarankan adalah untuk mencapai Transparansi Referensial yang lengkap atau hampir lengkap dalam kode Anda. Melakukannya akan menghasilkan basis kode yang dapat diprediksi, dapat diuji, dan dapat dipertahankan.
Contoh lengkap
Kode berikut adalah teks lengkap dari file Program.cs sebagai contoh.
using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.AspNetCore.Mvc;
class Button
{
public Func<object, object, Task>? Clicked
{
get;
internal set;
}
}
class DamageResult
{
public int Damage
{
get { return 0; }
}
}
class User
{
public bool isEnabled
{
get;
set;
}
public int id
{
get;
set;
}
}
public class Program
{
private static readonly Button s_downloadButton = new();
private static readonly Button s_calculateButton = new();
private static readonly HttpClient s_httpClient = new();
private static readonly IEnumerable<string> s_urlList = new string[]
{
"https://learn.microsoft.com",
"https://learn.microsoft.com/aspnet/core",
"https://learn.microsoft.com/azure",
"https://learn.microsoft.com/azure/devops",
"https://learn.microsoft.com/dotnet",
"https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio",
"https://learn.microsoft.com/education",
"https://learn.microsoft.com/shows/net-core-101/what-is-net",
"https://learn.microsoft.com/enterprise-mobility-security",
"https://learn.microsoft.com/gaming",
"https://learn.microsoft.com/graph",
"https://learn.microsoft.com/microsoft-365",
"https://learn.microsoft.com/office",
"https://learn.microsoft.com/powershell",
"https://learn.microsoft.com/sql",
"https://learn.microsoft.com/surface",
"https://dotnetfoundation.org",
"https://learn.microsoft.com/visualstudio",
"https://learn.microsoft.com/windows",
"https://learn.microsoft.com/maui"
};
private static void Calculate()
{
// <PerformGameCalculation>
static DamageResult CalculateDamageDone()
{
return new DamageResult()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
};
}
s_calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
// </PerformGameCalculation>
}
private static void DisplayDamage(DamageResult damage)
{
Console.WriteLine(damage.Damage);
}
private static void Download(string URL)
{
// <UnblockingDownload>
s_downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await s_httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
// </UnblockingDownload>
}
private static void DoSomethingWithData(object stringData)
{
Console.WriteLine("Displaying data: ", stringData);
}
// <GetUsersForDataset>
private static async Task<User> GetUserAsync(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
return await Task.FromResult(new User() { id = userId });
}
private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
}
return await Task.WhenAll(getUserTasks);
}
// </GetUsersForDataset>
// <GetUsersForDatasetByLINQ>
private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
// </GetUsersForDatasetByLINQ>
// <ExtractDataFromNetwork>
[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
// Suspends GetDotNetCount() to allow the caller (the web server)
// to accept another request, rather than blocking on this one.
var html = await s_httpClient.GetStringAsync(URL);
return Regex.Matches(html, @"\.NET").Count;
}
// </ExtractDataFromNetwork>
static async Task Main()
{
Console.WriteLine("Application started.");
Console.WriteLine("Counting '.NET' phrase in websites...");
int total = 0;
foreach (string url in s_urlList)
{
var result = await GetDotNetCount(url);
Console.WriteLine($"{url}: {result}");
total += result;
}
Console.WriteLine("Total: " + total);
Console.WriteLine("Retrieving User objects with list of IDs...");
IEnumerable<int> ids = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
var users = await GetUsersAsync(ids);
foreach (User? user in users)
{
Console.WriteLine($"{user.id}: isEnabled={user.isEnabled}");
}
Console.WriteLine("Application ending.");
}
}
// Example output:
//
// Application started.
// Counting '.NET' phrase in websites...
// https://learn.microsoft.com: 0
// https://learn.microsoft.com/aspnet/core: 57
// https://learn.microsoft.com/azure: 1
// https://learn.microsoft.com/azure/devops: 2
// https://learn.microsoft.com/dotnet: 83
// https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio: 31
// https://learn.microsoft.com/education: 0
// https://learn.microsoft.com/shows/net-core-101/what-is-net: 42
// https://learn.microsoft.com/enterprise-mobility-security: 0
// https://learn.microsoft.com/gaming: 0
// https://learn.microsoft.com/graph: 0
// https://learn.microsoft.com/microsoft-365: 0
// https://learn.microsoft.com/office: 0
// https://learn.microsoft.com/powershell: 0
// https://learn.microsoft.com/sql: 0
// https://learn.microsoft.com/surface: 0
// https://dotnetfoundation.org: 16
// https://learn.microsoft.com/visualstudio: 0
// https://learn.microsoft.com/windows: 0
// https://learn.microsoft.com/maui: 6
// Total: 238
// Retrieving User objects with list of IDs...
// 1: isEnabled= False
// 2: isEnabled= False
// 3: isEnabled= False
// 4: isEnabled= False
// 5: isEnabled= False
// 6: isEnabled= False
// 7: isEnabled= False
// 8: isEnabled= False
// 9: isEnabled= False
// 0: isEnabled= False
// Application ending.