程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 在異步調用匿名函數時明智地使用局部變量

在異步調用匿名函數時明智地使用局部變量

編輯:關於.NET

問題:由於在多線程中使用了匿名函數外的局部變量而導致的Bug

執行代碼

static void Main(string[] args)
{
    for (int i = 0; i < 10; i++)
    {
        Thread t = new Thread(delegate()
        {
            Thread.Sleep(new Random().Next(1, 10000));
            Console.Write(i + ", ");
        });
        t.Start();
    }
}

將得到輸出:10, 10, 10, 10, 10, 10, 10, 10, 10, 10,

而不是我們期望的類似於:3, 5, 6, 1, 0, 7, 9, 8, 4, 2, 這樣的輸出。這是為什麼呢?(在實際 項目中出現這個Bug的代碼請參考[1])

分析:使用Reflector查看編譯之後的代碼

下面是編譯前後的代碼對比。(使用Reflector的具體方法請參考[2],這裡僅展現結果)

可以發現,i 對於匿名方法來說算得上是“全局”變量,如果在線程處理 i 之前,i 的值就被改變了 的話,就會出現我們不希望出現的結果。

解決方法1:使用更小范圍的局部變量

我們做一個小小的更改,在for循環裡面定義一個變量 j ,讓匿名函數只訪問這個 j

所以運行程序,可得到正確的輸出:3, 5, 6, 1, 0, 7, 9, 8, 4, 2,

由此,我們可以做出一個假設:編譯器會在聲明匿名函數所使用的局部變量的地方聲明AutoGenClass 的實例。那麼,可以推出另一個結論:如果匿名函數裡面使用了成員變量,那麼ThreadStart對象也會變 成成員變量,有興趣的話可以自己用Reflector看一下。

這個方法雖然能有效解決問題,但有一個缺點:聲明臨時變量的意圖不明顯。為了避免哪天有個十分 熱心又不明就裡的程序員覺得“變量 j 根本和 i 一樣嘛”而把 j 給移除了,我強烈建議在 j 的後面加 一個注釋:“這裡聲明一個臨時變量 j 是有深意的,誰敢動它老子跟誰玩命!!”。為避免這個缺點, 可考慮使用下面的解決方法。

解決方法2:在匿名函數中只使用它的參數

這次匿名函數壓根就沒使用“全局”變量,所以同樣可得到正確的結果:9, 7, 0, 5, 3, 8, 1, 6, 4, 2,

如果需要使用線程池,代碼大同小異:

static void Main(string[] args)
{
    for (int i = 0; i < 10; i++)
    {
        ThreadPool.QueueUserWorkItem(delegate(object arg)
        {
            Thread.Sleep(new Random().Next(1, 1000));
            Console.Write(arg + ", ");
        }, i);
    }
    Thread.Sleep(10000);
}

讓我們再一次仔細思考一下為什麼會出現Bug,以及解決方法1和解決方法2是如何生效的。為什麼會出 現Bug呢?表面上看是因為使用了多線程。但為什麼使用了多線程結果就不對了呢?是因為我們期望的執 行順序是“把i的值增加1、輸出i、把i的值再增加1、再輸出i……”,而實際上由於輸出 i 的操作由另 一個線程來執行,導致輸出 i 的操作進入了另一個平行宇宙——呃,我是說異步操作之中。這樣,i 的 值在不停地改變,而輸出 i 的操作隨時都可能被執行,所以輸出的 i 的值就成了隨機的,程序的行為也 成了隨機的,這可真是個不折不扣的Bug。想想看,如果另一個平行宇宙裡的1-2-3脫掉褲子,會影響到我 的褲子也一同掉下來,這是件多可怖的事情!

