Bu eşzamansız eylem neden askıda kalıyor?


103

Çok katmanlı bir .Net 4.5 uygulamam var asyncve C # 'ın yeni ve awaitkilitlenen anahtar sözcüklerini kullanarak bir yöntemi çağırıyor ve nedenini göremiyorum.

En altta, veritabanı yardımcı programımızı genişleten bir zaman uyumsuz yöntemim var OurDBConn(temelde temel DBConnectionve DBCommandnesneler için bir sarmalayıcı ):

public static async Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    T result = await Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });

    return result;
}

Ardından, yavaş çalışan toplamları almak için bunu çağıran orta düzey bir zaman uyumsuz yöntemim var:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var result = await this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));

    return result;
}

Son olarak, senkronize olarak çalışan bir UI yöntemim (bir MVC eylemi) var:

Task<ResultClass> asyncTask = midLevelClass.GetTotalAsync(...);

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;

Sorun şu ki, sonsuza dek o son satırda asılı kalması Ararsam da aynı şeyi yapar asyncTask.Wait(). Yavaş SQL yöntemini doğrudan çalıştırırsam yaklaşık 4 saniye sürer.

Beklediğim davranış şudur ki asyncTask.Result, eğer bitmemişse, bitene kadar beklemesi ve bir kez olduğunda sonucu geri vermesi gerekir.

Bir hata ayıklayıcı ile adım atarsam, SQL ifadesi tamamlanır ve lambda işlevi biter, ancak return result;satırına GetTotalAsyncasla ulaşılmaz.

Neyi yanlış yaptığım hakkında bir fikrin var mı?

Bunu düzeltmek için araştırmam gereken yerlere dair herhangi bir öneriniz var mı?

Bu bir yerlerde bir çıkmaz olabilir mi ve öyleyse, onu bulmanın doğrudan bir yolu var mı?

Yanıtlar:


150

Evet, bu bir çıkmaz. Ve TPL ile ilgili yaygın bir hata, bu yüzden kendinizi kötü hissetmeyin.

Yazdığınızda await foo, çalışma zamanı varsayılan olarak, yöntemin başladığı aynı SynchronizationContext üzerinde işlevin devamını zamanlar. İngilizce olarak, diyelim ki sizi ExecuteAsyncUI iş parçacığından aradınız. Sorgunuz iş parçacığı iş parçacığı üzerinde çalışır (çağırdığınız için Task.Run), ancak daha sonra sonucu beklersiniz. Bu, çalışma zamanının " return result;" satırınızı iş parçacığı havuzuna geri planlamak yerine UI iş parçacığında çalışacak şekilde zamanlayacağı anlamına gelir .

Peki bu çıkmaz nasıl kilitleniyor? Şu koda sahip olduğunuzu hayal edin:

var task = dataSource.ExecuteAsync(_ => 42);
var result = task.Result;

Böylece ilk satır eşzamansız çalışmaya başlıyor. İkinci satır daha sonra UI iş parçacığını engeller . Dolayısıyla, çalışma zamanı "dönüş sonucu" satırını UI iş parçacığında geri çalıştırmak istediğinde, Resulttamamlanana kadar bunu yapamaz . Ama tabii ki Sonuç, dönüş gerçekleşene kadar verilemez. Kilitlenme.

Bu, TPL'yi kullanmanın temel bir kuralını gösterir: .ResultBir UI iş parçacığında (veya başka bir fantezi eşitleme bağlamında) kullandığınızda, Görev'in bağlı olduğu hiçbir şeyin UI iş parçacığına programlanmamasına dikkat etmeniz gerekir. Yoksa kötülük olur.

Ee ne yapıyorsun? Seçenek 1, kullanım her yerde beklemektir, ancak sizin de söylediğiniz gibi bu zaten bir seçenek değil. Size sunulan ikinci seçenek, sadece await'i kullanmayı bırakmaktır. İki işlevinizi şu şekilde yeniden yazabilirsiniz:

public static Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    return Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });
}

public static Task<ResultClass> GetTotalAsync( ... )
{
    return this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));
}

Fark ne? Artık hiçbir yerde bekleme yok, bu nedenle UI iş parçacığına dolaylı olarak hiçbir şey zamanlanmıyor. Tek bir dönüşü olan bu gibi basit yöntemler için, bir var result = await...; return resultkalıp " " yapmanın bir anlamı yoktur ; yalnızca zaman uyumsuz değiştiriciyi kaldırın ve görev nesnesini doğrudan iletin. Hiçbir şey değilse, daha az genel gider.

