Bir iş parçacığı while döngüsü içinde bir görev beklediğinde tam olarak ne olur?


10

Bir süre C # 'in async / await pattern ile uğraştıktan sonra, aniden şu kodda ne olduğunu açıklayacağımı gerçekten bilmediğimi fark ettim:

async void MyThread()
{
    while (!_quit)
    {
        await GetWorkAsync();
    }
}

GetWorkAsync()Taskdevam edildiğinde bir iplik anahtarına neden olabilecek veya olmayabilecek bir beklenen değeri döndüreceği varsayılmaktadır .

Bekleyen bir döngü içinde olmasaydı kafam karışmazdı. Doğal olarak yöntemin geri kalanının (yani devam) potansiyel olarak başka bir iş parçacığında çalışmasını beklerim.

Ancak, bir döngü içinde, "yöntemin geri kalanı" kavramı bana biraz sisli oluyor.

İş parçacığı devam ettirilirse açık değilse, açık değilse "döngünün geri kalanı" ne olur? Döngünün sonraki yinelemesi hangi iş parçacığında yürütülür?

Gözlemlerim, devamlamanın başka bir işlemde yürütülürken her yinelemenin aynı iş parçacığında (orijinal olan) başladığını (kesin olarak doğrulanmadı) gösteriyor. Bu gerçekten olabilir mi? Evetse, bu GetWorkAsync yönteminin iş parçacığı güvenliği açısından dikkate alınması gereken bir derece beklenmedik paralellik midir?

GÜNCELLEME: Sorum, bazılarının önerdiği gibi kopya değil. while (!_quit) { ... }Kod kalıbı yalnız benim gerçek kod basitleştirilmiş bir şeklidir. Gerçekte, iş parçacığım girdi iş kuyruğunu düzenli aralıklarla (varsayılan olarak her 5 saniyede bir) işleyen uzun ömürlü bir döngüdür. Gerçek çıkış koşulu kontrolü, örnek kodun önerdiği gibi basit bir saha kontrolü değil, bir olay tutma kontrolüdür.



1
Ayrıca bkz. Verim ve denetim akışını .NET'te nasıl uygular? bunların nasıl birbirine bağlandığına dair harika bilgiler için.
John Wu

@John Wu: Bu SO dizisini henüz görmedim. Orada ilginç bilgi nuggets bir sürü. Teşekkürler!
aoven

Yanıtlar:


6

Aslında Try Roslyn'de kontrol edebilirsiniz . Await yönteminiz void IAsyncStateMachine.MoveNext()oluşturulan asenkron sınıfında yeniden yazılır .

Göreceğiniz şey şudur:

            if (this.state != 0)
                goto label_2;
            //set up the state machine here
            label_1:
            taskAwaiter.GetResult();
            taskAwaiter = default(TaskAwaiter);
            label_2:
            if (!OuterClass._quit)
            {
               taskAwaiter = GetWorkAsync().GetAwaiter();
               //state machine stuff here
            }
            goto label_1;

Temel olarak, hangi iş parçacığında olduğunuz önemli değildir; durum makinesi, döngünüzü eşdeğer bir if / goto yapısıyla değiştirerek düzgün bir şekilde çalışmaya devam edebilir.

Söyledikten sonra, zaman uyumsuz yöntemler farklı bir iş parçacığı üzerinde yürütmek gerekmez. Eric Lippert'in sadece bir iş parçacığında nasıl çalışabileceğinizi açıklamak için "Bu sihir değil" açıklamasına bakın async/await.


2
Derleyicinin zaman uyumsuz kodumda yaptığı yeniden yazma boyutunu küçümsüyor gibiyim. Aslında, yeniden yazımdan sonra bir "döngü" yoktur! Benim için eksik olan kısım buydu. Müthiş ve 'Roslyn deneyin' bağlantısı için teşekkür ederiz!
oda

GOTO orijinal döngü yapısıdır. Bunu unutmayalım.

2

İlk olarak, Servy benzer bir sorunun cevabında bazı kodlar yazdı ve bu cevabın dayandığı:

/programming/22049339/how-to-create-a-cancellable-task-loop

