Neden Görev'in devamları.Ne zaman senkronize olarak yürütülür?


14

Task.WhenAll.NET Core 3.0'da çalışırken yöntemle ilgili meraklı bir gözlem yaptım . Basit bir Task.Delaygörevi tek bir argüman olarak Task.WhenAllgeçtim ve sarılmış görevin orijinal göreve aynı şekilde davranmasını bekledim. Ancak durum böyle değil. Orijinal görevin devamları eşzamansız olarak (istenir) Task.WhenAll(task)yürütülür ve çoklu sarmalayıcıların süreleri birbiri ardına eşzamanlı olarak yürütülür (bu istenmeyen bir durumdur).

İşte bu davranışın bir demosu . Dört işçi görevi aynı Task.Delaygörevi tamamlamak için bekliyor ve sonra ağır bir hesaplama ile devam ediyor (a ile simüle edilmiş Thread.Sleep).

var task = Task.Delay(500);
var workers = Enumerable.Range(1, 4).Select(async x =>
{
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
        $" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} before await");

    await task;
    //await Task.WhenAll(task);

    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
        $" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} after await");

    Thread.Sleep(1000); // Simulate some heavy CPU-bound computation
}).ToArray();
Task.WaitAll(workers);

İşte çıktı. Dört devam, farklı iş parçacıklarında (paralel olarak) beklendiği gibi çalışıyor.

05:23:25.511 [1] Worker1 before await
05:23:25.542 [1] Worker2 before await
05:23:25.543 [1] Worker3 before await
05:23:25.543 [1] Worker4 before await
05:23:25.610 [4] Worker1 after await
05:23:25.610 [7] Worker2 after await
05:23:25.610 [6] Worker3 after await
05:23:25.610 [5] Worker4 after await

Şimdi çizgiye yorum yapar await taskve aşağıdaki satırı kaldırırsam await Task.WhenAll(task), çıktı oldukça farklıdır. Tüm devamlar aynı iş parçacığında çalışıyor, bu nedenle hesaplamalar paralel değil. Her hesaplama bir öncekinin tamamlanmasından sonra başlar:

05:23:46.550 [1] Worker1 before await
05:23:46.575 [1] Worker2 before await
05:23:46.576 [1] Worker3 before await
05:23:46.576 [1] Worker4 before await
05:23:46.645 [4] Worker1 after await
05:23:47.648 [4] Worker2 after await
05:23:48.650 [4] Worker3 after await
05:23:49.651 [4] Worker4 after await

Şaşırtıcı bir şekilde, bu sadece her işçi farklı bir ambalaj beklediğinde olur. Sarıcıyı önceden tanımlarsam:

var task = Task.WhenAll(Task.Delay(500));

... ve sonra awaittüm işçilerin içindeki aynı görev, davranış ilk durumla aynıdır (eşzamansız devamlar).

Sorum şu: bu neden oluyor? Aynı görevin farklı sarmalayıcılarının devamlarının aynı iş parçacığında, eşzamanlı olarak yürütülmesine ne sebep olur?

Not: bir görevi aynı garip davranışla sonuçlamak Task.WhenAnyyerine kaydırmak Task.WhenAll.

Başka bir gözlem: Sargının bir içine sarılmasının Task.Runsüreksizliği asenkronize etmesini bekledim . Ama olmuyor. Aşağıdaki satırın devamları hala aynı iş parçacığında (eşzamanlı olarak) yürütülür.

await Task.Run(async () => await Task.WhenAll(task));

Açıklama: Yukarıdaki farklılıklar, .NET Core 3.0 platformunda çalışan bir Konsol uygulamasında gözlemlenmiştir. .NET Framework 4.8'de, orijinal görevi veya görev sarmalayıcıyı beklemek arasında bir fark yoktur. Her iki durumda da, devamlar aynı iş parçacığında senkronize olarak yürütülür.


sadece merak ediyorum, ne olacak await Task.WhenAll(new[] { task });?
vasily.sib

1
Sanırım içindeki kısa devre yüzündenTask.WhenAll
Michael Randall

3
LinqPad her iki varyant için de aynı beklenen ikinci çıktıyı verir ... Paralel çalışma elde etmek için hangi ortamı kullanıyorsunuz (konsol vs. WinForms vs ..., .NET vs. Core, ..., framework sürümü)?
Alexei Levenkov

1
Ben .NET Çekirdek 3.0 ve 3.1 bu davranışı çoğaltmak mümkün, ama sadece ilk değiştirdikten sonra Task.Delaygelen 100için 1000o zaman tamamlanmaz böylece awaited.
Stephen Cleary

