การกำหนดค่าอ้างอิงเป็นแบบอะตอมเหตุใดจึงจำเป็นต้องมี Interlocked.Exchange (ref Object, Object)?


108

ในบริการเว็บ asmx แบบมัลติเธรดของฉันฉันมีฟิลด์คลาส _allData ของประเภท SystemData ของฉันเองซึ่งประกอบด้วยไม่กี่รายการList<T>และDictionary<T>ทำเครื่องหมายเป็นvolatile. ข้อมูลระบบ ( _allData) ถูกรีเฟรชเป็นครั้งคราวและฉันทำได้โดยการสร้างออบเจ็กต์อื่นที่เรียกว่าnewDataและเติมโครงสร้างข้อมูลด้วยข้อมูลใหม่ เมื่อเสร็จแล้วฉันก็มอบหมาย

private static volatile SystemData _allData

public static bool LoadAllSystemData()
{
    SystemData newData = new SystemData();
    /* fill newData with up-to-date data*/
     ...
    _allData = newData.
} 

สิ่งนี้ควรใช้งานได้เนื่องจากการกำหนดเป็นแบบปรมาณูและเธรดที่มีการอ้างอิงถึงข้อมูลเก่ายังคงใช้งานต่อไปและส่วนที่เหลือจะมีข้อมูลระบบใหม่หลังการมอบหมาย แต่คนสอนฉันบอกว่าแทนการใช้volatileคำหลักและมอบหมายง่ายฉันควรใช้InterLocked.Exchangeเพราะเขาบอกว่าบนแพลตฟอร์มบางคนก็ไม่ได้รับประกันว่าได้รับมอบหมายอ้างอิงอะตอม ยิ่งไปกว่านั้น: เมื่อฉันประกาศthe _allDataฟิลด์เป็นvolatileไฟล์

Interlocked.Exchange<SystemData>(ref _allData, newData); 

สร้างคำเตือน "การอ้างอิงถึงเขตข้อมูลที่มีความผันผวนจะไม่ถือว่าเป็นความผันผวน" ฉันควรคิดอย่างไรเกี่ยวกับเรื่องนี้

คำตอบ:


180

มีคำถามมากมายที่นี่ พิจารณาทีละครั้ง:

การกำหนดค่าอ้างอิงเป็นแบบอะตอมเหตุใดจึงจำเป็นต้องมี Interlocked.Exchange (ref Object, Object)?

การกำหนดอ้างอิงคือปรมาณู Interlocked.Exchange ไม่เพียง แต่อ้างอิงการมอบหมายเท่านั้น จะอ่านค่าปัจจุบันของตัวแปรซ่อนค่าเก่าและกำหนดค่าใหม่ให้กับตัวแปรทั้งหมดเป็นการดำเนินการของอะตอม

เพื่อนร่วมงานของฉันบอกว่าในบางแพลตฟอร์มไม่รับประกันว่าการมอบหมายงานอ้างอิงเป็นปรมาณู เพื่อนร่วมงานของฉันถูกต้องหรือไม่?

ไม่ได้รับประกันว่าการมอบหมายการอ้างอิงเป็นแบบปรมาณูบนแพลตฟอร์ม. NET ทั้งหมด

เพื่อนร่วมงานของฉันให้เหตุผลจากสถานที่ที่ไม่ถูกต้อง นั่นหมายความว่าข้อสรุปของพวกเขาไม่ถูกต้องหรือไม่?

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

สร้างคำเตือน "การอ้างอิงถึงเขตข้อมูลที่มีความผันผวนจะไม่ถือว่าเป็นความผันผวน" ฉันควรคิดอย่างไรเกี่ยวกับเรื่องนี้

คุณควรเข้าใจว่าเหตุใดจึงเป็นปัญหาโดยทั่วไป ซึ่งจะนำไปสู่ความเข้าใจว่าเหตุใดคำเตือนจึงไม่สำคัญในกรณีนี้

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

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

ตอนนี้สมมติว่าคุณสร้างตัวแปรซึ่งเป็นนามแฝงของเขตข้อมูลระเหยโดยส่งการอ้างอิงไปยังฟิลด์นั้น ภายในวิธีการที่เรียกว่าคอมไพเลอร์ไม่มีเหตุผลใด ๆ ที่จะรู้ว่าการอ้างอิงนั้นจำเป็นต้องมีความหมายที่ไม่แน่นอน! คอมไพเลอร์จะสร้างโค้ดอย่างร่าเริงสำหรับเมธอดที่ไม่สามารถใช้กฎสำหรับฟิลด์ที่ระเหยได้ แต่ตัวแปรนั้นเป็นฟิลด์ที่มีความผันผวน นั่นสามารถทำลายตรรกะที่ปราศจากการล็อกของคุณได้อย่างสมบูรณ์ สมมติฐานอยู่เสมอว่าฟิลด์ระเหยมักจะเข้าถึงได้ด้วยความหมายที่ผันผวน มันไม่มีเหตุผลที่จะถือว่ามันผันผวนในบางครั้งและไม่ใช่ครั้งอื่น ๆ คุณต้องเสมอให้สอดคล้องมิฉะนั้นคุณจะไม่สามารถรับประกันความสอดคล้องในการเข้าถึงอื่น ๆ

ดังนั้นคอมไพลเลอร์จะเตือนเมื่อคุณทำเช่นนี้เพราะมันอาจจะทำให้ลอจิกล็อคฟรีที่พัฒนาขึ้นอย่างระมัดระวัง

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


