รหัสนี้ค้างในโหมดเผยแพร่ แต่ทำงานได้ดีในโหมดดีบัก


110

ฉันเจอสิ่งนี้และต้องการทราบสาเหตุของพฤติกรรมนี้ในโหมดดีบักและรีลีส

public static void Main(string[] args)
{            
   bool isComplete = false;

   var t = new Thread(() =>
   {
       int i = 0;

        while (!isComplete) i += 0;
   });

   t.Start();

   Thread.Sleep(500);
   isComplete = true;
   t.Join();
   Console.WriteLine("complete!");
}

25
อะไรคือความแตกต่างในพฤติกรรม?
Mong Zhu

4
ถ้าเป็น java ฉันจะถือว่าคอมไพเลอร์ไม่เห็นการอัพเดตตัวแปร 'คอมไพล์' การเพิ่ม 'ระเหย' ในการประกาศตัวแปรจะแก้ไขปัญหานั้นได้ (และทำให้เป็นฟิลด์คงที่)
Sebastian

23
ah, Heisenbug
David Richerby

4
หมายเหตุ: นี่คือสาเหตุที่การพัฒนาแบบมัลติเธรดมีหลายสิ่งเช่น mutexes และการดำเนินการของอะตอม เมื่อคุณเริ่มใช้งานมัลติเธรดคุณต้องพิจารณาปัญหาหน่วยความจำเพิ่มเติมที่น่าทึ่งซึ่งไม่ชัดเจนมาก่อน เครื่องมือการซิงโครไนซ์เธรดเช่น mutexes จะแก้ไขปัญหานี้ได้
Cort Ammon

5
@DavidSchwartz: แน่นอนว่าจะได้รับอนุญาต และอนุญาตให้คอมไพเลอร์รันไทม์และ CPU ทำให้ผลลัพธ์ของโค้ดนั้นแตกต่างจากที่คุณคาดหวัง โดยเฉพาะอย่างยิ่ง C # ไม่ได้รับอนุญาตให้ทำการเข้าถึงบูลแบบไม่ใช้อะตอมแต่ได้รับอนุญาตให้ย้ายการอ่านแบบไม่ลบเลือนย้อนเวลาได้ ในทางตรงกันข้ามคู่ผสมไม่มีข้อ จำกัด เรื่องปรมาณู การอ่านและเขียนสองครั้งในสองเธรดที่แตกต่างกันโดยไม่ต้องซิงโครไนซ์จะได้รับอนุญาตให้ฉีกขาด
Eric Lippert

คำตอบ:


149

ฉันเดาว่าเครื่องมือเพิ่มประสิทธิภาพถูกหลอกโดยไม่มีคีย์เวิร์ด 'ระเหย' ในisCompleteตัวแปร

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

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

public static void Main(string[] args)
{
    TheHelper hlp = new TheHelper();

    var t = new Thread(hlp.Body);

    t.Start();

    Thread.Sleep(500);
    hlp.isComplete = true;
    t.Join();
    Console.WriteLine("complete!");
}

private class TheHelper
{
    public bool isComplete = false;

    public void Body()
    {
        int i = 0;

        while (!isComplete) i += 0;
    }
}

ตอนนี้ฉันสามารถจินตนาการได้ว่าคอมไพเลอร์ / เครื่องมือเพิ่มประสิทธิภาพ JIT ในสภาพแวดล้อมแบบมัลติเธรดเมื่อTheHelperคลาสการประมวลผลสามารถแคชค่าfalseในรีจิสเตอร์หรือสแต็กเฟรมที่จุดเริ่มต้นของBody()เมธอดและไม่รีเฟรชจนกว่าเมธอดจะสิ้นสุด นั่นเป็นเพราะไม่มีการรับประกันว่าเธรด & เมธอดจะไม่สิ้นสุดก่อนที่ "= true" จะถูกเรียกใช้งานดังนั้นหากไม่มีการรับประกันทำไมไม่แคชและเพิ่มประสิทธิภาพในการอ่านฮีปอ็อบเจ็กต์เพียงครั้งเดียวแทนที่จะอ่านในทุกๆ การทำซ้ำ

นี่คือเหตุผลว่าทำไมคำหลักจึงvolatileมีอยู่

เพื่อให้คลาสตัวช่วยนี้แก้ไขได้ ดีขึ้นเล็กน้อย 1)ในสภาพแวดล้อมแบบมัลติเธรดควรมี:

    public volatile bool isComplete = false;

