Eşzamansız / bekleyen kilitlenmeleri nasıl teşhis edebilirim?


24

Async / await'i yoğun şekilde kullanan yeni bir kod tabanı ile çalışıyorum. Benim takımımdaki insanların çoğu async / bekliyor için de oldukça yeni. Genel olarak Microsoft tarafından Belirtildiği şekilde En İyi Uygulamalara sahip olma eğilimindeyiz , ancak genel olarak eşzamansız çağrıdan geçebilmek için bağlamımıza ihtiyacımız var ve çalışmayan kütüphanelerle çalışıyoruz ConfigureAwait(false).

Bunların hepsini birleştirin ve makalede açıklanan asenkron kilitlenmelerle karşılaşıyoruz ... haftalık. Ünite testi sırasında görünmüyorlar, çünkü alay konusu veri kaynaklarımız (genellikle üzerinden Task.FromResult) kilitlenmeyi tetiklemek için yeterli değil. Böylece çalışma zamanı veya entegrasyon testleri sırasında, bazı servis çağrıları sadece öğle yemeğine çıkıyor ve asla geri dönmüyor. Bu, sunucuları öldürür ve genellikle bir şeyler karışıklığa neden olur.

Sorun, hatanın nerede yapıldığını takip etmenin (genellikle sadece async olmamak) takip etmesinin genellikle zaman alan ve otomatik hale getirilemeyen manuel kod incelemesini içermesidir.

Kilitlenmeye neyin neden olduğunu teşhis etmenin daha iyi bir yolu nedir?


1
İyi soru; Bunu kendim merak ettim. Bu adamın asyncmakale koleksiyonunu okudun mu?
Robert Harvey

@RobertHarvey - belki hepsi değil, ama bazılarını okudum. Daha fazla "Bu iki / üç şeyi her yerde yaptığınızdan emin olun, aksi halde kodunuz çalışma zamanında korkunç bir ölümle ölür".
Telastyn

Zaman uyumsuzluğunu düşürmeye veya kullanımını en faydalı noktalara düşürmeye açık mısınız? Asenkron IO hepsi ya da hiçbiri değildir.
usr

1
Kilitlenmeyi yeniden oluşturabilirseniz, engelleme çağrısını görmek için yığın izine bakamaz mısınız?
svick

2
Sorun "tümüyle eşzamansız değil" ise, bu, kilitlenmenin bir yarısının geleneksel bir kilitlenme olduğu ve senkronizasyon bağlamı iş parçacığının yığın izinde görünmesi gerektiği anlamına gelir.
svick

Yanıtlar:


4

Tamam - Aşağıdakilerin size herhangi bir yardımı olup olmayacağından emin değilim, çünkü sizin durumunuzda doğru olan veya olmayan bir çözüm geliştirme konusunda bazı varsayımlar yaptım. Belki benim "çözümüm" çok teorik ve sadece yapay örnekler için çalışıyor - aşağıdakilerin ötesinde herhangi bir test yapmadım.
Ek olarak, aşağıdakileri gerçek bir çözümden çok daha fazla geçici bir çözüm olarak görecektim, ancak yanıtların eksikliğini düşünerek hala hiçbir şeyden daha iyi olabileceğini düşünüyorum (sorunuzu bir çözüm beklerken izlemeye devam ettim, ancak birisinin yayınlandığını görmeden oynamaya başladım. sorunu etrafında).

Ancak yeterli sayıda şey söylendi: Diyelim ki bir tamsayı almak için kullanılabilecek basit bir veri servisimiz var:

public interface IDataService
{
    Task<int> LoadMagicInteger();
}

Basit bir uygulama asenkron kod kullanır:

public sealed class CustomDataService
    : IDataService
{
    public async Task<int> LoadMagicInteger()
    {
        Console.WriteLine("LoadMagicInteger - 1");
        await Task.Delay(100);
        Console.WriteLine("LoadMagicInteger - 2");
        var result = 42;
        Console.WriteLine("LoadMagicInteger - 3");
        await Task.Delay(100);
        Console.WriteLine("LoadMagicInteger - 4");
        return result;
    }
}

Şimdi, bu sınıfın gösterdiği gibi "yanlış" kodunu kullanıyorsak, bir sorun ortaya çıkıyor. sonucu gibi yapmak yerine Fooyanlış erişir :Task.ResultawaitBar

public sealed class ClassToTest
{
    private readonly IDataService _dataService;

    public ClassToTest(IDataService dataService)
    {
        this._dataService = dataService;
    }

    public async Task<int> Foo()
    {
        var result = this._dataService.LoadMagicInteger().Result;
        return result;
    }
    public async Task<int> Bar()
    {
        var result = await this._dataService.LoadMagicInteger();
        return result;
    }
}

Şimdi (size) ihtiyacımız olan şey, arama yaparken başarılı olan Barancak arama yaparken başarısız olan bir sınama yazmaktır Foo(en azından soruyu doğru anladıysam ;-)).

Kodun konuşmasına izin vereceğim; İşte ne buldum (Visual Studio testleri kullanarak, ama NUnit kullanarak da çalışması gerekir):

DataServiceMockkullanır TaskCompletionSource<T>. Bu, sonucu test çalışmasında tanımlanmış bir noktaya koymamızı sağlar ve bu da aşağıdaki teste neden olur. TaskCompletionSource'u teste geri döndürmek için bir temsilci kullandığımızı unutmayın. Bunu, testin Başlat yöntemine ve kullanım özelliklerini de koyabilirsiniz.