Seçenek # 3, beklemelerinizin UI iş parçacığına geri planlanmasını istemediğinizi, yalnızca iş parçacığı havuzuna programlanmasını istemediğinizi belirtmektir. Bunu şu ConfigureAwaityöntemle yaparsınız :

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var resultTask = this.DBConnection.ExecuteAsync<ResultClass>(
        ds => return ds.Execute("select slow running data into result");

    return await resultTask.ConfigureAwait(false);
}

Normalde bir görevi beklemek, üzerinde iseniz UI iş parçacığı için zamanlanır; sonucunu beklemek, ContinueAwaithangi bağlamda olursanız olun yok sayar ve her zaman iş parçacığı havuzuna programlanır. Bunun dezavantajı, sonucunuzun bağlı olduğu tüm işlevlerde bunu her yere serpmeniz gerektiğidir, çünkü kaçırılan herhangi .ConfigureAwaitbir kilitlenme başka bir kilitlenmenin nedeni olabilir.


6
BTW, soru ASP.NET ile ilgili, dolayısıyla UI iş parçacığı yok. Ancak, ASP.NET nedeniyle kilitlenmelerle ilgili sorun tamamen aynıdır SynchronizationContext.
svick

Bu çok şey açıkladı, çünkü problemi olmayan ama TPL'yi async/ awaitanahtar sözcükleri olmadan kullanan benzer .Net 4 koduna sahiptim .
Keith


VB.net kodunu arayan biri varsa (benim gibi), burada açıklanmıştır: docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/…
MichaelDarkBlue


36

Bu, blogumda anlattığım gibi klasik karma asynckilitlenme senaryosudur . Jason bunu iyi açıkladı: Varsayılan olarak, her seferinde bir "bağlam" kaydedilir ve yönteme devam etmek için kullanılır. Bu "bağlam" o olmadığı sürece geçerli , bu durumda geçerli olanıdır . Tüm yöntem denemeleri devam etmek için, ilk olarak yeniden girer (bu durumda, bir ASP.NET çekilen "içerik" ). ASP.NET , bağlamda yalnızca bir iş parçacığına izin verir ve bağlamda zaten bir iş parçacığı vardır - iş parçacığı üzerinde engellendi .awaitasyncSynchronizationContextnullTaskSchedulerasyncSynchronizationContextSynchronizationContextTask.Result

Bu çıkmazdan kaçınacak iki kural vardır:

  1. Tamamen kullanın async. Bunu "yapamayacağından" bahsediyorsun, ama neden olmasın emin değilim. NET 4.5 üzerinde ASP.NET MVC kesinlikle asynceylemleri destekleyebilir ve yapılması zor bir değişiklik değildir.
  2. ConfigureAwait(continueOnCapturedContext: false)Mümkün olduğunca çok kullanın . Bu, yakalanan bağlamda devam etme varsayılan davranışını geçersiz kılar.

ConfigureAwait(false)Mevcut işlevin farklı bir bağlamda devam edeceğini garanti ediyor mu ?
chue x

MVC çerçevesi bunu destekler, ancak bu, çok sayıda istemci tarafı JS'nin zaten mevcut olduğu mevcut bir MVC uygulamasının bir parçasıdır. asyncİstemci tarafının çalışma şeklini bozmadan bir eyleme kolayca geçemiyorum . Kesinlikle bu seçeneği daha uzun vadede araştırmayı planlıyorum.
Keith

Sadece yorumumu açıklığa kavuşturmak için - ConfigureAwait(false)çağrı ağacını kullanmanın OP'nin problemini çözüp çözmeyeceğini merak ediyordum .
chue x

3
@Keith: Bir MVC eylemi yapmak async, istemci tarafını hiç etkilemez. Bunu başka bir blog yazısında açıklıyorum async, HTTP Protokolünü Değiştirmez .
Stephen Cleary

1
@Keith: asyncKod tabanı aracılığıyla "büyümek" normaldir . Denetleyiciniz yöntemi asenkron işlemleri bağlı olabilir, o zaman temel sınıf yöntemi olmalıdır dönmek Task<ActionResult>. Büyük bir projeye geçiş asyncyapmak her zaman gariptir çünkü asynckodu karıştırmak ve senkronize etmek zor ve zordur. Saf asynckod çok daha basittir.
Stephen Cleary

