文章出處

提倡異步編程旨在給用戶更好的前端體驗,但異步編程也讓學習成本和犯錯幾率大大升高,其中最常見且最難處理的就是死鎖。

何謂“死鎖”,英文術語稱“Deadlock”,當兩個以上的運算單元,雙方都在等待對方停止運行,以取得系統資源,但是沒有一方提前退出時,這種狀況,就稱為死鎖。​

舉個例子吧,這里是一段經典的死鎖示例代碼:

int sharedResource1 = 1, sharedResource2 = 2;
var lockResource1 = newobject();
var lockResource2 = newobject();

var t1 = newThread(() =>
{
    Console.WriteLine("thead 1 begin");

    lock (lockResource1)
    {
        Thread.Sleep(10);

        lock (lockResource2)
        {
            sharedResource1++;
            sharedResource2++;
        }
    }
    Console.WriteLine("thead 1 end");
});

var t2 = newThread(() =>
{
    Console.WriteLine("thead 2 begin");

    lock (lockResource2)
    {
        Thread.Sleep(10);​

        lock (lockResource1)
        {
            sharedResource1++;
            sharedResource2++;
        }
    }

    Console.WriteLine("thead 2 end");
});

t1.Start();
t2.Start();

運行結果如下,永遠也不會看到“thread x end”:
Deadlock-Run
這是一個不同次序請求加鎖導致死鎖,歸功于我們的教材對此類死鎖的解釋非常詳細,這里我一筆帶過,接下來看看日常開發中經常遇到的一些更具體的死鎖情況——線程死鎖。

場景1—Task之間互相等待導致死鎖:

Task t1 = null, t2 = null;
t1 = Task.Factory.StartNew(() =>
{
    Console.WriteLine("task 1 begin");
    Task.Delay(10);
    Task.WaitAll(t2);
    Console.WriteLine("task 1 end");
});

t2 = Task.Factory.StartNew(() =>
{
    Console.WriteLine("task 2 begin");
    Task.Delay(10);
    Task.WaitAll(t1);
    Console.WriteLine("task 2 end");
});

Task.WaitAll(t1, t2);
Console.WriteLine("Done");

場景2—​WinForm Invoke搶奪UI線程死鎖:

privatevoid button1_Click(object sender, EventArgs e)
{
    var t = Task.Factory.StartNew<string>(() =>
        {

            Thread.Sleep(0);
            var text = Invoke(newFunc<string>(() =>

            {

                // do some ui-dependent works
                return Text;

            }));​

            return text + " - new title";

        });
    Text = t.Result;
}

場景3—WPF Dispatcher切換死鎖

privatevoid Button_Click(object sender, RoutedEventArgs e)
{
    var t = Task.Factory.StartNew<Brush>((state) =>
    {
        Task.Delay(10);
        var clr = (Color)newColorConverter()
            .ConvertFromInvariantString(state asstring);
        var brush = Dispatcher.Invoke<SolidColorBrush>(() =>
            {
                // do some works
                returnnewSolidColorBrush() { Color = clr };
            });
        return brush;
    }, "red");
    theButton.Background = t.Result;
}

這里將各種無關代碼精簡篩除,基本上很快就可以發現這些情況中的問題,是的,實際上以上幾種場景均是同一個原因——wait線程鎖:主執行線程調用子線程后掛起等待子線程結果,子線程又需要切換到主線程或者等待主線程返回,從而導致兩個線程均處在阻塞狀態(死鎖),如下圖所示:

 deadlock

解決方案很簡單,去除所有的同步等待,至少確保在主線程上一定不要使用同步等待,如何操作呢?你可以到多種選擇,這里我提幾點,拋磚引玉,希望大家可以在實際應用中或者更多靈感和解決方法。

1、去除所有wait,使用async和await關鍵字重寫,推薦使用。
這里或許你會有些迷惑,為什么async和await就能保證不會線程死鎖呢?​如下圖示意代碼片段,當前線程執行完(1)之后,接著執行(2),注意這里執行(2)會切換線程,但是不是阻塞當前線程,.NET在這里耍了個“花招”,實際編譯器發現async和await關鍵字的時候會自動插入一些代碼,利用狀態機在(3)的位置做了個標記,讓當前線程“飛”了一會,等到await所處的子線程結束的時候,修改狀態機狀態,讓當前線程恢復到(3)這里,接著就可以跑(4),從開發者的角度來看,好像這一段代碼是順序執行的。重要的是,這里沒有wait鎖。

async-await

2、去除所有wait,使用Task.ContinueWith來實現代碼順序。
var ta = new Task(()=>{ doSome(); });
ta.ContinueWith((tc)=>{ doAnother(tc.Result); });

3、去除所有wait,將wait之后的代碼移到單獨的調用中,使用事件或者回調函數的方式,在子線程結束的時候,激活主線程。以WinForm為例,如下圖所示:
winform%20async

附上文中所提到測試的代碼工程:下載地址


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()