1
จากสิ่งที่ฉันเคยได้ยิน Interlocked การแลกเปลี่ยนยังรับประกันว่ามีการสร้างกำแพงหน่วยความจำ ตัวอย่างเช่นหากคุณสร้างวัตถุใหม่ให้กำหนดคุณสมบัติสองสามอย่างจากนั้นเก็บวัตถุไว้ในการอ้างอิงอื่นโดยไม่ใช้ Interlocked การแลกเปลี่ยนคอมไพเลอร์อาจทำให้ลำดับของการดำเนินการเหล่านั้นยุ่งเหยิงทำให้การเข้าถึงการอ้างอิงที่สองไม่ใช่เธรด - ปลอดภัย. เป็นเช่นนั้นจริงเหรอ? มันสมเหตุสมผลหรือไม่ที่จะใช้ Interlocked การแลกเปลี่ยนเป็นสถานการณ์แบบนั้น?
Mike

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

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

2
@EricLippert ระหว่าง "ไม่ใช้มัลติเธรด" และ "ถ้าคุณต้องใช้โครงสร้างข้อมูลที่ไม่เปลี่ยนรูป" ฉันจะแทรกระดับกลางและระดับทั่วไปของ "มีเธรดลูกที่ใช้เฉพาะออบเจ็กต์อินพุตที่เป็นเจ้าของเท่านั้นและเธรดหลักจะใช้ผลลัพธ์ เมื่อเด็กเรียนจบแล้วเท่านั้น ". เช่นเดียวกับในvar myresult = await Task.Factory.CreateNew(() => MyWork(exclusivelyLocalStuffOrValueTypeOrCopy));.
John

1
@ จอห์น: นั่นเป็นความคิดที่ดี ฉันพยายามปฏิบัติต่อเธรดเหมือนกระบวนการราคาถูก: พวกเขาอยู่ที่นั่นเพื่อทำงานและสร้างผลลัพธ์ไม่ใช่เพื่อให้เป็นเธรดที่สองของการควบคุมภายในโครงสร้างข้อมูลของโปรแกรมหลัก แต่ถ้าจำนวนงานที่เธรดทำมีมากจนสมเหตุสมผลที่จะปฏิบัติเหมือนเป็นกระบวนการฉันก็บอกว่าแค่ทำให้มันเป็นกระบวนการ!
Eric Lippert

9

ไม่ว่าเพื่อนร่วมงานของคุณจะเข้าใจผิดหรือเขารู้บางอย่างที่ข้อกำหนดภาษา C # ไม่มี

5.5 Atomicity ของการอ้างอิงตัวแปร :

"การอ่านและการเขียนประเภทข้อมูลต่อไปนี้คือ atomic: bool, char, byte, sbyte, short, ushort, uint, int, float และประเภทการอ้างอิง"

ดังนั้นคุณสามารถเขียนไปยังการอ้างอิงแบบระเหยได้โดยไม่ต้องเสี่ยงต่อการได้รับค่าที่เสียหาย

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


3
@guffa: ใช่ฉันอ่านแล้วยัง สิ่งนี้ทำให้คำถามเดิม "การกำหนดอ้างอิงเป็นอะตอมเหตุใดจึงจำเป็นต้องมี Interlocked.Exchange (ref Object, Object)" ยังไม่ได้ตอบ
ถ่านม.

@zebrabox: คุณหมายถึงอะไร? เมื่อพวกเขาไม่อยู่? คุณจะทำอะไร?
ถ่าน

@matti: จำเป็นเมื่อคุณต้องอ่านและเขียนค่าเป็นการดำเนินการของอะตอม
Guffa

บ่อยแค่ไหนที่คุณต้องกังวลเกี่ยวกับหน่วยความจำที่จัดเรียงไม่ถูกต้องใน. NET จริง ๆ ? ของหนักระหว่างกัน?
Skurmedel

1
@zebrabox: ข้อกำหนดไม่ได้ระบุข้อแม้นั้น แต่ให้คำชี้แจงที่ชัดเจนมาก คุณมีข้อมูลอ้างอิงสำหรับสถานการณ์ที่ไม่ได้จัดแนวหน่วยความจำที่การอ่านหรือเขียนข้อมูลอ้างอิงล้มเหลวเป็นปรมาณูหรือไม่? ดูเหมือนว่าจะเป็นการละเมิดภาษาที่ชัดเจนในข้อกำหนด
TJ Crowder

6

เชื่อมต่อกันแลกเปลี่ยน <T>

ตั้งค่าตัวแปรประเภท T ที่ระบุเป็นค่าที่ระบุและส่งคืนค่าเดิมเป็นการดำเนินการแบบอะตอม

มันเปลี่ยนและคืนค่าเดิมมันไม่มีประโยชน์เพราะคุณต้องการเปลี่ยนมันเท่านั้นและอย่างที่ Guffa กล่าวว่ามันเป็นปรมาณูแล้ว

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


3

Iterlocked.Exchange() ไม่ใช่แค่ปรมาณู แต่ยังดูแลการมองเห็นหน่วยความจำ:

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

ฟังก์ชั่นที่เข้าหรือออกจากส่วนสำคัญ

ฟังก์ชั่นที่ส่งสัญญาณวัตถุการซิงโครไนซ์

รอฟังก์ชั่น

ฟังก์ชันที่เชื่อมต่อกัน

ปัญหาการซิงโครไนซ์และตัวประมวลผลหลายตัว

ซึ่งหมายความว่านอกจากปรมาณูแล้วยังช่วยให้มั่นใจได้ว่า:

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