2
@MaviStrat güzel bulmak! Kesinlikle bir şekilde ilişkili olabilir. İlginçtir , .NET kodlarının .NET Framework 4.6, 4.6.1, 4.7.1, 4.7.2 ve 4.8'deki Microsoft kodunun hatalı davranışını yeniden oluşturamadım . Her seferinde farklı iş parçacığı kimlikleri doğru davranış. İşte 4.7.2 üzerinde çalışan bir keman.
Theodor Zoulias

Yanıtlar:


2

Böylece aynı görev değişkenini bekleyen birden fazla zaman uyumsuz yönteminiz var;

    await task;
    // CPU heavy operation

Evet, bu devamlar tasktamamlandığında seri olarak çağrılır . Örneğinizde, her devam daha sonra iş parçacığını bir sonraki saniye için hong eder.

Her devamın eşzamansız olarak çalışmasını istiyorsanız, aşağıdaki gibi bir şeye ihtiyacınız olabilir;

    await task;
    await Task.Yield().ConfigureAwait(false);
    // CPU heavy operation

Böylece görevleriniz başlangıç ​​süresinden geri döner ve CPU yükünün SynchronizationContext.


Cevap için teşekkürler Jeremy. Evet, Task.Yieldsorunum için iyi bir çözüm. Benim sorum, bunun neden olduğu hakkında daha fazla ve istenen davranışı nasıl zorlayacağımız hakkında daha az.
Theodor Zoulias

Gerçekten bilmek istiyorsanız, kaynak kodu burada; github.com/microsoft/referencesource/blob/master/mscorlib/…
Jeremy Lakeman

İlgili sınıfların kaynak kodlarını inceleyerek sorumun cevabını elde etmenin bu kadar basit olmasını diliyorum. Kodu anlamak ve neler olup bittiğini anlamak benim yaşımı alacaktı!
Theodor Zoulias

Anahtar kaçınarak SynchronizationContextçağıran ConfigureAwait(false)yeterli olabilir orijinal görev kez.
Jeremy Lakeman

Bu bir konsol uygulamasıdır ve SynchronizationContext.Currentboştur. Ama emin olmak için kontrol ettim. Eklediğim ConfigureAwait(false)içinde awaithat ve hiçbir fark yarattı. Gözlemler öncekiyle aynıdır.
Theodor Zoulias

1

Bir görev kullanılarak oluşturulduğunda Task.Delay(), oluşturma seçenekleri Noneyerine olarak ayarlanır RunContinuationsAsychronously.

Bu, .net çerçevesi ve .net çekirdeği arasındaki değişikliği bozuyor olabilir. Ne olursa olsun, gözlemlediğiniz davranışı açıklıyor gibi görünüyor. Ayrıca kaynak koduna kazma bu doğrulayabilir Task.Delay()olduğu kadar newing bir DelayPromisevarsayılan çağıran Taskbelirtilmedi oluşturma seçenekleri bırakarak yapıcı.


Cevabınız için teşekkürler Tanveer. Yani .NET Core'da yeni bir nesne oluştururken RunContinuationsAsychronouslybunun varsayılan haline Nonegeldiğini düşünüyor Taskmusunuz? Bu benim gözlemlerimin bazılarını açıklar ama hepsini değil. Özellikle aynı Task.WhenAllambalajı beklemek ile farklı ambalajları beklemek arasındaki farkı açıklamaz .
Theodor Zoulias

0

Kodunuzda, aşağıdaki kod yinelenen gövdeden çıkar.

var task = Task.Delay(100);

böylece her çalıştırdığınızda, görev beklenir ve ayrı bir iş parçacığında çalıştırılır

await task;

ancak aşağıdakileri çalıştırırsanız, durumunu kontrol eder task, bu yüzden tek bir iş parçacığında çalıştırır

await Task.WhenAll(task);

ancak görev oluşturmayı yanına taşırsanız WhenAll, her görevi ayrı bir iş parçacığında çalıştırabilirsiniz.

var task = Task.Delay(100);
await Task.WhenAll(task);

Cevabınız için teşekkürler Seyedraouf. Açıklamanız bana pek tatmin edici gelmiyor. Tarafından döndürülen görev Task.WhenAllsadece düzenli Taskorijinali gibi task. Her iki görev de bir noktada, bir zamanlayıcı olayının sonucu olarak orijinal ve orijinal görevin tamamlanması sonucunda bileşik olarak tamamlanmaktadır. Devamları neden farklı davranışlar sergilemeli? Bir görev hangi açıdan diğerinden farklıdır?
Theodor Zoulias
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.