แสดงการใช้คำสำคัญที่ลบเลือนใน C #


88

ฉันต้องการโค้ดโปรแกรมเล็ก ๆ น้อย ๆ ที่แสดงให้เห็นถึงลักษณะการทำงานของvolatileคำหลัก ตามหลักการแล้วควรเป็นโปรแกรมที่ดำเนินการเข้าถึงพร้อมกันไปยังเขตข้อมูลคงที่ที่ไม่ลบเลือนและทำให้เกิดพฤติกรรมที่ไม่ถูกต้องด้วยเหตุนี้

การเพิ่มคีย์เวิร์ดที่ลบเลือนในโปรแกรมเดียวกันควรแก้ไขปัญหาได้

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

คุณมีความคิดเกี่ยวกับหัวข้อนี้หรือไม่? คุณรู้วิธีจำลองปัญหาดังกล่าวในแอปสาธิตอย่างง่ายหรือไม่? ขึ้นอยู่กับฮาร์ดแวร์หรือไม่?

คำตอบ:


102

ฉันประสบความสำเร็จเป็นตัวอย่างในการทำงาน!

แนวคิดหลักที่ได้รับจาก wiki แต่มีการเปลี่ยนแปลงบางอย่างสำหรับ C # บทความวิกิแสดงให้เห็นถึงสิ่งนี้สำหรับฟิลด์สแตติกของ C ++ ดูเหมือนว่า C # จะรวบรวมคำขอไปยังฟิลด์คงที่อย่างระมัดระวังเสมอ ... และฉันสร้างตัวอย่างโดยไม่คงที่:

หากคุณเรียกใช้ตัวอย่างนี้ในโหมดReleaseและไม่มีดีบักเกอร์ (เช่นใช้ Ctrl + F5) บรรทัดwhile (test.foo != 255)จะถูกปรับให้เหมาะสมเป็น 'while (true)' และโปรแกรมนี้จะไม่ส่งกลับ แต่หลังจากเพิ่มvolatileคำหลักคุณจะได้รับ "ตกลง" เสมอ

class Test
{
    /*volatile*/ int foo;

    static void Main()
    {
        var test = new Test();

        new Thread(delegate() { Thread.Sleep(500); test.foo = 255; }).Start();

        while (test.foo != 255) ;
        Console.WriteLine("OK");
    }
}

5
ทดสอบบน. NET 4.0 ทั้ง x86 และ x64 - สามารถยืนยันได้ว่าตัวอย่างยังใช้ได้ ขอบคุณ! :)
Roman Starkov

1
ดีมาก! ฉันไม่เคยรู้มาก่อนว่า JIT ทำการเพิ่มประสิทธิภาพแบบนี้และการระเหยนั้นจะปิดการใช้งาน
usr

3
เพื่อความชัดเจนคุณกำลังเพิ่มคีย์เวิร์ด "ระเหย" ในการประกาศของ foo ใช่ไหม?
JoeCool

21

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


6

ไม่ใช่เรื่องผิดปกติที่เกิดขึ้นเมื่อไม่ได้ระบุคีย์เวิร์ด 'ระเหย' ยิ่งไปกว่านั้นข้อผิดพลาดอาจเกิดขึ้นเมื่อไม่ได้ระบุไว้ โดยทั่วไปคุณจะรู้ว่าเมื่อใดเป็นกรณีนี้ดีกว่าคอมไพเลอร์!

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

ลักษณะการทำงานนี้ไม่ใช่คีย์เวิร์ดเดียวกับใน C ++

MSDN มีคำอธิบายสั้น ๆที่นี่ ต่อไปนี้เป็นโพสต์เชิงลึกเกี่ยวกับหัวข้อความผันผวนปรมาณูและการเชื่อมต่อกัน


4

เป็นการยากที่จะแสดงให้เห็นใน C # เนื่องจากโค้ดถูกทำให้เป็นนามธรรมโดยเครื่องเสมือนดังนั้นในการใช้งานเครื่องนี้เครื่องเดียวจึงทำงานได้อย่างถูกต้องโดยไม่มีการระเหยในขณะที่รหัสอาจล้มเหลวในอีกเครื่องหนึ่ง

อย่างไรก็ตาม Wikipedia มีตัวอย่างที่ดีในการแสดงให้เห็นในภาษา C

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

อีกตัวอย่างหนึ่งคือ Busy Waiting