แต่แน่นอนเนื่องจากเป็นโค้ดที่สร้างขึ้นโดยอัตโนมัติคุณจึงไม่สามารถเพิ่มได้ แนวทางที่ดีกว่าคือการเพิ่มlock()s รอบ ๆ การอ่านและการเขียนisCompletedหรือการใช้ยูทิลิตี้การซิงโครไนซ์หรือเธรด / การทำเธรดที่พร้อมใช้งานอื่น ๆ แทนที่จะพยายามทำแบบโลหะเปล่า (ซึ่งจะไม่เป็นโลหะเปล่า เนื่องจากเป็น C # บน CLR กับ GC, JIT และ (.. ))

ความแตกต่างในโหมดดีบักอาจเกิดขึ้นเนื่องจากในโหมดดีบักการเพิ่มประสิทธิภาพจำนวนมากถูกยกเว้นดังนั้นคุณสามารถดีบักโค้ดที่คุณเห็นบนหน้าจอได้ ดังนั้นจึงwhile (!isComplete)ไม่ได้รับการปรับให้เหมาะสมเพื่อให้คุณสามารถตั้งค่าเบรกพอยต์ได้ที่นั่นดังนั้นจึงisCompleteไม่ถูกแคชในรีจิสเตอร์หรือสแต็กอย่างจริงจังเมื่อเมธอดเริ่มต้นและอ่านจากอ็อบเจ็กต์บนฮีปในทุกๆการวนซ้ำ

BTW. นั่นเป็นเพียงการคาดเดาของฉัน ฉันไม่ได้พยายามรวบรวมมันด้วยซ้ำ

BTW. ดูเหมือนจะไม่ใช่ข้อผิดพลาด มันเหมือนกับผลข้างเคียงที่คลุมเครือมากกว่า นอกจากนี้ถ้าฉันพูดถูกก็อาจเป็นความบกพร่องทางภาษา - C # ควรอนุญาตให้วางคีย์เวิร์ด 'ระเหย' ในตัวแปรท้องถิ่นที่จับและเลื่อนระดับเป็นฟิลด์สมาชิกในการปิด

1)ดูด้านล่างสำหรับความคิดเห็นจาก Eric Lippert เกี่ยวกับvolatileและ / หรือบทความที่น่าสนใจนี้ซึ่งแสดงระดับความซับซ้อนที่เกี่ยวข้องในการตรวจสอบให้แน่ใจว่ารหัสที่อาศัยอยู่volatileนั้นปลอดภัย .. เอ่อดี .. เอ่อเอาเป็นว่าตกลง


2
@EricLippert: โอ้โฮขอบคุณมากที่ยืนยันว่าเร็วมาก! คุณคิดว่ามีโอกาสใดบ้างที่ในบางเวอร์ชันในอนาคตเราจะได้รับvolatileตัวเลือกในการจับเพื่อปิดตัวแปรภายใน ฉันคิดว่ามันสามารถเป็นบิตยากที่จะดำเนินโดยรวบรวม ..
Quetzalcoatl

7
@quetzalcoatl: ฉันจะไม่นับว่ามีการเพิ่มคุณสมบัตินั้นเร็ว ๆ นี้ นี้เป็นชนิดของการเขียนโปรแกรมที่คุณต้องการที่จะกีดกันและไม่ทำให้ง่ายขึ้น และนอกจากนี้การทำให้สิ่งต่างๆผันผวนไม่จำเป็นต้องแก้ปัญหาทุกอย่าง นี่คือตัวอย่างที่ทุกอย่างมีความผันผวนและโปรแกรมยังคงผิดพลาด คุณสามารถหาจุดบกพร่อง? blog.coverity.com/2014/03/26/reordering-optimizations
Eric Lippert

3
เข้าใจแล้ว. ฉันล้มเลิกความพยายามที่จะทำความเข้าใจการเพิ่มประสิทธิภาพแบบมัลติเธรด ... มันซับซ้อนแค่ไหน
ระหว่าง

10
@Pikoh: คิดเหมือนเครื่องมือเพิ่มประสิทธิภาพอีกครั้ง คุณมีตัวแปรที่เพิ่มขึ้น แต่ไม่เคยอ่าน ตัวแปรที่ไม่เคยอ่านสามารถลบได้ทั้งหมด
Eric Lippert

4
@EricLippert ตอนนี้ใจของฉันคลิก กระทู้นี้มีข้อมูลมากขอบคุณมากจริงๆ
Pikoh

82

คำตอบของ Quetzalcoatlถูกต้อง เพื่อให้แสงสว่างมากขึ้น:

