Zaman uyumsuz programlama senaryoları
G/Ç'ye bağlı gereksinimleriniz varsa (ağdan veri isteme, veritabanına erişme veya dosya sistemine okuma ve yazma gibi), zaman uyumsuz programlamayı kullanmak istersiniz. Zaman uyumsuz kod yazmak için de iyi bir senaryo olan pahalı bir hesaplama gerçekleştirme gibi CPU'ya bağlı kodunuz da olabilir.
C# dil düzeyinde zaman uyumsuz programlama modeline sahiptir. Bu model, geri çağırmaları hokkabazlık yapmak veya zaman uyumsuzluk desteği olan bir kitaplığa uymak zorunda kalmadan zaman uyumsuz kodu kolayca yazmanızı sağlar. Görev Tabanlı Zaman Uyumsuz Desen (TAP) olarak bilinen şeyi izler.
Zaman uyumsuz modele genel bakış
Zaman uyumsuz programlamanın temeli, zaman uyumsuz işlemleri modelleyen ve Task<T>
nesneleridirTask
. ve anahtar sözcükleri tarafından async
await
desteklenir. Model çoğu durumda oldukça basittir:
- G/Ç ile ilişkili kod için, bir yöntemin içinde veya
Task<T>
döndürenTask
birasync
işlem beklersiniz. - CPU'ya bağlı kod için yöntemiyle Task.Run bir arka plan iş parçacığında başlatılan bir işlemi beklersiniz.
Anahtar await
sözcük, sihrin gerçekleştiği yerdir. tarafından gerçekleştirilen await
yöntemi çağırana denetim verir ve sonuçta bir kullanıcı arabiriminin yanıt vermesine veya hizmetin esnek olmasına izin verir. ve await
dışında async
zaman uyumsuz koda yaklaşmanın yolları olsa da, bu makale dil düzeyindeki yapılara odaklanır.
Not
Aşağıdaki örneklerden System.Net.Http.HttpClient bazılarında sınıfı, bir web hizmetinden bazı verileri indirmek için kullanılır.
s_httpClient
Bu örneklerde kullanılan nesne, sınıfın Program
statik bir alanıdır (lütfen tam örneği denetleyin):
private static readonly HttpClient s_httpClient = new();
G/Ç bağlantılı örnek: Web hizmetinden veri indirme
Bir düğmeye basıldığında ancak kullanıcı arabirimi iş parçacığını engellemek istemediğinizde web hizmetinden bazı verileri indirmeniz gerekebilir. Şu şekilde gerçekleştirilebilir:
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);
};
Kod, nesnelerle Task
etkileşimde tıkanmadan amacı (verileri zaman uyumsuz olarak indirme) ifade eder.
CPU'ya bağlı örnek: Bir oyun için hesaplama yapma
Bir düğmeye basmanın ekrandaki birçok düşmana zarar verebildiği bir mobil oyun yazdığınızı varsayalım. Hasar hesaplaması yapmak pahalı olabilir ve ui iş parçacığında yapmak, hesaplama yapılırken oyunun duraklatılmış gibi görünmesini sağlar!
Bunu işlemenin en iyi yolu, kullanarak işi Task.Run
yapan ve kullanarak await
sonucunu bekleyen bir arka plan iş parçacığı başlatmaktır. Bu, iş yapılırken kullanıcı arabiriminin rahat hissetmesini sağlar.
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);
};
Bu kod, düğmenin tıklama olayının amacını açıkça ifade eder, arka plan iş parçacığını el ile yönetmeyi gerektirmez ve bunu engellemeyen bir şekilde yapar.
Kapakların altında ne olur?
Öğelerin C# tarafında, derleyici kodunuzu bir öğesine ulaşıldığında await
yürütmeyi döndürme ve arka plan işi tamamlandığında yürütmeyi devam ettiren bir durum makinesine dönüştürür.
Teorik olarak eğilimli olan için bu, zaman uyumsuzların Promise Modelinin bir uygulamasıdır.
Anlaşılması gereken önemli parçalar
- Zaman uyumsuz kod hem G/Ç'ye hem de CPU'ya bağlı kod için kullanılabilir, ancak her senaryo için farklı şekilde kullanılabilir.
- Zaman uyumsuz kod,
Task<T>
arka planda yapılan işleri modellemek için kullanılan yapılar olan veTask
kullanır. - anahtar
async
sözcüğü, bir yöntemi zaman uyumsuz bir yönteme dönüştürür ve bu sayede anahtar sözcüğü gövdesinde kullanabilirsinizawait
. - Anahtar sözcük uygulandığında
await
, çağrı yöntemini askıya alır ve beklenen görev tamamlanana kadar denetimi çağırana geri verir. await
yalnızca zaman uyumsuz bir yöntem içinde kullanılabilir.
CPU'ya bağlı ve G/Ç bağlı çalışmayı tanıma
Bu kılavuzun ilk iki örneği, G/Ç ve CPU'ya bağlı çalışma için ve'yi await
nasıl kullanabileceğinizi async
gösterdi. Kodunuzun performansını büyük ölçüde etkileyebileceğinden ve belirli yapıların yanlış kullanımına neden olabileceğinden, yapmanız gereken bir işin G/Ç veya CPU'ya bağlı olduğunu belirleyebilmeniz önemlidir.
Kod yazmadan önce sormanız gereken iki soru şunlardır:
Kodunuz bir veritabanındaki veriler gibi bir şeyi "bekliyor" mu olacak?
Yanıtınız "evet" ise, çalışmanız G/Ç'ye bağlıdır.
Kodunuz pahalı bir hesaplama gerçekleştirecek mi?
"Evet" yanıtını verdiyseniz işiniz CPU'ya bağlıdır.
Sahip olduğunuz iş G/Ç'ye bağlıysa ve await
olmadan Task.Run
kullanınasync
. Görev Paralel Kitaplığı'nı kullanmamalısınız.
Sahip olduğunuz iş CPU'ya bağlıysa ve yanıt verme hızını önemsiyorsanız ve await
kullanınasync
, ancak ile Task.Run
başka bir iş parçacığında çalışmaya bırakın. Çalışma eşzamanlılık ve paralellik için uygunsa, Görev Paralel Kitaplığı'nı da kullanmayı göz önünde bulundurun.
Ayrıca, kodunuzun yürütülmesini her zaman ölçmeniz gerekir. Örneğin, çok iş parçacığı kullanıldığında bağlam anahtarlarının yüküne kıyasla CPU'ya bağlı çalışmanızın yeterince maliyetli olmadığı bir durumda kendinizi bulabilirsiniz. Her seçimin bir dezavantajı vardır ve durumunuz için doğru dengeyi seçmelisiniz.
Daha fazla örnek
Aşağıdaki örneklerde C# dilinde zaman uyumsuz kod yazmanın çeşitli yolları gösterilmektedir. Karşılaşabileceğiniz birkaç farklı senaryo ele alınıyor.
Ağdan veri ayıklama
Bu kod parçacığı, verilen URL'den HTML'yi indirir ve HTML'de ".NET" dizesinin kaç kez gerçekleştiğini sayar. Bu görevi gerçekleştiren ve sayıyı döndüren bir Web API denetleyicisi yöntemi tanımlamak için ASP.NET kullanır.
Not
Üretim kodunda HTML ayrıştırma yapmayı planlıyorsanız normal ifadeleri kullanmayın. Bunun yerine ayrıştırma kitaplığı kullanın.
[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;
}
Bir Düğmeye basıldığında aynı görevi gerçekleştiren Evrensel Windows Uygulaması için yazılan senaryonun aynısı aşağıdadır:
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;
}
Birden çok görev tamamlanmasını bekleyin
Kendinizi birden çok veri parçasını eşzamanlı olarak almanız gereken bir durumda bulabilirsiniz. Task
API, Task.WhenAll Task.WhenAnybirden çok arka plan işi üzerinde engelleyici olmayan bir bekleme gerçekleştiren zaman uyumsuz kod yazmanıza olanak sağlayan ve olmak üzere iki yöntem içerir.
Bu örnekte, bir dizi sn için verileri nasıl alabileceğiniz User
gösterilmektedir 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);
}
LINQ kullanarak bunu daha kısa bir şekilde yazmanın başka bir yolu da aşağıdadır:
private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
Daha az kod olsa da LINQ'yi zaman uyumsuz kodla karıştırırken dikkatli olun. LINQ ertelenmiş (gecikmeli) yürütme kullandığından, oluşturulan diziyi veya .ToArray()
çağrısıyla yinelemeye zorlamadığınız sürece zaman uyumsuz çağrılar .ToList()
döngüde foreach
olduğu gibi hemen gerçekleşmez. Yukarıdaki örnek, sorguyu hevesle gerçekleştirmek ve sonuçları bir dizide depolamak için kullanır Enumerable.ToArray . Bu, kodu id => GetUserAsync(id)
çalıştırmaya ve görevi başlatmaya zorlar.
Önemli bilgiler ve öneriler
Zaman uyumsuz programlamada beklenmeyen davranışları önleyebilecek bazı ayrıntılar göz önünde bulundurulması gerekir.
async
yöntemlerinawait
bedenlerinde anahtar sözcük yoksa asla verim vermezler!Bu, göz önünde bulundurulması gereken önemli bir konudur. Bir
async
yöntemin gövdesinde kullanılmazsaawait
, C# derleyicisi bir uyarı oluşturur, ancak kod derlenir ve normal bir yöntemmiş gibi çalışır. Zaman uyumsuz yöntem için C# derleyicisi tarafından oluşturulan durum makinesi hiçbir şey başarmadığından, bu inanılmaz derecede verimsizdir.Yazdığınız her zaman uyumsuz yöntem adının soneki olarak "Async" ekleyin.
Bu, zaman uyumlu ve zaman uyumsuz yöntemleri daha kolay ayırt etmek için .NET'te kullanılan kuraldır. Kodunuz tarafından açıkça çağrılmamış bazı yöntemlerin (olay işleyicileri veya web denetleyicisi yöntemleri gibi) geçerli olması gerekmez. Bunlar kodunuz tarafından açıkça çağrılmadığından, adlandırmaları hakkında açık olmak o kadar önemli değildir.
async void
yalnızca olay işleyicileri için kullanılmalıdır.async void
olayların dönüş türleri olmadığından zaman uyumsuz olay işleyicilerinin çalışmasına izin vermenin tek yoludur (bu nedenle veTask<T>
'yiTask
kullanamaz). Diğer kullanımlarıasync void
TAP modelini izlemez ve kullanımı aşağıdakiler gibi zor olabilir:- Bir
async void
yöntemde oluşan özel durumlar bu yöntemin dışında yakalanamaz. async void
yöntemleri test etmek zordur.async void
yöntemleri, çağıranın zaman uyumsuz olmasını beklemiyorsa hatalı yan etkilere neden olabilir.
- Bir
LINQ ifadelerinde zaman uyumsuz lambda kullanırken dikkatli bir şekilde koşun
LINQ'teki Lambda ifadeleri ertelenmiş yürütmeyi kullanır; bu da kodun beklediğiniz bir zamanda yürütülebileceği anlamına gelir. Engelleme görevlerinin buna eklenmesi, doğru yazılmaması durumunda kolayca kilitlenmeye neden olabilir. Buna ek olarak, zaman uyumsuz kodun bunun gibi iç içe yerleştirilmiş olması, kodun yürütülmesiyle ilgili mantık yürütmeyi de zorlaştırabilir. Zaman uyumsuz ve LINQ güçlüdür, ancak mümkün olduğunca dikkatli ve net bir şekilde birlikte kullanılmalıdır.
Görevleri engelleyici olmayan bir şekilde bekleyen kod yazma
Geçerli iş parçacığının tamamlanmasını bekleme
Task
aracı olarak engellenmesi kilitlenmelere ve engellenen bağlam iş parçacıklarına neden olabilir ve daha karmaşık hata işleme gerektirebilir. Aşağıdaki tabloda, görevleri engelleyici olmayan bir şekilde beklemeyle ilgili yönergeler sağlanmaktadır:Bunu kullanın... Bunun yerine... Bunu yapmak istediğinizde... await
Task.Wait
veyaTask.Result
Arka plan görevinin sonucunu alma await Task.WhenAny
Task.WaitAny
Herhangi bir görevin tamamlanmasını bekleme await Task.WhenAll
Task.WaitAll
Tüm görevlerin tamamlanmasını bekleme await Task.Delay
Thread.Sleep
Bir süre bekleniyor Mümkün olduğunca kullanmayı
ValueTask
göz önünde bulundurunTask
Bir nesneyi zaman uyumsuz yöntemlerden döndürmek, belirli yollarda performans sorunlarına neden olabilir.Task
bir başvuru türüdür, bu nedenle kullanmak bir nesneyi ayırma anlamına gelir. Değiştirici ile bildirilen bir yönteminasync
önbelleğe alınmış bir sonuç döndürdüğü veya zaman uyumlu olarak tamamlandığı durumlarda, ek ayırmalar kodun performans açısından kritik bölümlerinde önemli bir zaman maliyetine dönüşebilir. Bu ayırmalar sıkı döngülerde gerçekleşirse maliyetli hale gelebilir. Daha fazla bilgi için bkz . Genelleştirilmiş zaman uyumsuz dönüş türleri.Kullanmayı göz önünde bulundurun
ConfigureAwait(false)
Sık sorulan bir soru şudur: "Yöntemini ne zaman kullanmalıyım Task.ConfigureAwait(Boolean) ?". yöntemi, bir
Task
örneğin awaiter'ını yapılandırmasına olanak tanır. Bu önemli bir konudur ve yanlış bir şekilde ayarlanması, performans üzerindeki etkileri ve hatta kilitlenmeleri olabilir. hakkındaConfigureAwait
daha fazla bilgi için bkz . ConfigureAwait SSS.Daha az durum bilgisi olan kod yazma
Genel nesnelerin durumuna veya belirli yöntemlerin yürütülmesine bağlı değildir. Bunun yerine, yalnızca yöntemlerin dönüş değerlerine bağlıdır. Neden?
- Kod hakkında daha kolay mantık yürütmesi gerekir.
- Kodu test etmek daha kolay olacaktır.
- Zaman uyumsuz ve zaman uyumlu kodu karıştırmak çok daha basittir.
- Yarış koşulları genellikle tamamen önlenebilir.
- Dönüş değerlerine bağlı olarak zaman uyumsuz kodun koordinasyonu basitleştirilir.
- (Bonus) bağımlılık ekleme ile gerçekten iyi çalışır.
Önerilen bir hedef, kodunuzda eksiksiz veya neredeyse eksiksiz Bilgi Saydamlığı elde etmektir. Bunu yaptığınızda tahmin edilebilir, test edilebilir ve sürdürülebilir bir kod tabanı elde edilir.
Tam örnek
Aşağıdaki kod, örneğin Program.cs dosyasının tam metnidir.
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.