อีกครั้งสิ่งนี้อาจเกิดขึ้นกับ C # เช่นกัน แต่ขึ้นอยู่กับเครื่องเสมือนและคอมไพเลอร์ JIT (หรือล่ามถ้าไม่มี JIT ... ในทางทฤษฎีฉันคิดว่า MS ใช้คอมไพเลอร์ JIT เสมอและโมโนก็ใช้ หนึ่ง; แต่คุณอาจปิดการใช้งานได้ด้วยตนเอง)


4

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

using System;
using System.Threading;

namespace VolatileTest
{
  class VolatileTest 
  {
    private volatile int _volatileInt;
    public void Run() {
      new Thread(delegate() { Thread.Sleep(500); _volatileInt = 1; }).Start();
      while ( _volatileInt != 1 ) 
        ; // Do nothing
      Console.WriteLine("_volatileInt="+_volatileInt);
    }
  }

  class NormalTest 
  {
    private int _normalInt;
    public void Run() {
      new Thread(delegate() { Thread.Sleep(500); _normalInt = 1; }).Start();
      // NOTE: Program hangs here in Release mode only (not Debug mode).
      // See: http://stackoverflow.com/questions/133270/illustrating-usage-of-the-volatile-keyword-in-c-sharp
      // for an explanation of why. The short answer is because the
      // compiler optimisation caches _normalInt on a register, so
      // it never re-reads the value of the _normalInt variable, so
      // it never sees the modified value. Ergo: while ( true )!!!!
      while ( _normalInt != 1 ) 
        ; // Do nothing
      Console.WriteLine("_normalInt="+_normalInt);
    }
  }

  class Program
  {
    static void Main() {
#if DEBUG
      Console.WriteLine("You must run this program in Release mode to reproduce the problem!");
#endif
      new VolatileTest().Run();
      Console.WriteLine("This program will now hang!");
      new NormalTest().Run();
    }

  }
}

มีคำอธิบายสั้น ๆ ที่ยอดเยี่ยมจริงๆด้านบนรวมถึงข้อมูลอ้างอิงที่ยอดเยี่ยม ขอบคุณทุกคนที่ช่วยให้หัวของฉันvolatile(อย่างน้อยก็พอที่จะรู้ว่าvolatileสัญชาตญาณแรกของฉันอยู่ที่ไหนlock )

ไชโยและขอบคุณสำหรับปลาทุกตัว คี ธ


PS:ฉันจะให้ความสนใจมากในการสาธิตของการร้องขอเดิมซึ่งเป็น "ผมต้องการที่จะเห็นคง int ระเหยพฤติกรรมอย่างถูกต้องที่คง intเกเร

ฉันได้ลองและล้มเหลวในการท้าทายนี้ (อันที่จริงฉันยอมแพ้อย่างรวดเร็ว ;-) ในทุกสิ่งที่ฉันลองใช้กับ vars คงที่พวกเขาทำงาน "ถูกต้อง" ไม่ว่าจะมีความผันผวนหรือไม่ก็ตาม... และฉันชอบคำอธิบายว่าทำไมถึงเป็นเช่นนั้นถ้าเป็นเช่นนั้นจริง ... เป็นเช่นนั้นหรือไม่ คอมไพเลอร์ไม่แคชค่าของตัวแปรแบบคงที่ในรีจิสเตอร์ (กล่าวคือแคชการอ้างอิงถึงฮีปแอดเดรสนั้นแทน)?

ไม่นี่ไม่ใช่คำถามใหม่ ... เป็นความพยายามที่จะดึงชุมชนกลับไปสู่คำถามเดิม


2

ฉันเจอข้อความต่อไปนี้โดย Joe Albahari ซึ่งช่วยฉันได้มาก

ฉันหยิบตัวอย่างจากข้อความด้านบนซึ่งฉันได้ปรับเปลี่ยนเล็กน้อยโดยการสร้างฟิลด์คงที่ระเหย เมื่อคุณลบvolatileคีย์เวิร์ดโปรแกรมจะบล็อกโดยไม่มีกำหนด เรียกใช้ตัวอย่างนี้ในReleaseโหมดส

class Program
{
    public static volatile bool complete = false;

    private static void Main()
    {           
        var t = new Thread(() =>
        {
            bool toggle = false;
            while (!complete) toggle = !toggle;
        });

        t.Start();
        Thread.Sleep(1000); //let the other thread spin up
        complete = true;
        t.Join(); // Blocks indefinitely when you remove volatile
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.