TaskCompletionSource<int> tcs = null;
this._dataService.LoadMagicIntegerMock = t => tcs = t;

Task<int> task = null;
TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Foo());

tcs.TrySetResult(42);

var result = task.Result;
Assert.AreEqual(42, result);

this._end = true;

Burada olan, ilk önce yöntemi engellemeden bırakabileceğimizi doğrulamamızdır (bu, eğer biri erişirse işe yaramaz Task.Result- bu durumda, görev sonucu yöntem geri dönene kadar müsait olmadığından zaman aşımına uğrarız) ).
Ardından sonucu belirledik (şimdi yöntem çalışabilir) ve sonucu doğrularız (bir birim testinde Task.Result'a erişebiliriz, aslında engellemenin gerçekleşmesini istiyoruz ).

Komple test sınıfı - istendiğinde BarTestbaşarılı ve FooTestbaşarısız.

[TestClass]
public class UnitTest1
{
    private DataServiceMock _dataService;
    private ClassToTest _instance;
    private bool _end;

    [TestInitialize]
    public void Initialize()
    {
        this._dataService = new DataServiceMock();
        this._instance = new ClassToTest(this._dataService);

        this._end = false;
    }
    [TestCleanup]
    public void Cleanup()
    {
        Assert.IsTrue(this._end);
    }

    [TestMethod]
    public void FooTest()
    {
        TaskCompletionSource<int> tcs = null;
        this._dataService.LoadMagicIntegerMock = t => tcs = t;

        Task<int> task = null;
        TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Foo());

        tcs.TrySetResult(42);

        var result = task.Result;
        Assert.AreEqual(42, result);

        this._end = true;
    }
    [TestMethod]
    public void BarTest()
    {
        TaskCompletionSource<int> tcs = null;
        this._dataService.LoadMagicIntegerMock = t => tcs = t;

        Task<int> task = null;
        TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Bar());

        tcs.TrySetResult(42);

        var result = task.Result;
        Assert.AreEqual(42, result);

        this._end = true;
    }
}

Ve kilitlenmeleri / zaman aşımlarını test etmek için küçük bir yardımcı sınıf:

public static class TaskTestHelper
{
    public static void AssertDoesNotBlock(Action action, int timeout = 1000)
    {
        var timeoutTask = Task.Delay(timeout);
        var task = Task.Factory.StartNew(action);

        Task.WaitAny(timeoutTask, task);

        Assert.IsTrue(task.IsCompleted);
    }
}

Güzel cevap Biraz zamanım varken kodunuzu kendim denemeyi planlıyorum (gerçekten işe yarayıp yaramadığını bilmiyorum), ama çaba için bir teşvik.
Robert Harvey,

-2

İşte çok büyük ve çok, çok iş parçacıklı bir uygulamada kullandığım bir strateji:

Öncelikle, bir muteks çevresinde (ne yazık ki) bazı veri yapısına ihtiyacınız var ve senkronizasyon çağrıları dizini yapmıyorsunuz. Bu veri yapısında, daha önce kilitlenmiş herhangi bir muteksin bağlantısı var. Her mutex, 0'dan başlamak üzere mutex'in oluşturulduğu ve asla değişemeyeceği zaman atadığınız bir "seviye" ye sahiptir.

Kural şudur: Eğer bir muteks kilitlenirse, diğer muteksleri yalnızca daha düşük bir seviyede kilitlemelisiniz. Bu kurala uyarsanız, kilitlenmeye sahip olamazsınız. Bir ihlal tespit ettiğinizde başvurunuz hala devam ediyor ve sorunsuz çalışıyor.

Bir ihlal tespit ettiğinizde iki olasılık vardır: Düzeyleri yanlış atamış olabilirsiniz. A'yı ve ardından B'yi kilitlemenin ardından B'nin daha düşük bir seviyeye sahip olması gerekir. Yani seviyeyi düzeltip tekrar dene.

Diğer olasılık: Düzeltemezsin. Bazı kodlar A kilitlerini takip eder, ardından B kilitlenir, bazı kodlar B kilitlenir ve onu A kilitler. Bunu sağlamak için seviyeleri atamanın bir yolu yoktur. Ve elbette bu potansiyel bir kilitlenmedir: Her iki kod da aynı iş parçacığında aynı anda çalışırsa, kilitlenme olasılığı vardır.

Bunu başlattıktan sonra, seviyelerin ayarlanması gereken oldukça kısa bir aşama vardı, ardından potansiyel kilitlenmelerin bulunduğu daha uzun bir aşama geldi.


4
Üzgünüm, bu nasıl uyumsuz / bekliyor davranışları için geçerli? Görev Paralel Kütüphanesine özel bir muteks yönetim yapısını gerçekçi bir şekilde enjekte edemiyorum.
Telastyn

-3

Bir veritabanı gibi pahalı aramaları paralel hale getirmek için Async / Await kullanıyor musunuz? DB'deki yürütme yoluna bağlı olarak bu mümkün olmayabilir.

Zaman uyumsuz / beklemede test kapsamı zor olabilir ve hataları bulmak için gerçek üretim kullanımı gibi bir şey yoktur. Göz önünde bulundurabileceğiniz bir örnek, bir korelasyon kimliğini iletmek ve bunu yığının altına kaydetmek, ardından hatayı kaydeden basamaklı bir zaman aşımına sahip olmaktır. Bu daha çok SOA düzenine sahip ama en azından nereden geldiğine dair bir fikir verecektir. Bunu kilitlenmeleri bulmak için Splunk ile birlikte kullandık.

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.