ฉันจะวินิจฉัย async / รอคอยการหยุดชะงักได้อย่างไร


24

ฉันกำลังทำงานกับ codebase ใหม่ที่ใช้ async / await อย่างหนัก คนส่วนใหญ่ในทีมของฉันก็ค่อนข้างใหม่สำหรับ async / คอย โดยทั่วไปเรามักจะยึดถือแนวทางปฏิบัติที่ดีที่สุดตามที่ระบุไว้โดย Microsoftแต่โดยทั่วไปต้องการบริบทของเราที่จะไหลผ่านการเรียกใช้ async และทำงานกับไลบรารีที่ไม่ConfigureAwait(false)ต้องการ

รวมทุกสิ่งเหล่านั้นเข้าด้วยกันและเราพบ aslock deadlock ที่อธิบายไว้ในบทความ ... ทุกสัปดาห์ พวกเขาจะไม่ปรากฏขึ้นในระหว่างการทดสอบหน่วยเพราะแหล่งข้อมูลของเราเยาะเย้ย (มักจะผ่านTask.FromResult) ไม่เพียงพอที่จะเรียกการหยุดชะงัก ดังนั้นในระหว่างการทดสอบรันไทม์หรือการรวมการบริการบางอย่างเพิ่งออกไปทานอาหารกลางวันและไม่ส่งคืน นั่นจะฆ่าเซิร์ฟเวอร์และโดยทั่วไปก็ทำสิ่งต่าง ๆ เป็นระเบียบ

ปัญหาคือการติดตามจุดที่เกิดข้อผิดพลาด (โดยปกติแล้วจะไม่เป็นแบบซิงค์ตลอดเวลา) โดยทั่วไปแล้วจะเกี่ยวข้องกับการตรวจสอบรหัสด้วยตนเองซึ่งใช้เวลานานและไม่สามารถใช้งานได้โดยอัตโนมัติ

วิธีที่ดีกว่าในการวินิจฉัยสิ่งที่ทำให้เกิดการหยุดชะงักคืออะไร


1
คำถามที่ดี; ฉันสงสัยตัวเอง คุณอ่านบทความชุดสะสมของผู้ชายคนนี้แล้วasyncหรือยัง?
Robert Harvey

@ RobertHarvey - อาจจะไม่ใช่ทั้งหมด แต่ฉันได้อ่านมาบ้าง เพิ่มเติม "อย่าลืมทำสองหรือสามสิ่งต่อไปนี้ทุกที่ไม่เช่นนั้นรหัสของคุณจะตายอย่างน่าสยดสยองขณะใช้งานจริง"
Telastyn

คุณเปิดให้ปล่อย async หรือลดการใช้งานไปยังจุดที่เป็นประโยชน์มากที่สุดหรือไม่? Async IO ไม่ใช่ทั้งหมดหรือไม่มีอะไรเลย
usr

1
หากคุณสามารถสร้างการหยุดชะงักได้คุณไม่สามารถดูที่การติดตามสแต็กเพื่อดูการบล็อคการโทรได้หรือไม่?
svick

2
หากปัญหาคือ "ไม่ใช่ async ตลอดทาง" หมายความว่าครึ่งหนึ่งของ deadlock เป็น deadlock แบบดั้งเดิมและควรมองเห็นได้ในการติดตามสแต็กของเธรดบริบทการซิงโครไนซ์
svick

คำตอบ:


4

ตกลง - ฉันไม่แน่ใจว่าสิ่งต่อไปนี้จะช่วยคุณได้หรือไม่เพราะฉันได้ตั้งสมมติฐานบางอย่างในการพัฒนาวิธีแก้ปัญหาซึ่งอาจเป็นจริงหรือไม่จริงในกรณีของคุณ บางที "การแก้ปัญหา" ของฉันอาจมีเหตุผลทางทฤษฎีและใช้ได้กับตัวอย่างที่เป็นสิ่งประดิษฐ์เท่านั้น - ฉันยังไม่ได้ทำการทดสอบใด ๆ นอกเหนือจากสิ่งที่อยู่ด้านล่าง
นอกจากนี้ฉันจะเห็นวิธีแก้ปัญหาต่อไปนี้มากกว่าวิธีแก้ปัญหาจริง แต่พิจารณาการขาดการตอบสนองฉันคิดว่ามันอาจจะดีกว่าไม่มีอะไรเลย (ฉันคอยดูคำถามของคุณรอวิธีแก้ปัญหา แต่ไม่เห็นโพสต์ที่เริ่มโพสต์ รอบกับปัญหา)

แต่พอกล่าวว่า: สมมติว่าเรามีบริการข้อมูลอย่างง่ายซึ่งสามารถใช้เพื่อดึงข้อมูลจำนวนเต็ม:

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

การใช้งานง่ายใช้รหัสไม่ตรงกัน:

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;
    }
}

ตอนนี้มีปัญหาเกิดขึ้นถ้าเราใช้รหัส "ไม่ถูกต้อง" ตามที่แสดงในคลาสนี้ Fooเข้าถึงอย่างไม่ถูกต้องTask.Resultแทนที่จะawaitใช้ผลลัพธ์เช่นBar:

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;
    }
}

สิ่งที่เรา (คุณ) ต้องการคือวิธีเขียนข้อสอบที่ประสบความสำเร็จเมื่อโทรBarแต่ล้มเหลวเมื่อโทรFoo(อย่างน้อยถ้าฉันเข้าใจคำถามอย่างถูกต้อง ;-))

ฉันจะให้โค้ดพูด นี่คือสิ่งที่ฉันคิดขึ้นมา (โดยใช้การทดสอบ Visual Studio แต่ควรทำงานโดยใช้ NUnit ด้วย):