คอมไพลเลอร์ C # และ CLR jitter ได้รับอนุญาตให้ทำการปรับให้เหมาะสมจำนวนมากซึ่งถือว่าเธรดปัจจุบันเป็นเธรดเดียวที่รัน หากการเพิ่มประสิทธิภาพผู้ที่ทำให้โปรแกรมที่ไม่ถูกต้องในโลกที่เธรดปัจจุบันไม่ได้เป็นเพียงการทำงานด้ายที่เป็นปัญหาของคุณ คุณจะต้องเขียนโปรแกรมมัลติเธรดที่บอกคอมไพเลอร์และกระวนกระวายใจว่าคุณกำลังทำอะไรอยู่

ในกรณีนี้อนุญาตให้ใช้การกระวนกระวายใจ - แต่ไม่จำเป็น - เพื่อสังเกตว่าตัวแปรนั้นไม่เปลี่ยนแปลงโดยร่างกายของลูปดังนั้นจึงสรุปได้ว่า - เนื่องจากสมมติฐานนี้เป็นเธรดเดียวที่รันตัวแปรจะไม่เปลี่ยนแปลง หากไม่เคยเปลี่ยนแปลงตัวแปรจะต้องได้รับการตรวจสอบความจริงเพียงครั้งเดียวไม่ใช่ทุกครั้งที่ผ่านลูป และนี่คือสิ่งที่เกิดขึ้นจริง

วิธีแก้ปัญหานี้ อย่าเขียนโปรแกรมแบบมัลติเธรด การทำมัลติเธรดเป็นเรื่องยากอย่างไม่น่าเชื่อแม้กระทั่งสำหรับผู้เชี่ยวชาญ ถ้าคุณต้องแล้วใช้กลไกระดับสูงสุดเพื่อให้บรรลุเป้าหมายของคุณ การแก้ปัญหาในที่นี้ไม่ใช่การทำให้ตัวแปรผันผวน วิธีการแก้ปัญหาที่นี่คือการเขียนเป็นงานที่ยกเลิกและใช้กลไกการขนานยกเลิกงานห้องสมุด ให้ TPL กังวลเกี่ยวกับการทำให้ตรรกะของเธรดถูกต้องและการยกเลิกจะถูกส่งข้ามเธรดอย่างถูกต้อง


1
ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
Madara's Ghost

14

ฉันยึดติดกับกระบวนการทำงานและพบว่า (ถ้าฉันไม่ได้ทำผิดพลาดฉันไม่ค่อยได้ฝึกฝนกับสิ่งนี้) ว่าThreadวิธีนี้ถูกแปลเป็นดังนี้:

debug051:02DE04EB loc_2DE04EB:                            
debug051:02DE04EB test    eax, eax
debug051:02DE04ED jz      short loc_2DE04EB
debug051:02DE04EF pop     ebp
debug051:02DE04F0 retn

eax(ซึ่งมีค่าของisComplete) ถูกโหลดครั้งแรกและไม่เคยรีเฟรช


8

ไม่ใช่คำตอบจริงๆ แต่เพื่อให้ความกระจ่างเพิ่มเติมเกี่ยวกับปัญหา:

ปัญหาน่าจะเกิดขึ้นเมื่อiมีการประกาศภายในร่างกายแลมด้าและจะอ่านเฉพาะในนิพจน์การมอบหมายเท่านั้น มิฉะนั้นรหัสจะทำงานได้ดีในโหมดเผยแพร่:

  1. i ประกาศภายนอกร่างกายแลมด้า:

    int i = 0; // Declared outside the lambda body
    
    var t = new Thread(() =>
    {
        while (!isComplete) { i += 0; }
    }); // Completes in release mode
    
  2. i ไม่ได้อ่านในนิพจน์การมอบหมาย:

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { i = 0; }
    }); // Completes in release mode
    
  3. i ยังอ่านที่อื่น:

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { Console.WriteLine(i); i += 0; }
    }); // Completes in release mode
    

เดิมพันของฉันคือคอมไพเลอร์หรือการเพิ่มประสิทธิภาพ JIT เกี่ยวกับการiทำให้สิ่งต่าง ๆ ยุ่งเหยิง ใครบางคนที่ฉลาดกว่าฉันอาจจะสามารถให้ความกระจ่างเกี่ยวกับปัญหานี้ได้

อย่างไรก็ตามฉันจะไม่กังวลมากเกินไปเพราะฉันไม่เห็นว่าโค้ดที่คล้ายกันจะตอบสนองจุดประสงค์ใดได้จริง


1
ดูคำตอบของฉันฉันค่อนข้างแน่ใจว่าทุกอย่างเกี่ยวกับคีย์เวิร์ด 'ระเหย' ที่ไม่สามารถเพิ่มลงในตัวแปรท้องถิ่นได้ (ซึ่งต่อมาได้รับการเลื่อนระดับเป็นฟิลด์สมาชิกในการปิด) ..
quetzalcoatl
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.