要解決這個問題,就要使 i 一但被傳遞給線程來執行,就不再受到主線程裡對 i 的改變的影響。解 決方法1通過每次創建新的線程前都創建一個新的AutoGenClass實例,並Copy i 的值(因為 i 是值類型 )給AutoGenClass實例的成員變量達到這個目標。解決方法2通過在啟動線程時把 i 的值壓入匿名方法的 參數堆棧來達到這個目標。

既然確保 i 的值在傳遞給線程執行之後就不再改變這麼重要,我們是否應該去微軟總部門前示威,要 求在匿名函數裡只能使用只讀的局部變量呢?就這麼定了!機票錢老趙出,現在報名……

冷靜。不應該做這樣的限制,因為 1)匿名函數既可以用作同步執行,也可被異步執行。在同步執行 的時候更改局部變量是沒問題的,而且同步執行的情況比較多,我們哪能因噎廢食呢?2)即使限制成只 讀變量也沒用。因為如果 i 是個復雜類型的實例的話,即使聲明成只讀的,一樣可以更改它的屬性的值 ,而如果匿名函數正巧依賴它的屬性值,Bug還是會發生。3)有時我們需要故意讓線程共享可修改的對象 ,請看下文。

“這TMD是怎麼回事?”突然聽到 Boss 吼道,“為什麼你的程序輸出的結果是無序的?馬上給我改成 輸出 0,1,2,3,4,5,6,7,8,9, !”

可是我們用的多線程呀,怎麼能保證各個線程按順序取得數據呢?這種時候,我們就需要故意使用一 個可被主線程和其它線程共享、修改的對象,當然一些同步操作也是必須的,請看下面的例子。

解決方法3:使用泛型Queue傳遞數據

代碼如下:

static void Main(string[] args)
{
    Queue<int> q = new Queue<int>();
    for (int i = 0; i < 10; i++)
    {
        q.Enqueue(i);

        Thread t = new Thread(delegate()
        {
            Thread.Sleep(new Random().Next(1, 10000));
            lock (q)
            {
                if (q.Count > 0)
                    Console.WriteLine(Thread.CurrentThread.Name + ":  do " + q.Dequeue());
            }
        });
        t.Name = "線程" + i;
        t.Start();
    }
}

輸出:

線程2: do 0
線程1: do 1
線程7: do 2
線程9: do 3
線程3: do 4
線程5: do 5
線程6: do 6
線程8: do 7
線程0: do 8
線程4: do 9
請按任意鍵繼續. . .

使用了線程同步之後,線程們排著隊去Queue裡取數據,然後執行,在效率上就體現不出多線程的優勢 了。不過,如果換成用線程池利用後台空閒線程還是有意義的。

聰明的你一定想到了,泛型Queue一樣可以通過參數傳遞進去。

static void Main(string[] args)
{
    Queue<int> q = new Queue<int>();
    for (int i = 0; i < 10; i++)
    {
        q.Enqueue(i);

        Thread t = new Thread(delegate(object arg)
        {
            Thread.Sleep(new Random().Next(1, 10000));
            Queue<int> qq = arg as Queue<int>;
            lock (qq)
            {
                if (qq.Count > 0)
                    Console.WriteLine(Thread.CurrentThread.Name + ":  do " + qq.Dequeue());
            }
        });
        t.Name = "線程" + i;
        t.Start(q);
    }
}

結論

既然.net提供了由線程向匿名函數傳遞參數值的功能,你想要定下一條“多線程回調的匿名函數只允 許使用它的參數,禁止使用函數外的變量”的規矩是可以理解的。不過即使這樣做,當變量是復雜類型的 實例的時候,同樣會有產生Bug的危險。所以,要理解為什麼以及如何同步變量和線程的執行,靈活運用 ,別莫名其妙地掉了褲子。

由於Thread.Start()的參數只能有一個,所以需要傳遞多個數值的時候就必須提前構造一個數組或 Struct,這多少還是有些不便。讓我們去微軟門前游行,要求為Thread.Start()提供一個可變參數的重載 吧,機票錢老趙出……

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved