Not
Bu sayfaya erişim yetkilendirme gerektiriyor. Oturum açmayı veya dizinleri değiştirmeyi deneyebilirsiniz.
Bu sayfaya erişim yetkilendirme gerektiriyor. Dizinleri değiştirmeyi deneyebilirsiniz.
Zaman uyumsuz işlemlerle çalışmak için Görev Tabanlı Zaman Uyumsuz Deseni (TAP) kullandığınızda, engelleme olmadan beklemeyi başarmak için geri çağırmaları kullanabilirsiniz. Görevler için bu, Task.ContinueWith gibi yöntemlerle elde edilir. Dil tabanlı zaman uyumsuz destek, normal denetim akışında zaman uyumsuz işlemlerin beklenmesine izin vererek geri çağırmaları gizler ve derleyici tarafından oluşturulan kod aynı API düzeyinde destek sağlar.
Await ile Yürütmeyi Askıya Alma
C# dilinde await anahtar sözcüğünü ve Visual Basic'teki Await İşlecini kullanarak Task ve Task<TResult> nesnelerini zaman uyumsuz şekilde bekleyebilirsiniz. Bir Task beklediğinizde, await ifadesi void türündedir. Bir Task<TResult> beklediğinizde, await ifadesi TResult türündedir. Bir await ifadenin, zaman uyumsuz bir yöntemin gövdesinde yer alması gerekir. (Bu dil özellikleri .NET Framework 4.5'te kullanıma sunulmuştur.)
Perde arkasında, await işlevi bir devam mekanizması kullanarak göreve bir geri çağırma yükler. Bu geriçağırım, askıya alma noktasında asenkron yöntemi sürdürür. Zaman uyumsuz yöntem sürdürülürken, beklenen işlem başarıyla tamamlandıysa ve bir `Task<TResult>` ise, `TResult` döndürülür. Beklenen Task veya Task<TResult>Canceled durumunda sona erdiyse, bir OperationCanceledException özel durum oluşturulur.
Task veya Task<TResult> beklenen unsurların Faulted durumunda sona ermesi durumunda, hatayı tetikleyen istisna fırlatılır. Bir Task birden fazla istisna sonucu hata yapabilir, ancak bu istisnalardan yalnızca biri aktarılır. Ancak, Task.Exception özelliği tüm hataları içeren bir AggregateException özel durum döndürür.
Askıya alma sırasında zaman uyumsuz yöntemi yürüten iş parçacığıyla bir eşitleme bağlamı (SynchronizationContext nesne) ilişkilendirildiyse SynchronizationContext.Current (örneğin, özellik değilse null), zaman uyumsuz yöntem bağlamın Post yöntemini kullanarak aynı eşitleme bağlamında devam eder. Aksi takdirde, askıya alma sırasında geçerli olan görev zamanlayıcıya (TaskScheduler nesne) dayanır. Bu genellikle iş parçacığı havuzunu hedefleyen varsayılan görev zamanlayıcıdır (TaskScheduler.Default ). Bu görev zamanlayıcı, beklenen zaman uyumsuz işlemin tamamlandığı noktadan devam edip etmeyeceğini veya devamın zamanlanması gerekip gerekmediğini belirler. Varsayılan zamanlayıcı genellikle devamın beklenen işlemin tamamlandığı iş parçacığında çalışmasına izin verir.
Zaman uyumsuz bir yöntem çağrıldığında, işlevin gövdesini henüz tamamlanmamış bir beklenebilecek örnekteki ilk await ifadesine kadar senkron olarak yürütür ve bu noktada çağrı çağırana döner. Zaman uyumsuz yöntem döndürmezse void, devam eden hesaplamayı temsil etmek için bir Task veya Task<TResult> nesnesi döndürülür. Geçersiz olmayan bir zaman uyumsuz yöntemde, bir dönüş deyimiyle karşılaşıldığında veya yöntem gövdesinin sonuna gelindiğinde, görev son durumunda tamamlanır RanToCompletion. Eğer işlenmeyen bir özel durum, kontrolün zaman uyumsuz yöntemin gövdesinden çıkmasına neden olursa, görev Faulted durumunda biter. Eğer bu bir OperationCanceledException özel durumuysa, görev bunun yerine Canceled durumunda tamamlanır. Bu şekilde sonuç veya özel durum sonunda yayımlanır.
Bu davranışın birkaç önemli varyasyonu vardır. Performans nedenleriyle, görev beklenmeden önce bir görev zaten tamamlandıysa, kontrol bırakılmaz ve işlev yürütülmeye devam eder. Ayrıca özgün bağlama geri dönmek her zaman istenen davranış değildir ve değiştirilebilir; bu, sonraki bölümde daha ayrıntılı olarak açıklanmıştır.
Yield ve ConfigureAwait ile Askıya Alma ve Yeniden Başlatmayı Yapılandırma
Çeşitli yöntemler, zaman uyumsuz bir yöntemin yürütülmesi üzerinde daha fazla denetim sağlar. Örneğin, eşzamansız bir yönteme bir verim noktası eklemek için Task.Yield yöntemini kullanabilirsiniz.
public class Task : …
{
public static YieldAwaitable Yield();
…
}
Bu, mevcut bağlama zaman uyumsuz olarak paylaşma veya zamanlama ile eşdeğerdir.
Task.Run(async delegate
{
for(int i=0; i<1000000; i++)
{
await Task.Yield(); // fork the continuation into a separate work item
...
}
});
Asenkron bir yöntemde askıya alma ve yeniden başlatma üzerinde daha iyi denetim sağlamak için Task.ConfigureAwait yöntemini de kullanabilirsiniz. Daha önce belirtildiği gibi, geçerli bağlam varsayılan olarak zaman uyumsuz bir yöntem askıya alınırken yakalanır ve bu yakalanan bağlam, zaman uyumsuz yöntemin sürdürmesini yeniden başlatma sırasında çağırmak için kullanılır. Çoğu durumda, tam olarak istediğiniz davranış budur. Diğer durumlarda, devam bağlamını önemsemeyebilir ve bu tür gönderileri özgün bağlama geri döndürerek daha iyi bir performans elde edebilirsiniz. Bunu sağlamak için Task.ConfigureAwait yöntemini kullanarak await işlemini bağlamı yakalamayıp mevcut yerde devam etmesi için yönlendirin; bu, beklenen zaman uyumsuz işlemin tamamlandığı yerdir.
await someTask.ConfigureAwait(continueOnCapturedContext:false);
Zaman Uyumsuz İşlemi İptal Etme
.NET Framework 4'den başlayarak, iptali destekleyen TAP yöntemleri, iptal belirteci (CancellationToken nesnesi) kabul eden en az bir aşırı yükleme sağlar.
İptal belirteci, bir iptal belirteci kaynağı (CancellationTokenSource nesnesi) aracılığıyla oluşturulur. Kaynağın Token özelliği, kaynağın Cancel yöntemi çağrıldığında sinyal verilecek iptal belirtecini döndürür. Örneğin, tek bir web sayfasını indirmek ve işlemi iptal edebilmek istiyorsanız, bir CancellationTokenSource nesne oluşturur, belirtecini TAP yöntemine geçirir ve işlemi iptal etmeye hazır olduğunuzda kaynağın Cancel yöntemini çağırırsınız:
var cts = new CancellationTokenSource();
string result = await DownloadStringTaskAsync(url, cts.Token);
… // at some point later, potentially on another thread
cts.Cancel();
Birden çok eşzamansız çağrıyı iptal etmek için tüm çağrılara aynı belirteci geçirebilirsiniz.
var cts = new CancellationTokenSource();
IList<string> results = await Task.WhenAll(from url in urls select DownloadStringTaskAsync(url, cts.Token));
// at some point later, potentially on another thread
…
cts.Cancel();
Ya da aynı belirteci işlemlerin seçmeli bir alt kümesine geçirebilirsiniz:
var cts = new CancellationTokenSource();
byte [] data = await DownloadDataAsync(url, cts.Token);
await SaveToDiskAsync(outputPath, data, CancellationToken.None);
… // at some point later, potentially on another thread
cts.Cancel();
Önemli
İptal istekleri herhangi bir konu ya da thread üzerinden başlatılabilir.
İptalin hiçbir zaman talep edilmeyeceğini belirtmek için CancellationToken.None değerini iptal belirtecini kabul eden herhangi bir yönteme geçirebilirsiniz. Bu durum, özelliğin CancellationToken.CanBeCanceled değerini false olarak döndürmesine yol açar ve çağrılan yöntem buna göre optimize edebilir. Test amacıyla, belirtecin zaten iptal edilmiş veya iptal edilemeyen bir durumda başlaması gerekip gerekmediğini belirtmek için Boole değeri kabul eden oluşturucu kullanılarak örneği oluşturulmuş önceden iptal edilmiş bir iptal belirteci de geçirebilirsiniz.
bu iptal yaklaşımının çeşitli avantajları vardır:
Aynı iptal belirtecini istediğiniz sayıda zaman uyumsuz ve zaman uyumlu işlemlere geçirebilirsiniz.
Aynı iptal isteği, herhangi bir sayıda dinleyiciye dağıtılabilir.
Zaman uyumsuz API'nin geliştiricisi, iptalin istenip istenmeyeceği ve ne zaman geçerli olabileceği konusunda tam denetime sahiptir.
API'yi kullanan kod, iptal isteklerinin yayılacağı zaman uyumsuz çağrıları seçmeli olarak belirleyebilir.
İzleme İlerleme Durumu
Bazı zaman uyumsuz yöntemler, zaman uyumsuz yönteme geçirilen bir ilerleme arabirimi aracılığıyla ilerleme durumunu kullanıma sunar. Örneğin, zaman uyumsuz olarak bir metin dizesi indiren ve yol boyunca şimdiye kadar tamamlanan indirme yüzdesini içeren ilerleme güncelleştirmelerini yükselten bir işlev düşünün. Böyle bir yöntem bir Windows Presentation Foundation (WPF) uygulamasında aşağıdaki gibi kullanılabilir:
private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.IsEnabled = false;
try
{
txtResult.Text = await DownloadStringTaskAsync(txtUrl.Text,
new Progress<int>(p => pbDownloadProgress.Value = p));
}
finally { btnDownload.IsEnabled = true; }
}
Yerleşik Görev Tabanlı Kombinatörleri Kullanma
System.Threading.Tasks ad alanı, görevleri oluşturmak ve görevlerle çalışmak için çeşitli yöntemler içerir.
Task.Run
Task sınıfı, örneğin, işleri bir Run veya Task olarak iş parçacığı havuzuna kolayca atmanıza olanak sağlayan çeşitli Task<TResult> yöntemler içerir.
public async void button1_Click(object sender, EventArgs e)
{
textBox1.Text = await Task.Run(() =>
{
// … do compute-bound work here
return answer;
});
}
Örneğin, aşırı yükleme yöntemi gibi bazı Run yöntemler, Task.Run(Func<Task>) yönteminin kısaltması olarak mevcuttur. Bu aşırı yükleme, boşaltılan iş içinde await'yi kullanmanıza olanak tanır, örneğin:
public async void button1_Click(object sender, EventArgs e)
{
pictureBox1.Image = await Task.Run(async() =>
{
using(Bitmap bmp1 = await DownloadFirstImageAsync())
using(Bitmap bmp2 = await DownloadSecondImageAsync())
return Mashup(bmp1, bmp2);
});
}
Bu tür aşırı yüklemeler mantıksal olarak, Task Parallel Library içerisindeki TaskFactory.StartNew yöntemi ile uzantı yöntemi Unwrap'in eş zamanlı kullanımına eşdeğerdir.
Task.FromResult
FromResult yöntemini, verilerin zaten kullanılabilir olabileceği ve sadece bir Task<TResult> olarak yükseltilmiş bir görev döndüren yöntemden döndürülmesi gereken senaryolarda kullanın.
public Task<int> GetValueAsync(string key)
{
int cachedValue;
return TryGetCachedValue(out cachedValue) ?
Task.FromResult(cachedValue) :
GetValueAsyncInternal();
}
private async Task<int> GetValueAsyncInternal(string key)
{
…
}
Task.WhenAll
Birden çok zaman uyumsuz işlemi görev olarak temsil edenlerde zaman uyumsuz olarak beklemek için WhenAll yöntemini kullanın. Yöntem, genel olmayan görevleri veya düzensiz genel görevleri destekleyen birden çok aşırı yüklemeye sahiptir (örneğin, birden çok geçersiz döndüren işlem için zaman uyumsuz beklemek veya her değerin farklı türde olabileceği birden çok değer döndüren metot için zaman uyumsuz beklemek) ve tekdüzen bir genel görev kümesini (örneğin, birden çok TResult döndüren metotları zaman uyumsuz olarak beklemek) destekler.
Birkaç müşteriye e-posta iletileri göndermek istediğinizi varsayalım. İletileri gönderirken zamanlamalarını çakıştırabilirsiniz, böylece bir iletinin tamamlanmasını beklemeden bir sonraki iletiyi gönderebilirsiniz. Ayrıca gönderme işlemlerinin ne zaman tamamlandığını ve herhangi bir hata oluşup oluşmadığını da öğrenebilirsiniz:
IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);
Bu kod, oluşabilecek özel durumları açıkça işlemez; ancak özel durumların await'den alınan sonucu görevi olan WhenAll üzerinden yayılmasına izin verir. Özel durumları işlemek için aşağıdaki gibi bir kod kullanabilirsiniz:
IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
try
{
await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
...
}
Bu durumda, herhangi bir zaman uyumsuz işlem başarısız olursa, tüm özel durumlar, AggregateException yönteminden döndürülen Task içinde depolanan bir WhenAll özel durumda bir araya gelir. Ancak bu istisnalardan yalnızca biri await anahtar sözcüğüyle aktarılır. Tüm özel durumları incelemek istiyorsanız, önceki kodu aşağıdaki gibi yeniden yazabilirsiniz:
Task [] asyncOps = (from addr in addrs select SendMailAsync(addr)).ToArray();
try
{
await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
foreach(Task faulted in asyncOps.Where(t => t.IsFaulted))
{
… // work with faulted and faulted.Exception
}
}
Web'den zaman uyumsuz olarak birden çok dosya indirme örneğini ele alalım. Bu durumda, tüm zaman uyumsuz işlemler homojen sonuç türlerine sahiptir ve sonuçlara kolayca erişebilirsiniz:
string [] pages = await Task.WhenAll(
from url in urls select DownloadStringTaskAsync(url));
Önceki void-returning senaryosunda ele aldığımız özel durum işleme tekniklerinin aynısını kullanabilirsiniz:
Task<string> [] asyncOps =
(from url in urls select DownloadStringTaskAsync(url)).ToArray();
try
{
string [] pages = await Task.WhenAll(asyncOps);
...
}
catch(Exception exc)
{
foreach(Task<string> faulted in asyncOps.Where(t => t.IsFaulted))
{
… // work with faulted and faulted.Exception
}
}
Task.WhenAny
WhenAny yöntemini, görev olarak temsil edilen birden fazla zaman uyumsuz işlemden yalnızca birinin tamamlanmasını zaman uyumsuz olarak beklemek için kullanabilirsiniz. Bu yöntem dört birincil kullanım örneği sunar:
Yedeklilik: Bir işlemi birden çok kez gerçekleştirme ve ilk tamamlananı seçme (örneğin, tek bir sonuç oluşturacak birden çok hisse senedi teklifi web hizmetiyle iletişim kurma ve en hızlı işlemi tamamlayanı seçme).
Araya ekleme: Birden çok işlem başlatma ve tümünün tamamlanmasını bekleme, ancak tamamlandıkça işleme.
Kısıtlama: Diğer işlemler tamamlandıkça ek işlemlerin başlamasına izin verme. Bu, araya ekleme senaryosunun bir uzantısıdır.
Erken kurtarma: Örneğin, t1 göreviyle temsil edilen bir işlem, t2 göreviyle birlikte bir WhenAny görevde gruplandırılabilir ve WhenAny görevini bekleyebilirsiniz. T2 görevi zaman aşımını, iptali veya t1 tamamlanmadan önce WhenAny görevinin tamamlanmasına neden olan başka bir sinyali temsil edebilir.
Yedeklilik
Hisse senedi satın alıp almayacağınıza karar vermek istediğiniz bir olayı düşünün. Güvendiğiniz birkaç stok önerisi web hizmeti vardır, ancak günlük yüke bağlı olarak her hizmet farklı zamanlarda yavaş olabilir. Herhangi bir işlem tamamlandığında bildirim almak için yöntemini kullanabilirsiniz WhenAny :
var recommendations = new List<Task<bool>>()
{
GetBuyRecommendation1Async(symbol),
GetBuyRecommendation2Async(symbol),
GetBuyRecommendation3Async(symbol)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
if (await recommendation) BuyStock(symbol);
'nin aksine WhenAll, başarıyla tamamlanan tüm görevlerin eşlenmemiş sonuçlarını döndürür, WhenAny tamamlanan görevi döndürür. Bir görev başarısız olursa, başarısız olduğunu bilmek önemlidir ve bir görev başarılı olursa, dönüş değerinin hangi görevle ilişkilendirildiğini bilmek önemlidir. Bu nedenle, döndürülen görevin sonucuna erişmeniz veya bu örnekte gösterildiği gibi daha fazla beklemeniz gerekir.
' WhenAllde olduğu gibi özel durumlara da uyum sağlayabilmek gerekir. Tamamlanmış görevi geri aldığınızda, döndürülen görevdeki hataların yayılmasını bekleyebilir ve bunları uygun şekilde düzeltebilirsiniz; örneğin:
Task<bool> [] recommendations = …;
while(recommendations.Count > 0)
{
Task<bool> recommendation = await Task.WhenAny(recommendations);
try
{
if (await recommendation) BuyStock(symbol);
break;
}
catch(WebException exc)
{
recommendations.Remove(recommendation);
}
}
Ayrıca, ilk görev başarıyla tamamlanırsa bile sonraki görevler başarısız olabilir. Bu noktada, özel durumlarla ilgilenmek için çeşitli seçenekleriniz vardır: Başlatılan tüm görevler tamamlanana kadar bekleyebilirsiniz; bu durumda yöntemini kullanabilir WhenAll veya tüm özel durumların önemli olduğuna ve günlüğe kaydedilmesi gerektiğine karar vekleyebilirsiniz. Bunun için, görevler eşzamansız olarak tamamlandığında bildirim almak için tamamlamaları kullanabilirsiniz.
foreach(Task recommendation in recommendations)
{
var ignored = recommendation.ContinueWith(
t => { if (t.IsFaulted) Log(t.Exception); });
}
veya:
foreach(Task recommendation in recommendations)
{
var ignored = recommendation.ContinueWith(
t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}
veya hatta:
private static async void LogCompletionIfFailed(IEnumerable<Task> tasks)
{
foreach(var task in tasks)
{
try { await task; }
catch(Exception exc) { Log(exc); }
}
}
…
LogCompletionIfFailed(recommendations);
Son olarak, kalan tüm işlemleri iptal etmek isteyebilirsiniz:
var cts = new CancellationTokenSource();
var recommendations = new List<Task<bool>>()
{
GetBuyRecommendation1Async(symbol, cts.Token),
GetBuyRecommendation2Async(symbol, cts.Token),
GetBuyRecommendation3Async(symbol, cts.Token)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
cts.Cancel();
if (await recommendation) BuyStock(symbol);
Dolaşım
Web'den görüntü indirdiğiniz ve her görüntüyü işleyeceğiniz bir durum düşünün (örneğin, görüntüyü kullanıcı arabirimi denetimine ekleme). Ui iş parçacığında görüntüleri sıralı olarak işlersiniz, ancak görüntüleri mümkün olduğunca eşzamanlı olarak indirmek istersiniz. Ayrıca, görüntülerin tümü indirilene kadar kullanıcı arabirimine eklemeye devam etmek istemezsiniz. Bunun yerine, tamamlandıklarında eklemeyi tercih edersiniz.
List<Task<Bitmap>> imageTasks =
(from imageUrl in urls select GetBitmapAsync(imageUrl)).ToList();
while(imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
panel.AddImage(image);
}
catch{}
}
ayrıca, indirilen görüntülerin üzerinde yoğun işlem gerektiren bir senaryoya ThreadPool da araya ekleme uygulayabilirsiniz; örneğin:
List<Task<Bitmap>> imageTasks =
(from imageUrl in urls select GetBitmapAsync(imageUrl)
.ContinueWith(t => ConvertImage(t.Result)).ToList();
while(imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
panel.AddImage(image);
}
catch{}
}
Hız kısıtlama
Kullanıcının çok fazla görüntü indirdiği ve bu nedenle indirmelerin kısıtlanması gerektiği durumu, birbiriyle iç içe geçen işlemler örneği olarak düşünün; örneğin, sadece belirli sayıda indirmenin aynı anda gerçekleştirilmesini istiyorsunuz. Bunu başarmak için zaman uyumsuz işlemlerin bir alt kümesini başlatabilirsiniz. İşlemler tamamlandıktan sonra, yerlerine geçmek için ek işlemler başlatabilirsiniz:
const int CONCURRENCY_LEVEL = 15;
Uri [] urls = …;
int nextIndex = 0;
var imageTasks = new List<Task<Bitmap>>();
while(nextIndex < CONCURRENCY_LEVEL && nextIndex < urls.Length)
{
imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
nextIndex++;
}
while(imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
panel.AddImage(image);
}
catch(Exception exc) { Log(exc); }
if (nextIndex < urls.Length)
{
imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
nextIndex++;
}
}
Erken Kurtarma
Kullanıcının iptal isteğine aynı anda yanıt verirken bir işlemin tamamlanmasını zaman uyumsuz olarak beklediğinizi düşünün (örneğin, kullanıcı bir iptal düğmesine tıkladı). Aşağıdaki kodda bu senaryo gösterilmektedir:
private CancellationTokenSource m_cts;
public void btnCancel_Click(object sender, EventArgs e)
{
if (m_cts != null) m_cts.Cancel();
}
public async void btnRun_Click(object sender, EventArgs e)
{
m_cts = new CancellationTokenSource();
btnRun.Enabled = false;
try
{
Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text);
await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
if (imageDownload.IsCompleted)
{
Bitmap image = await imageDownload;
panel.AddImage(image);
}
else imageDownload.ContinueWith(t => Log(t));
}
finally { btnRun.Enabled = true; }
}
private static async Task UntilCompletionOrCancellation(
Task asyncOp, CancellationToken ct)
{
var tcs = new TaskCompletionSource<bool>();
using(ct.Register(() => tcs.TrySetResult(true)))
await Task.WhenAny(asyncOp, tcs.Task);
return asyncOp;
}
Bu uygulama, siz vazgeçmeye karar verir vermez kullanıcı arabirimini yeniden etkinleştirir, ancak arka plandaki zaman uyumsuz işlemleri iptal etmez. Bir diğer alternatif, süreçten vazgeçmeye karar verdiğinizde bekleyen işlemleri iptal etmek, ancak iptal isteği nedeniyle erken sona erme ihtimali olan işlemler tamamlanana kadar kullanıcı arabirimini yeniden oluşturmamaktır.
private CancellationTokenSource m_cts;
public async void btnRun_Click(object sender, EventArgs e)
{
m_cts = new CancellationTokenSource();
btnRun.Enabled = false;
try
{
Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text, m_cts.Token);
await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
Bitmap image = await imageDownload;
panel.AddImage(image);
}
catch(OperationCanceledException) {}
finally { btnRun.Enabled = true; }
}
Erken kurtarma yöntemlerinin bir başka örneği, sonraki bölümde tartışıldığı gibi WhenAny yöntemi ile Delay yönteminin birlikte kullanılmasıdır.
Task.Delay
Zaman uyumsuz bir yöntemin yürütülmesine duraklamalar eklemek için Task.Delay yöntemini kullanabilirsiniz. Bu, yoklama döngüleri oluşturma ve kullanıcı girişinin işlenmesini önceden belirlenmiş bir süre boyunca geciktirme gibi birçok işlev türü için kullanışlıdır. Task.Delay yöntemi, Task.WhenAny ile birlikte awaits üzerinde zaman aşımları uygulamak için yararlı olabilir.
Daha büyük bir asenkron işlemin parçası olan bir görev (örneğin, bir ASP.NET web hizmeti) tamamlanması çok uzun sürerse ve hiç tamamlanamazsa, genel işlem zarar görebilir. Bu nedenle, zaman uyumsuz bir işlem beklerken zaman aşımı belirleyebilmek önemlidir. Eşzamanlı Task.Wait, Task.WaitAll ve Task.WaitAny yöntemleri zaman aşımı değerlerini kabul eder, ancak karşılık gelen TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny ve daha önce bahsedilen Task.WhenAll/Task.WhenAny yöntemleri kabul etmez. Bunun yerine, zaman aşımını uygulamak için Task.Delay ve Task.WhenAny'i birlikte kullanabilirsiniz.
Örneğin, kullanıcı arabirimi uygulamanızda bir görüntü indirmek ve görüntü indirilirken kullanıcı arabirimini devre dışı bırakmak istediğinizi varsayalım. Ancak indirme işlemi çok uzun sürerse kullanıcı arabirimini yeniden etkinleştirmek ve indirmeyi atmak istersiniz:
public async void btnDownload_Click(object sender, EventArgs e)
{
btnDownload.Enabled = false;
try
{
Task<Bitmap> download = GetBitmapAsync(url);
if (download == await Task.WhenAny(download, Task.Delay(3000)))
{
Bitmap bmp = await download;
pictureBox.Image = bmp;
status.Text = "Downloaded";
}
else
{
pictureBox.Image = null;
status.Text = "Timed out";
var ignored = download.ContinueWith(
t => Trace("Task finally completed"));
}
}
finally { btnDownload.Enabled = true; }
}
Aynı durum, bir görev döndürdüğünden WhenAll birden çok indirme için de geçerlidir:
public async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.Enabled = false;
try
{
Task<Bitmap[]> downloads =
Task.WhenAll(from url in urls select GetBitmapAsync(url));
if (downloads == await Task.WhenAny(downloads, Task.Delay(3000)))
{
foreach(var bmp in downloads.Result) panel.AddImage(bmp);
status.Text = "Downloaded";
}
else
{
status.Text = "Timed out";
downloads.ContinueWith(t => Log(t));
}
}
finally { btnDownload.Enabled = true; }
}
Görev Tabanlı Birleştiriciler Oluşturma
Bir görev, zamansız bir işlemi tamamen temsil edebildiğinden ve işlemle katılmak, sonuçlarını almak vb. için senkron ve asenkron özellikler sağlayabildiğinden, daha büyük desenler oluşturmak için görevleri birleştiren kullanışlı birleştirici kitaplıklar oluşturabilirsiniz. Önceki bölümde açıklandığı gibi.NET birkaç yerleşik birleştirici içerir, ancak kendiniz de oluşturabilirsiniz. Aşağıdaki bölümlerde olası birleştirici yöntemlerinin ve türlerinin birkaç örneği verilmiştir.
RetryOnFault
Birçok durumda, önceki bir deneme başarısız olursa işlemi yeniden denemek isteyebilirsiniz. Zaman uyumlu kod için, bunu gerçekleştirmek için aşağıdaki örnekte olduğu gibi RetryOnFault bir yardımcı yöntem oluşturabilirsiniz:
public static T RetryOnFault<T>(
Func<T> function, int maxTries)
{
for(int i=0; i<maxTries; i++)
{
try { return function(); }
catch { if (i == maxTries-1) throw; }
}
return default(T);
}
TAP ile gerçekleştirilen ve dolayısıyla görev nesnelerini döndüren asenkron işlemler için neredeyse birebir yardımcı metodu oluşturabilirsiniz:
public static async Task<T> RetryOnFault<T>(
Func<Task<T>> function, int maxTries)
{
for(int i=0; i<maxTries; i++)
{
try { return await function().ConfigureAwait(false); }
catch { if (i == maxTries-1) throw; }
}
return default(T);
}
Daha sonra uygulamanın mantığına yeniden denemeleri kodlamak için bu birleştiriciyi kullanabilirsiniz; mesela:
// Download the URL, trying up to three times in case of failure
string pageContents = await RetryOnFault(
() => DownloadStringTaskAsync(url), 3);
İşlevi RetryOnFault daha da genişletebilirsiniz. Örneğin işlev, işlemin yeniden ne zaman deneneceğini belirlemek için yeniden denemeler arasında çağrılacak başka bir Func<Task> işlemi kabul edebilir; örneğin:
public static async Task<T> RetryOnFault<T>(
Func<Task<T>> function, int maxTries, Func<Task> retryWhen)
{
for(int i=0; i<maxTries; i++)
{
try { return await function().ConfigureAwait(false); }
catch { if (i == maxTries-1) throw; }
await retryWhen().ConfigureAwait(false);
}
return default(T);
}
Ardından, işlemi yeniden denemeden önce bir saniye beklemek için işlevini aşağıdaki gibi kullanabilirsiniz:
// Download the URL, trying up to three times in case of failure,
// and delaying for a second between retries
string pageContents = await RetryOnFault(
() => DownloadStringTaskAsync(url), 3, () => Task.Delay(1000));
NeedOnlyOne
Bazen, bir işlemin gecikme süresini ve başarı şansını geliştirmek için yedeklilikten yararlanabilirsiniz. Hisse senedi fiyatları sağlayan birden çok web hizmetini göz önünde bulundurun, ancak günün çeşitli saatlerinde her hizmet farklı kalite ve yanıt süreleri düzeyleri sağlayabilir. Bu dalgalanmalarla başa çıkmak için tüm web hizmetlerine istek gönderebilir ve bir yanıt alır almaz kalan istekleri iptal edebilirsiniz. Birden çok işlem başlatma, bekleme ve geri kalan işlemleri iptal etme gibi yaygın bir düzeni uygulamayı kolaylaştırmak için bir yardımcı işlevi uygulayabilirsiniz.
NeedOnlyOne Aşağıdaki örnekteki işlev bu senaryoyu gösterir:
public static async Task<T> NeedOnlyOne(
params Func<CancellationToken,Task<T>> [] functions)
{
var cts = new CancellationTokenSource();
var tasks = (from function in functions
select function(cts.Token)).ToArray();
var completed = await Task.WhenAny(tasks).ConfigureAwait(false);
cts.Cancel();
foreach(var task in tasks)
{
var ignored = task.ContinueWith(
t => Log(t), TaskContinuationOptions.OnlyOnFaulted);
}
return completed;
}
Ardından bu işlevi aşağıdaki gibi kullanabilirsiniz:
double currentPrice = await NeedOnlyOne(
ct => GetCurrentPriceFromServer1Async("msft", ct),
ct => GetCurrentPriceFromServer2Async("msft", ct),
ct => GetCurrentPriceFromServer3Async("msft", ct));
Araya Kaydedilen İşlemler
Büyük görev kümeleriyle çalışırken, iç içe geçirme senaryosunu desteklemek için WhenAny yöntemini kullanmanın olası bir performans sorunu vardır. Her çağrı, WhenAny her görevde bir devamlılık kaydedilmesiyle sonuçlanır. N görev sayısı için bu, araya ekleme işleminin ömrü boyunca oluşturulan O(N2) devamlılıklarına neden olur. Büyük bir görev kümesiyle çalışıyorsanız, performans sorununu çözmek için bir birleştirici (Interleaved aşağıdaki örnekte) kullanabilirsiniz:
static IEnumerable<Task<T>> Interleaved<T>(IEnumerable<Task<T>> tasks)
{
var inputTasks = tasks.ToList();
var sources = (from _ in Enumerable.Range(0, inputTasks.Count)
select new TaskCompletionSource<T>()).ToList();
int nextTaskIndex = -1;
foreach (var inputTask in inputTasks)
{
inputTask.ContinueWith(completed =>
{
var source = sources[Interlocked.Increment(ref nextTaskIndex)];
if (completed.IsFaulted)
source.TrySetException(completed.Exception.InnerExceptions);
else if (completed.IsCanceled)
source.TrySetCanceled();
else
source.TrySetResult(completed.Result);
}, CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
return from source in sources
select source.Task;
}
Ardından, görevlerin sonuçlarını tamamlandıklarında işlemek için birleştiriciyi kullanabilirsiniz; mesela:
IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
int result = await task;
…
}
WhenAllOrFirstException
Bazı dağılım/toplama senaryolarında, hatalardan biri olmadığı sürece kümedeki tüm görevleri beklemek isteyebilirsiniz. Bu durumda, özel durum oluştuğu anda beklemeyi durdurmak isteyebilirsiniz. Bunu aşağıdaki örnekte olduğu gibi WhenAllOrFirstException bir combinator yöntemiyle gerçekleştirebilirsiniz:
public static Task<T[]> WhenAllOrFirstException<T>(IEnumerable<Task<T>> tasks)
{
var inputs = tasks.ToList();
var ce = new CountdownEvent(inputs.Count);
var tcs = new TaskCompletionSource<T[]>();
Action<Task> onCompleted = (Task completed) =>
{
if (completed.IsFaulted)
tcs.TrySetException(completed.Exception.InnerExceptions);
if (ce.Signal() && !tcs.Task.IsCompleted)
tcs.TrySetResult(inputs.Select(t => t.Result).ToArray());
};
foreach (var t in inputs) t.ContinueWith(onCompleted);
return tcs.Task;
}
Görev Tabanlı Veri Yapıları Oluşturma
Özel görev odaklı birleştiriciler oluşturabilmenin yanı sıra, zaman uyumsuz işlemlerin sonuçlarını ve bu sonuçlarla birleşmek için gereken senkronizasyonu temsil eden Task ve Task<TResult> içerisindeki veri yapısı, zaman uyumsuz senaryolarda kullanılacak özel veri yapılarının inşasında güçlü bir temel oluşturur.
AsyncCache
Bir görevin önemli özelliklerinden biri, birden fazla tüketiciye dağıtılabilmesi ve tüm tüketicilerin bunu bekleyebilmesi, devam durumlarını kaydedebilmesi, sonucunu veya, Task<TResult> örneğinde olduğu gibi, istisnalarını alabilmesi ve bu şekilde devam edebilmesidir. Bu, Task ve Task<TResult>'in zaman uyumsuz bir önbelleğe alma altyapısında kullanılmak için son derece uygun olmasını sağlar. İşte, Task<TResult> temel alınarak oluşturulmuş küçük ama güçlü bir zaman uyumsuz önbellek örneği:
public class AsyncCache<TKey, TValue>
{
private readonly Func<TKey, Task<TValue>> _valueFactory;
private readonly ConcurrentDictionary<TKey, Lazy<Task<TValue>>> _map;
public AsyncCache(Func<TKey, Task<TValue>> valueFactory)
{
if (valueFactory == null) throw new ArgumentNullException("valueFactory");
_valueFactory = valueFactory;
_map = new ConcurrentDictionary<TKey, Lazy<Task<TValue>>>();
}
public Task<TValue> this[TKey key]
{
get
{
if (key == null) throw new ArgumentNullException("key");
return _map.GetOrAdd(key, toAdd =>
new Lazy<Task<TValue>>(() => _valueFactory(toAdd))).Value;
}
}
}
AsyncCache<TKey,TValue> sınıfı, oluşturucusuna bir TKey alan ve bir Task<TResult> döndüren işlevi temsilci olarak kabul eder. Önbellekten daha önce erişilen tüm değerler iç sözlükte depolanır ve AsyncCache önbellek eşzamanlı olarak erişiliyor olsa bile anahtar başına yalnızca bir görevin oluşturulmasını sağlar.
Örneğin, indirilen web sayfaları için bir önbellek oluşturabilirsiniz:
private AsyncCache<string,string> m_webPages =
new AsyncCache<string,string>(DownloadStringTaskAsync);
Daha sonra bir web sayfasının içeriğine ihtiyacınız olduğunda zaman uyumsuz yöntemlerle bu önbelleği kullanabilirsiniz. sınıfı mümkün AsyncCache olduğunca az sayfa indirdiğinizden emin olur ve sonuçları önbelleğe alır.
private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.IsEnabled = false;
try
{
txtContents.Text = await m_webPages["https://www.microsoft.com"];
}
finally { btnDownload.IsEnabled = true; }
}
AsenkronÜreticiTüketiciKoleksiyonu
Zaman uyumsuz etkinlikleri koordine etmek için veri yapıları oluşturmak için görevleri de kullanabilirsiniz. Klasik paralel tasarım desenlerinden birini düşünün: üretici/tüketici. Bu düzende üreticiler tüketiciler tarafından tüketilen verileri oluşturur ve üreticiler ve tüketiciler paralel olarak çalışabilir. Örneğin tüketici, daha önce 2. öğeyi üreten bir üretici tarafından oluşturulan 1. öğeyi işler. Üretici/tüketici düzeni için, tüketicilere yeni veriler bildirilmesi ve kullanılabilir olduğunda bulunabilmesi için üreticiler tarafından oluşturulan çalışmaları depolamak için sürekli olarak bazı veri yapısına ihtiyacınız vardır.
Aşağıda, zaman uyumsuz yöntemlerin üretici ve tüketici olarak kullanılmasını sağlayan, görevlerin üzerine kurulmuş basit bir veri yapısı bulunur:
public class AsyncProducerConsumerCollection<T>
{
private readonly Queue<T> m_collection = new Queue<T>();
private readonly Queue<TaskCompletionSource<T>> m_waiting =
new Queue<TaskCompletionSource<T>>();
public void Add(T item)
{
TaskCompletionSource<T> tcs = null;
lock (m_collection)
{
if (m_waiting.Count > 0) tcs = m_waiting.Dequeue();
else m_collection.Enqueue(item);
}
if (tcs != null) tcs.TrySetResult(item);
}
public Task<T> Take()
{
lock (m_collection)
{
if (m_collection.Count > 0)
{
return Task.FromResult(m_collection.Dequeue());
}
else
{
var tcs = new TaskCompletionSource<T>();
m_waiting.Enqueue(tcs);
return tcs.Task;
}
}
}
}
Bu veri yapısıyla aşağıdaki gibi bir kod yazabilirsiniz:
private static AsyncProducerConsumerCollection<int> m_data = …;
…
private static async Task ConsumerAsync()
{
while(true)
{
int nextItem = await m_data.Take();
ProcessNextItem(nextItem);
}
}
…
private static void Produce(int data)
{
m_data.Add(data);
}
Ad alanı System.Threading.Tasks.Dataflow türünü içerir BufferBlock<T>, benzer şekilde kullanabilir, ancak özel bir koleksiyon türü oluşturmak zorunda kalmadan.
private static BufferBlock<int> m_data = …;
…
private static async Task ConsumerAsync()
{
while(true)
{
int nextItem = await m_data.ReceiveAsync();
ProcessNextItem(nextItem);
}
}
…
private static void Produce(int data)
{
m_data.Post(data);
}
Uyarı
Ad alanı System.Threading.Tasks.Dataflow bir NuGet paketi olarak mevcuttur. Ad alanını içeren System.Threading.Tasks.Dataflow derlemeyi yüklemek için projenizi Visual Studio'da açın, Proje menüsünden NuGet Paketlerini Yönet'i seçin ve çevrimiçi ortamda System.Threading.Tasks.Dataflow paketi arayın.