Servy'nin yanıtı ContinueWith(), asyncve awaitanahtar sözcüklerini açıkça kullanmadan TPL yapılarını kullanan benzer bir döngü içerir ; sorunuzu cevaplamak için, döngünüzün kilidi açılırken kodunuzun nasıl görünebileceğini düşününContinueWith()

    private static Task GetWorkWhileNotQuit()
    {
        var tcs = new TaskCompletionSource<bool>();

        Task previous = Task.FromResult(_quit);
        Action<Task> continuation = null;
        continuation = t =>
        {
            if (!_quit)
            {
                previous = previous.ContinueWith(_ => GetWorkAsync())
                    .Unwrap()
                    .ContinueWith(_ => previous.ContinueWith(continuation));
            }
            else
            {
                tcs.SetResult(_quit);
            }
        };
        previous.ContinueWith(continuation);
        return tcs.Task;
    }

Bu, kafanızı sarmak için biraz zaman alır, ancak özet olarak:

  • continuation"mevcut yineleme" için bir kapanışı temsil eder
  • previoustemsil Taskdurumunu içeren "bir önceki iterasyondan bu" ( 'yineleme' bitmiş ve bir sonraki başlatma kullanıldığında bildiği yani ..)
  • A GetWorkAsync()döndürdüğünü varsayarsak Task, bu , 'iç görev'i (yani asıl sonucu ) alma çağrısını ContinueWith(_ => GetWorkAsync())döndürecektir .Task<Task>Unwrap()GetWorkAsync()

Yani:

  1. Başlangıçta daha önceki bir yineleme yoktur , bu yüzden ona sadece bir değer atanır Task.FromResult(_quit) - durumu olarak başlar Task.Completed == true.
  2. İlk continuationkullanımdaprevious.ContinueWith(continuation)
  3. continuationkapatma güncellemeler previoustamamlanması durumunu yansıtacak_ => GetWorkAsync()
  4. Ne zaman _ => GetWorkAsync()tamamlanır, bu "ile devam ediyor" _previous.ContinueWith(continuation)- yani çağırarak continuationtekrar lambda
    • Açıkçası bu noktada, previousdurumu ile güncellendi, _ => GetWorkAsync()böylece geri döndüğünde continuationlambda çağrılır GetWorkAsync().

continuationLambda hep durumunu kontrol eder _quit, eğer, bu yüzden _quit == falseo artık continuations vardır ve TaskCompletionSourcedeğerine ayarlanır alır _quitve her şey tamamlanır.

Farklı bir iş parçacığında yürütülen devam ile ilgili gözleminize gelince , bu blogda "Görevler (hala) iş parçacığı değil ve zaman uyumsuz değildir" gibi , async/ awaitanahtar kelimenin sizin için yapacağı bir şey değildir . - https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/

GetWorkAsync()İplik geçirme ve iplik güvenliği açısından yönteminize daha yakından bakmaya değer olduğunu öneririm . Tanınız tekrarlanan zaman uyumsuzluk / bekleme kodunuzun bir sonucu olarak farklı bir iş parçacığında yürütüldüğünü açıklıyorsa, bu yöntemle ilgili veya bu yöntemle ilgili bir şey başka bir yerde yeni bir iş parçacığının oluşturulmasına neden oluyor olmalıdır. (Bu beklenmedikse, belki bir .ConfigureAwaityerlerde var mı?)


2
Gösterdiğim kod (çok) basitleştirilmiş. GetWorkAsync () içinde birden fazla beklemek var. Bazıları veritabanına ve ağa erişir, bu da gerçek G / Ç anlamına gelir. Anladığım kadarıyla, iş parçacığı anahtarı bu tür beklemenin doğal bir sonucudur (zorunlu olmasa da), çünkü ilk iş parçacığı, devamların nerede yürütüleceğini belirleyecek herhangi bir eşitleme bağlamı oluşturmaz. Böylece bir iş parçacığı havuzu iş parçacığı üzerinde yürütmek. Akıl yürütmem yanlış mı?
aoven

@aoven İyi nokta - Ben farklı tür dikkate almadım SynchronizationContext- bu kesinlikle önemlidir çünkü .ContinueWith()devam gönderme için SynchronizationContext kullanır; gerçekten awaitbir ThreadPool iş parçacığı veya ASP.NET iş parçacığında çağrılırsa gördüğünüz davranış açıklar . Bu durumlarda bir devam kesinlikle farklı bir iş parçacığına gönderilebilir. Diğer yandan, awaitWPF Dispatcher veya Winforms bağlamı gibi tek iş parçacıklı bir bağlamın çağrılması , devam etmenin orijinalde gerçekleşmesini sağlamak için yeterli olmalıdır. iplik
Ben Cottrell
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.