12

Aynı kilitlenme durumundaydım, ancak benim durumumda bir senkronizasyon yönteminden bir eşzamansız yöntemi çağırmak benim için işe yarayan şuydu:

private static SiteMetadataCacheItem GetCachedItem()
{
      TenantService TS = new TenantService(); // my service datacontext
      var CachedItem = Task.Run(async ()=> 
               await TS.GetTenantDataAsync(TenantIdValue)
      ).Result; // dont deadlock anymore
}

bu iyi bir yaklaşım mı, herhangi bir fikrin var mı?


Bu çözüm benim için de işe yarıyor, ancak bunun iyi bir çözüm olduğundan veya bir yerde bozulabileceğinden emin değilim. Bunu kimse açıklayabilir
Konstantin Vdovkin

nihayet bu çözüme gittim ve verimli bir ortamda sorunsuz çalışıyor .....
Danilow

1
Task.Run kullanarak performans konusunda bir vuruş yaptığınızı düşünüyorum. Task.Run testimde, 100 ms'lik bir http isteği için yürütme süresini neredeyse iki katına çıkarıyor.
Timothy Gonzalez

1
bu mantıklı, eşzamansız bir aramayı
tamamlamak

Fantastik bu benim için de çalıştı, benim durumum da eşzamansız olanı çağıran eşzamanlı bir yöntemden kaynaklanıyordu. Teşekkür ederim!
Leonardo Spina

4

Sadece kabul cevap (yorumuna yeterli rep) eklemek, kullanıyorum engellerken bu sorun ortaya vardı task.Resulther rağmen olay awaitaltındaki vardı ConfigureAwait(false)bu örnekte olduğu gibi,:

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

Sorun aslında harici kitaplık koduyla ilgiliydi. Zaman uyumsuz kitaplık yöntemi, beklemeyi nasıl yapılandırdığım önemli değil, kilitlenmeye neden olan çağıran eşitleme bağlamında devam etmeye çalıştı.

Bu nedenle, cevap, harici kitaplık kodunun kendi versiyonumu ExternalLibraryStringAsync, istenen devam özelliklerine sahip olacak şekilde döndürmekti .


tarihsel amaçlar için yanlış cevap

Çok fazla acı ve ıstıraptan sonra , bu blog yazısında gömülü çözümü buldum ('kilitlenme' için Ctrl-f). task.ContinueWithÇıplak yerine kullanmak etrafında döner task.Result.

Daha önce kilitlenme örneği:

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

Çıkmazdan şu şekilde kaçının:

public Foo GetFooSynchronous
{
    var foo = new Foo();
    GetInfoAsync()  // ContinueWith doesn't run until the task is complete
        .ContinueWith(task => foo.Info = task.Result);
    return foo;
}

private async Task<string> GetInfoAsync
{
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

Olumsuz oy ne için? Bu çözüm benim için çalışıyor.
Cameron Jeffers

Nesneyi Tasktamamlanmadan önce geri gönderiyorsunuz ve arayan kişiye, döndürülen nesnenin mutasyonunun gerçekte ne zaman gerçekleştiğini belirleme imkanı sunmuyorsunuz.
Servy

hmm evet anlıyorum. Öyleyse, elle engelleme sırasında döngüsü (veya buna benzer bir şey) kullanan bir tür "görev tamamlanana kadar bekle" yöntemini göstermeli miyim? Ya da böyle bir bloğu GetFooSynchronousyönteme sığdırmak ?
Cameron Jeffers

1
Eğer yaparsanız, kilitlenecektir. TaskEngelleme yerine a döndürerek eşzamansız hale getirmeniz gerekir .
Servy

Ne yazık ki bu bir seçenek değil, sınıf değiştiremeyeceğim zaman uyumlu bir arayüz uyguluyor.
Cameron Jeffers

0

hızlı cevap: bu satırı değiştir

ResultClass slowTotal = asyncTask.Result;

-e

ResultClass slowTotal = await asyncTask;

neden? Konsol uygulamaları haricindeki çoğu uygulamadaki görevlerin sonucunu almak için .result kullanmamalısınız, bunu yaparsanız programınız oraya geldiğinde kilitlenir

Ayrıca kullanmak istiyorsanız aşağıdaki kodu deneyebilirsiniz.

ResultClass slowTotal = Task.Run(async ()=>await asyncTask).Result;
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.