2013-04-16 102 views
2

以下代碼無需多線程即可正常工作。但是如果我使用線程,它會失敗。如果我在checkedListBox中選擇多個項目,則第一項將被忽略,其他項將隨機...線程和函數參數

我認爲提交數據存在問題。你怎麼看?

private void sendCom(String com) 
    { 
     //send command to selected item 
     int i=0; 
     String IP; 
     foreach (var item in checkedListBox1.CheckedItems) 
     { 
      Console.WriteLine(item.ToString()); 
      IP = item.ToString(); 
      theThreads[i] = new Thread(new ThreadStart(() => sendComThread(IP, com))); 
      theThreads[i].Start(); 
      //sendCom(IP, com); 
      i++; 
     } 
    } 

    private void sendComThread(String IP, String com) 
    { 
     // send an command 
     System.Console.WriteLine(IP + com); 
    } 

回答

4

最根本的問題是,您的變量捕捉捕捉一個變量,然後所有線程之間共享。所以每當一個線程讀取共享變量時,它就會得到最近發生的任何值。除了那個語義錯誤之外,共享變量還有一個明確的數據競爭。

最簡單的解決方案是爲每個線程創建一個變量。只需在循環內移動變量的聲明即可。像這樣:

foreach (var item in checkedListBox1.CheckedItems) 
{ 
    ....   
    String IP = item.ToString(); //NB variable declared inside loop  
    theThreads[i] = new Thread(new ThreadStart(() => sendComThread(IP, com)));  
    .... 
} 

現在每個線程都有自己的字符串變量的私有實例。

+0

不,看上面,它不起作用。 – tux007

+0

上面看看究竟在哪裏? –

+1

@ tux007這裏的細微差別是'IP'被聲明在循環的範圍內,而不是外部。因此,每次迭代都會創建一個新對象,並傳遞給lambda。當它在循環之外聲明時,每次迭代都會寫入* same *對象,導致遇到的問題。 – Polynomial

2

這裏的問題是,你的線程讀取來自lambda表達式循環的狀態,新的線程內,而不是轉移到線通過的實際值。

這意味着,在CPU上調度新線程時,循環實際上已經迭代到未知狀態。這就是爲什麼你的值是隨機的。

這裏是什麼一步在步驟發生:

  1. () => sendComThread(IP, com)拉姆達被創建,它引用兩個參數。
  2. theThreads[i].Start();被調用,但這是而不是保證該線程中的代碼將立即運行。在系統上的線程調度程序將上下文切換到另一個線程之前,目前的代碼很可能會持續一段時間。
  3. 發生下一次循環迭代,並執行IP = item.ToString();,更改IP的值。這可能會發生一次以上。
  4. 上下文切換髮生在處理器上,另一個線程被執行,或另一個線程在另一個處理器(核心)上執行,從lambda表達式讀取對IP的引用。
  5. 這會導致跨線程讀取,這意味着IP的狀態未定義。

的解決方案是線程創建期間在傳遞的價值觀,讓他們複製本地線程:

struct SendComThreadParams 
{ 
    public string IP; 
    public string Com; 

    public SendComThreadParams(string ip, string com) 
    { 
     this.IP = ip; 
     this.Com = com; 
    } 
} 

private void sendCom(String com) 
{ 
    //send command to selected item 
    int i=0; 
    String IP; 
    foreach (var item in checkedListBox1.CheckedItems) 
    { 
     Console.WriteLine(item.ToString()); 
     IP = item.ToString(); 
     theThreads[i] = new Thread(new ParameterizedThreadStart(sendComThread)); 
     theThreads[i].Start(new SendComThreadParams(IP, com)); 
     i++; 
    } 
} 

private void sendComThread(object threadParam) 
{ 
    var p = (SendComThreadParams)threadParam; 
    // send an command 
    System.Console.WriteLine(p.IP + p.Com); 
} 

這正確複製參數到線程,使得它們的值保證處於確定的狀態。

+0

謝謝,它工作正常!順便說一句,你能給我一個線程池的例子嗎? – tux007

+2

或者你可以使用循環內部定義的*。無需爲此創建課程。 – svick

+0

這看起來好像過火了。另外,你的意思是這個跨線程讀取點。這對我來說是錯誤的。 –

1

theThreads[i].Start()將不會立即運行新線程,並且IP變量可能會同時發生變化。

定義IP變量中的for循環將會解決這個問題:

string IP = item.ToString(); 
+0

不,它不會,因爲那樣會簡單地在迭代器引用的當前對象上生成跨線程讀取,導致相同的問題。除了它可能會更糟,因爲它可能會導致與UI交互的跨線程操作異常。 – Polynomial

+1

@Polynomial不,如果你有一個本地內循環,然後循環的每次迭代重新創建本地。所以,線程之間沒有共享。我不確定這與用戶界面有什麼關係。 – svick

+0

@svick仍然可怕的做法。如果在新創建的線程運行之前該方法返回,該怎麼辦?當地人都在那個時候處理掉了。垃圾收集週期可能會導致類似的問題。 – Polynomial

0

這裏是多項式代碼的另一個版本。這次用線程池

struct SendComThreadParams 
{ 
public string IP; 
public string Com; 

public SendComThreadParams(string ip, string com) 
{ 
    this.IP = ip; 
    this.Com = com; 
} 
} 

private void sendCom(String com) 
{ 
//send command to selected item 
int i=0; 
String IP; 
foreach (var item in checkedListBox1.CheckedItems) 
{ 
    Console.WriteLine(item.ToString()); 
    IP = item.ToString(); 
    ThreadPool.QueueUserWorkItem(new WaitCallback(sendComThread), (object)new SendComThreadParams(IP, com)); 
    i++; 
} 
} 

private void sendComThread(object threadParam) 
{ 
var p = (SendComThreadParams)threadParam; 
// send an command 
System.Console.WriteLine(p.IP + p.Com); 
} 
+2

這是什麼添加。線程池是一個側面問題。這個問題涉及變量捕獲。我希望你正在閱讀各種評論。 –

+0

這添加了threadpool的選項。如果有人能給我一個例子,我可以問一個問題。我親自接到併發布了答案。 – tux007

+0

答案應該解決問題。線程池變種只是真的與你有關。我認爲這個答案增加了一點,只是阻礙了一個有趣的話題。 –