DataServiceMockTaskCompletionSource<T>Utilizes สิ่งนี้ช่วยให้เราสามารถกำหนดผลลัพธ์ ณ จุดที่กำหนดในการทดสอบการทำงานซึ่งนำไปสู่การทดสอบต่อไปนี้ โปรดทราบว่าเรากำลังใช้ผู้รับมอบสิทธิ์เพื่อส่งกลับ TaskCompletionSource กลับสู่การทดสอบ คุณอาจวางสิ่งนี้ลงในวิธีการเตรียมใช้งานของการทดสอบและใช้คุณสมบัติ

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;

สิ่งที่เกิดขึ้นที่นี่คือเราตรวจสอบก่อนว่าเราสามารถออกจากวิธีการโดยไม่มีการปิดกั้น (จะไม่ทำงานถ้ามีคนเข้าถึงTask.Result- ในกรณีนี้เราจะหมดเวลาเนื่องจากผลลัพธ์ของงานไม่สามารถใช้ได้จนกว่าหลังจากวิธีการคืน )
จากนั้นเราตั้งค่าผลลัพธ์ (ตอนนี้วิธีสามารถใช้งานได้) และเราตรวจสอบผลลัพธ์ (ในการทดสอบหน่วยที่เราสามารถเข้าถึง Task.Result ตามที่เราต้องการการบล็อกเกิดขึ้นจริง)

ทำคลาสทดสอบเสร็จสมบูรณ์ - BarTestสำเร็จและFooTestล้มเหลวตามต้องการ

[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;
    }
}

และชั้นเรียนตัวช่วยเล็ก ๆ เพื่อทดสอบการหยุดชะงัก / หมดเวลา

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);
    }
}

คำตอบที่ดี ฉันวางแผนที่จะลองใช้รหัสของคุณด้วยตัวเองเมื่อฉันมีเวลา (ฉันไม่รู้จริง ๆ ว่ามันใช้งานได้จริงหรือไม่) แต่ขอชื่นชมและชื่นชมด้วยความพยายาม
Robert Harvey

-2

นี่เป็นกลยุทธ์ที่ฉันใช้ในแอพพลิเคชั่นที่มีขนาดใหญ่มากและมีหลายเธรดมาก:

ก่อนอื่นคุณต้องมีโครงสร้างข้อมูลบางอย่างรอบ ๆ mutex (น่าเสียดาย) และไม่มีการซิงโครไนซ์การโทร ในโครงสร้างข้อมูลนั้นมีลิงก์ไปยัง mutex ที่ล็อคไว้ก่อนหน้านี้ mutex ทุกตัวมี "ระดับ" เริ่มต้นที่ 0 ซึ่งคุณกำหนดเมื่อสร้าง mutex และไม่สามารถเปลี่ยนแปลงได้

และกฎคือ: หาก mutex ถูกล็อคคุณจะต้องล็อค mutex อื่น ๆ ในระดับที่ต่ำกว่าเท่านั้น หากคุณปฏิบัติตามกฎนั้นคุณจะไม่สามารถหยุดชะงักได้ เมื่อคุณพบการละเมิดแอปพลิเคชันของคุณยังคงทำงานได้ตามปกติ

เมื่อคุณพบการละเมิดมีความเป็นไปได้สองอย่าง: คุณอาจกำหนดระดับที่ไม่ถูกต้อง คุณล็อค A แล้วตามด้วยการล็อค B ดังนั้น B ควรมีระดับที่ต่ำกว่า ดังนั้นคุณจะต้องกำหนดระดับและลองใหม่อีกครั้ง

ความเป็นไปได้อื่น ๆ : คุณไม่สามารถแก้ไขได้ โค้ดบางส่วนของคุณล็อค A ตามด้วยการล็อค B ในขณะที่บางรหัสอื่นล็อค B ตามด้วยการล็อค A. ไม่มีวิธีกำหนดระดับเพื่อให้สิ่งนี้ และแน่นอนว่านี่คือการหยุดชะงักที่เป็นไปได้: หากรหัสทั้งสองทำงานพร้อมกันในเธรดที่แตกต่างกันมีโอกาสเกิดการหยุดชะงัก

หลังจากแนะนำสิ่งนี้จะมีระยะเวลาค่อนข้างสั้นที่ต้องปรับระดับตามด้วยระยะเวลานานกว่าที่พบการหยุดชะงักที่อาจเกิดขึ้น


4
ฉันขอโทษที่นำไปใช้กับพฤติกรรม async / รอ? ฉันไม่สามารถฉีดโครงสร้างการจัดการ mutex แบบกำหนดเองลงใน Task Parallel Library ได้
Telastyn

-3

คุณใช้ Async / Await เพื่อให้คุณสามารถโทรหาคู่ที่มีราคาแพงเช่นฐานข้อมูลได้หรือไม่? ขึ้นอยู่กับเส้นทางการดำเนินการในฐานข้อมูลซึ่งอาจเป็นไปไม่ได้

การทดสอบครอบคลุมด้วย async / await อาจเป็นเรื่องที่ท้าทายและไม่มีอะไรที่เหมือนกับการใช้งานจริงเพื่อค้นหาข้อบกพร่อง รูปแบบหนึ่งที่คุณอาจพิจารณาคือการส่งรหัสสหสัมพันธ์และบันทึกลงในสแต็กจากนั้นมีการหมดเวลาแบบเรียงซ้อนที่บันทึกข้อผิดพลาด นี่เป็นรูปแบบ SOA มากกว่า แต่อย่างน้อยมันจะให้ความรู้สึกว่ามาจากไหน เราใช้สิ่งนี้กับ Splunk เพื่อค้นหาการหยุดชะงัก

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.