ข้อความเกริ่นนำ SMP สำหรับ Android

แพลตฟอร์ม Android 3.0 ขึ้นไปได้รับการเพิ่มประสิทธิภาพให้รองรับ แบบมัลติโปรเซสเซอร์ เอกสารนี้จะแนะนำปัญหาที่ สามารถเกิดขึ้นได้เมื่อเขียนโค้ดแบบหลายเธรดสำหรับระบบมัลติโปรเซสเซอร์แบบสมมาตรใน C, C++ และ Java ภาษาโปรแกรม (ต่อไปนี้จะเรียกว่า "Java" ข้อความสั้นกระชับ) ซึ่งมีจุดประสงค์เพื่อเป็นเกริ่นนำสำหรับนักพัฒนาแอป Android ไม่ใช่เพื่อให้ การสนทนาในเรื่องนี้

ข้อมูลเบื้องต้น

SMP เป็นตัวย่อของ “Symmetric Multi-Processor” โดยจะอธิบายการออกแบบใน ซึ่งแกน CPU ที่เหมือนกัน 2 แกนขึ้นไปแชร์สิทธิ์เข้าถึงหน่วยความจำหลัก จนถึง เมื่อไม่กี่ปีที่ผ่านมา อุปกรณ์ Android ทั้งหมดเป็นรุ่น "UP" (Uni-Processor)

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

อุปกรณ์ Android ส่วนใหญ่ที่ขายอยู่ในทุกวันนี้สร้างขึ้นจากการออกแบบ SMP ทำให้การทำงานซับซ้อนขึ้น สำหรับนักพัฒนาซอฟต์แวร์ เงื่อนไขการแข่งขัน ในโปรแกรมที่มีหลายเธรดอาจไม่ทำให้เกิดปัญหาที่มองเห็นได้ในเครื่องโปรเซสเซอร์ แต่อาจล้มเหลวเป็นประจำเมื่อชุดข้อความ 2 ชุดขึ้นไป ทำงานพร้อมกันบนแกนที่ต่างกัน ยิ่งไปกว่านั้น โค้ดอาจมีแนวโน้มมากขึ้นหรือน้อยลงที่จะล้มเหลวเมื่อเรียกใช้ สถาปัตยกรรมโปรเซสเซอร์ หรือแม้กระทั่งในการปรับใช้ สถาปัตยกรรม โค้ดที่ได้รับการทดสอบอย่างละเอียดใน x86 อาจเสียหายอย่างมากใน ARM โค้ดอาจเริ่มล้มเหลวเมื่อคอมไพเลอร์ใหม่ด้วยคอมไพเลอร์ที่ทันสมัยกว่า

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

รูปแบบความสอดคล้องของหน่วยความจำ: เหตุใด SMP จึงแตกต่างออกไปเล็กน้อย

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

ดูอ่านเพิ่มเติมในตอนท้ายของเอกสารสำหรับ จะชี้ให้เห็นถึงการรักษา ในระดับที่ละเอียดขึ้น

รูปแบบความสอดคล้องของหน่วยความจำ หรือมักจะเรียกสั้นๆ ว่า “โมเดลหน่วยความจำ” อธิบายถึง รับประกันภาษาโปรแกรมหรือสถาปัตยกรรมฮาร์ดแวร์ เกี่ยวกับการเข้าถึงหน่วยความจำ ตัวอย่างเช่น หากคุณเขียนค่าไปยังที่อยู่ A แล้วเขียนค่าไปยังที่อยู่ B โมเดลอาจรับประกันว่าแกนประมวลผลทั้งหมดของ CPU จะเห็นการเขียนเหล่านั้นเกิดขึ้น คำสั่งซื้อ

โมเดลที่โปรแกรมเมอร์ส่วนใหญ่คุ้นเคยคือ ความสอดคล้อง ซึ่งมีคำอธิบายเช่นนี้ (โฆษณา Gharachorloo):

  • ดูเหมือนว่าการดำเนินการหน่วยความจำทั้งหมดจะทำงานทีละรายการ
  • ดูเหมือนว่าการดำเนินการทั้งหมดในเทรดเดียวจะทำงานตามลำดับที่อธิบายไว้ โดยโปรแกรมของผู้ประมวลผลข้อมูล

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

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

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

ต่อไปนี้เป็นตัวอย่างง่ายๆ โดยมีโค้ดที่ทำงานในเทรด 2 ชุดข้อความ

ชุดข้อความที่ 1 ชุดข้อความที่ 2
A = 3
B = 5
reg0 = B
reg1 = A

ในตัวอย่างลิทัสนี้และในอนาคตทั้งหมด ตำแหน่งความทรงจำจะแสดงด้วย ตัวอักษรพิมพ์ใหญ่ (A, B, C) และการลงทะเบียน CPU จะขึ้นต้นด้วย "reg" ความทรงจำทั้งหมดคือ เริ่มต้นเป็น 0 ระบบจะดำเนินการตามคำสั่งจากบนลงล่าง มาต่อกันที่ชุดข้อความที่ 1 จัดเก็บค่า 3 ในสถานที่ A และเป็นค่า 5 ในสถานที่ B ชุดข้อความที่ 2 จะโหลดค่าจากตำแหน่ง B ลงใน reg0 แล้วโหลดค่าจาก ตำแหน่ง A ใน reg1 (โปรดทราบว่าเราเขียนเรียงตามลำดับเพียงลำดับเดียวและเขียนตาม อีกรายการหนึ่ง)

คาดว่าเทรด 1 และเทรด 2 จะทำงานบนแกน CPU ที่แตกต่างกัน คุณ ควรสร้างสมมติฐานนี้เสมอเมื่อนึกถึง โค้ดแบบมัลติเธรด

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

ลงทะเบียน รัฐ
reg0=5, reg1=3 เป็นไปได้ (ชุดข้อความ 1 ทำงานก่อน)
reg0=0, reg1=0 เป็นไปได้ (ชุดข้อความ 2 ทำงานก่อน)
reg0=0, reg1=3 เป็นไปได้ (การดำเนินการพร้อมกัน)
reg0=5, reg1=0 ไม่เคยใช้

หากต้องการดูสถานการณ์ที่เราเห็น B=5 ก่อนที่เราจะเห็นร้านค้าต่อ A การอ่านและเขียนจะต้องเกิดขึ้นโดยไม่เป็นไปตามลำดับ ใน ที่ทำงานอย่างเสมอภาค ซึ่งไม่อาจเกิดขึ้นได้

โดยปกติ Uni-processor รวมถึง x86 และ ARM จะสอดคล้องกันตามลำดับ ดูเหมือนว่าเทรดจะทำงานแบบแทรกสลับ เนื่องจากเคอร์เนลของระบบปฏิบัติการสลับ ระหว่าง 2 นี้อยู่ ระบบ SMP ส่วนใหญ่ รวมถึง x86 และ ARM ไม่สม่ำเสมอตามลำดับ ตัวอย่างเช่น เป็นเรื่องปกติสำหรับ ฮาร์ดแวร์ที่ช่วยเก็บบัฟเฟอร์ระหว่างที่เก็บ ไว้ในหน่วยความจำ ไม่มีการเข้าถึงหน่วยความจำในทันทีและแกนอื่นๆ มองเห็นได้

รายละเอียดแตกต่างกันมาก เช่น x86 แม้ว่าจะไม่เรียงตามลำดับ สอดคล้องกัน ยังคงรับประกันว่า reg0 = 5 และ reg1 = 0 เป็นไปไม่ได้ ร้านค้ามีการบัฟเฟอร์ แต่ยังคงรักษาลำดับไว้ ในทางกลับกัน ARM ไม่ได้ทำเช่นนั้น ลำดับของร้านค้าที่บัฟเฟอร์ไม่ และ Store อาจไม่เข้าถึงแกนอื่นๆ ทั้งหมดในเวลาเดียวกัน ความแตกต่างเหล่านี้สำคัญต่อการประกอบโปรแกรมเมอร์ แต่อย่างที่เราเห็นด้านล่างนี้ โปรแกรมเมอร์ C, C++ หรือ Java สามารถ และควรตั้งโปรแกรมในลักษณะที่ปิดบังความแตกต่างทางสถาปัตยกรรมดังกล่าว

ณ ปัจจุบัน เราได้สันนิษฐานว่า นี่เป็นเพียงฮาร์ดแวร์ที่ วิธีการเรียงลำดับใหม่ ในความเป็นจริง คอมไพเลอร์ยังเรียงลำดับวิธีการใหม่เป็น เพื่อปรับปรุงประสิทธิภาพ ในตัวอย่างของเรา คอมไพเลอร์อาจตัดสินใจว่า โค้ดในเทรดที่ 2 ต้องการค่า reg1 ก่อนที่จะต้อง reg0 จึงโหลด reg1 ก่อน หรือบางโค้ดก่อนหน้านี้อาจโหลด A ไว้แล้ว และคอมไพเลอร์ คุณอาจตัดสินใจใช้ค่านั้นซ้ำแทนการโหลด A อีกครั้ง ไม่ว่าในกรณีใด การโหลดไปยัง reg0 และ reg1 อาจมีการจัดเรียงใหม่

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

เนื่องจากคอมไพเลอร์สามารถเรียงลำดับการเข้าถึงหน่วยความจำใหม่ได้ด้วย จริงๆ แล้วปัญหานี้คือ ไม่ใช่เรื่องใหม่สำหรับ SMP แม้แต่บนยูนิโปรเซสเซอร์ คอมไพเลอร์ยังสามารถเรียงลำดับการโหลดใหม่เป็น reg0 และ reg1 ในตัวอย่างของเรา และ Thread 1 อาจมีกำหนดการระหว่าง วิธีการเรียงลําดับใหม่ แต่ถ้าคอมไพเลอร์ของเราไม่มีการเรียงลำดับใหม่ ไม่เคยสังเกตเห็นปัญหานี้ บน ARM SMP ส่วนใหญ่ แม้จะไม่มีคอมไพเลอร์ การเรียงลำดับใหม่ อาจเห็นการจัดเรียงใหม่หลังจากใหญ่มาก จำนวนการดำเนินการที่สำเร็จ เว้นแต่ว่าคุณจะเขียนโปรแกรมแบบประกอบ SMP โดยทั่วไปแล้ว มีแนวโน้มสูงที่คุณจะพบปัญหา มาโดยตลอด

โปรแกรมแบบไม่มีการแข่งข้อมูล

แต่ก็มีวิธีง่ายๆ ในการหลีกเลี่ยงการนึกถึง รายละเอียดเหล่านี้ หากคุณปฏิบัติตามกฎที่ตรงไปตรงมา โดยทั่วไปแล้วจะปลอดภัย เพื่อไม่ให้ลืมส่วนก่อนหน้าทั้งหมด ยกเว้น "ความสอดคล้องตามลำดับ" แต่ก็อาจทำให้คุณเห็นภาวะแทรกซ้อนอื่นๆ ได้หากคุณ ละเมิดกฎเหล่านั้นโดยไม่ตั้งใจ

ภาษาโปรแกรมสมัยใหม่ส่งเสริมสิ่งที่เรียกว่า "การปลอดข้อมูล" สไตล์การเขียนโปรแกรม ตราบใดที่คุณสัญญาว่าจะไม่นำ "การแข่งขันด้านข้อมูล" เข้ามา และหลีกเลี่ยงโครงสร้างที่บอกคอมไพเลอร์ ไม่เช่นนั้น คอมไพเลอร์ และฮาร์ดแวร์ของเราเองที่จะมอบผลลัพธ์ที่สอดคล้องกัน การดำเนินการนี้จะไม่ เพื่อหลีกเลี่ยงการเรียงลำดับ การเข้าถึงหน่วยความจำใหม่ นั่นหมายความว่าหากคุณ ปฏิบัติตามกฎที่คุณจะไม่สามารถบอกได้ว่าการเข้าถึงหน่วยความจำ เรียงลำดับใหม่ เหมือนการบอกคุณว่าไส้กรอกอร่อย ของอาหารอร่อยๆ ตราบใดที่คุณสัญญาว่าจะไม่ไป โรงงานไส้กรอก การแข่งขันด้านข้อมูลเปิดโปงความจริงที่น่าเกลียดเกี่ยวกับความทรงจำ การจัดเรียงใหม่

"การแข่งขันด้านข้อมูล" คืออะไร

การแข่งขันด้านข้อมูลเกิดขึ้นเมื่อเทรดอย่างน้อย 2 รายการเข้าถึงพร้อมกัน ข้อมูลทั่วไปเดียวกัน และมีข้อมูลอย่างน้อยหนึ่งรายการทำการแก้ไข ตาม "ปกติ ข้อมูล" เราหมายถึงสิ่งที่ไม่ใช่ออบเจ็กต์การซิงค์ข้อมูลโดยเฉพาะ ที่มีไว้เพื่อการสื่อสารชุดข้อความ Mutex, ตัวแปรเงื่อนไข, Java ความผันผวนหรือวัตถุอะตอม C++ ไม่ใช่ข้อมูลทั่วไปและการเข้าถึงของวัตถุเหล่านั้น ที่ได้รับอนุญาตให้ลงแข่งขัน อันที่จริงแล้ว มีการใช้เพื่อป้องกัน การแข่งขันด้านข้อมูลบน ออบเจ็กต์

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

ชุดข้อความที่ 1 ชุดข้อความที่ 2
if (A) B = true if (B) A = true

เนื่องจากการดำเนินการไม่ได้มีการจัดลำดับใหม่ ทั้ง 2 เงื่อนไขจะประเมินเป็นเท็จ และ จะไม่มีการอัปเดตตัวแปรใดเลย ดังนั้นจึงไม่สามารถแข่งขันด้านข้อมูลได้ มี ไม่ต้องคำนึงถึงสิ่งที่จะเกิดขึ้นหากการโหลดจาก A และจัดเก็บไปที่ B ใน อย่างไรก็ตาม มีการเรียงลำดับชุดข้อความที่ 1 ใหม่ ไม่อนุญาตให้คอมไพเลอร์จัดเรียงชุดข้อความใหม่ 1 โดยการเขียนใหม่เป็น "B = true; if (!A) B = false" อาจจะ เช่น ทำไส้กรอกกลางเมืองท่ามกลางแสงแดด

การแข่งขันด้านข้อมูลมีการกำหนดอย่างเป็นทางการในประเภทพื้นฐานพื้นฐาน เช่น จำนวนเต็มและ ข้อมูลอ้างอิงหรือตัวชี้ กำลังมอบหมายให้ int ขณะเดียวกัน การอ่านในชุดข้อความอื่น ถือเป็นการแข่งขันด้านข้อมูล แต่ทั้ง C++ และไลบรารีมาตรฐาน มีการเขียนไลบรารีคอลเล็กชัน Java ไว้เพื่อให้คุณระบุเหตุผลเกี่ยวกับ การแข่งขันของข้อมูลในระดับไลบรารี พวกเขาสัญญาว่าจะไม่ทำการแข่งขันด้านข้อมูล เว้นแต่จะมีการเข้าถึงคอนเทนเนอร์เดียวกันพร้อมกันได้ ซึ่งทำการอัปเดต กำลังอัปเดต set<T> ใน 1 ชุดข้อความขณะ ไปอ่านในอุปกรณ์อื่นพร้อมกัน ช่วยให้ไลบรารีสามารถแนะนำ การแข่งขันด้านข้อมูล และอาจเรียกได้อย่างไม่เป็นทางการว่าเป็น "การแข่งขันด้านข้อมูลระดับห้องสมุด" ในทางกลับกัน กำลังอัปเดต set<T> 1 รายการในชุดข้อความ 1 รายการขณะอ่าน อีกอย่างหนึ่ง จะไม่ทำให้เกิดการแข่งขันด้านข้อมูล เนื่องจาก ไลบรารีสัญญาว่าจะไม่นำการแข่งขันข้อมูล (ระดับต่ำ) มาใช้ในกรณีนั้น

โดยปกติจะเข้าถึงช่องต่างๆ ได้พร้อมกันในโครงสร้างข้อมูล ไม่สามารถเริ่มการแข่งข้อมูล แต่มีข้อยกเว้นที่สำคัญอย่างหนึ่งคือ กฎนี้: แถวที่ต่อเนื่องกันของบิตฟิลด์ใน C หรือ C++ จะถือว่าเป็น "ตำแหน่งหน่วยความจำ" เพียงตำแหน่งเดียว การเข้าถึงฟิลด์บิตต่างๆ ในลำดับดังกล่าว ถือเป็นการเข้าถึงเว็บไซต์ ทั้งหมด เพื่อวัตถุประสงค์ในการพิจารณา การมีอยู่ของการแข่งขันด้านข้อมูล ข้อมูลนี้สะท้อนให้เห็นว่าฮาร์ดแวร์ทั่วไปไม่สามารถใช้งานได้ เพื่ออัปเดตบิตแต่ละบิตโดยไม่ต้องอ่านและเขียนบิตที่อยู่ติดกันอีกครั้ง โปรแกรมเมอร์ Java ไม่มีข้อกังวลที่เทียบเคียงกันได้

การหลีกเลี่ยงการแข่งขันด้านข้อมูล

ภาษาโปรแกรมสมัยใหม่มีการซิงค์ข้อมูลจำนวนมาก เพื่อหลีกเลี่ยงการแข่งขันด้านข้อมูล เครื่องมือขั้นพื้นฐานที่สุด ได้แก่

ล็อกหรือปิดเสียง
ปิดเสียง (C++11 std::mutex หรือ pthread_mutex_t) หรือ สามารถใช้บล็อก synchronized ใน Java เพื่อให้แน่ใจว่า โค้ดไม่ทำงานพร้อมกันกับส่วนอื่นๆ ข้อมูลเดียวกัน โดยทั่วไปแล้ว เราจะพูดถึงบริการเหล่านี้และสิ่งอำนวยความสะดวกอื่นๆ ที่คล้ายกัน เป็น "ล็อก" รับล็อกอย่างสม่ำเสมอก่อนที่จะเข้าถึงล็อกที่แชร์ โครงสร้างข้อมูล และปล่อยข้อมูลนั้นในภายหลัง ซึ่งจะป้องกันการแข่งขันด้านข้อมูลเมื่อเข้าถึงข้อมูล โครงสร้างข้อมูล และยังช่วยให้มั่นใจว่าการอัปเดตและการเข้าถึงนั้นเป็นแบบอะตอม กล่าวคือ ไม่ใช่ การอัปเดตอื่นๆ ในโครงสร้างข้อมูล จะเกิดขึ้นในตอนกลาง สมควรที่ เป็นเครื่องมือที่นิยมใช้กันมากที่สุดในการป้องกันการแข่งขันด้านข้อมูล การใช้ Java synchronized บล็อก หรือ C++ lock_guard หรือunique_lock ตรวจสอบว่ามีการปล่อยล็อกอย่างถูกต้องใน เหตุการณ์ข้อยกเว้น
ตัวแปรระเหยง่าย/อะตอม
Java มีช่อง volatile ช่องที่รองรับการเข้าถึงพร้อมกัน โดยไม่ต้องลงแข่งข้อมูล ตั้งแต่ปี 2011 เป็นต้นมา การสนับสนุนภาษา C และ C++ atomic ตัวแปรและช่องที่มีความหมายคล้ายกัน สิ่งเหล่านี้คือ มักจะใช้งานยากกว่าล็อก เนื่องจากช่วยให้มั่นใจว่า การเข้าถึงตัวแปรตัวเดียวแต่ละตัวเป็นระดับอะตอม (ใน C++ คือ ขยายไปยังการดำเนินการอ่าน-แก้ไข-เขียนแบบง่าย เช่น การเพิ่ม ชวา ต้องใช้เมธอดพิเศษสำหรับการดำเนินการดังกล่าว) ตัวแปร volatile หรือ atomic ต่างจากล็อกไม่ได้ ได้โดยตรงเพื่อป้องกันไม่ให้เทรดอื่นๆ รบกวนการทำงานของลำดับโค้ดที่ยาว

คุณต้องทราบว่า volatile มีความแตกต่างกันอย่างมาก ในภาษา C++ และ Java ใน C++ volatile จะไม่ปกป้องข้อมูล แม้ว่าโค้ดเวอร์ชันที่เก่ากว่ามักจะใช้โค้ดนี้เป็นวิธีแก้ปัญหาเบื้องต้นเมื่อขาด atomic ออบเจ็กต์ แต่เราไม่แนะนำให้ทำเช่นนั้น ใน C++ ใช้ atomic<T> สำหรับตัวแปรที่แสดงพร้อมกัน เข้าถึงโดยชุดข้อความหลายรายการ C++ volatile มีไว้สำหรับ อุปกรณ์ที่ลงทะเบียนและสิ่งอื่นๆ ที่คล้ายกัน

C/C++ atomic ตัวแปร หรือตัวแปร Java volatile ซึ่งใช้ในการป้องกันการแข่งขันของข้อมูลกับตัวแปรอื่นๆ ได้ หาก flag คือ ประกาศว่ามีประเภท atomic<bool> หรือ atomic_bool(C/C++) หรือ volatile boolean (Java) และมีค่าเป็น "เท็จ" ในตอนแรก ข้อมูลโค้ดต่อไปนี้จะไม่มีการประมวลผลข้อมูล

ชุดข้อความที่ 1 ชุดข้อความที่ 2
A = ...
  flag = true
while (!flag) {}
... = A

เนื่องจากเทรด 2 รอให้ตั้งค่า flag สิทธิ์เข้าถึง A ในชุดข้อความที่ 2 ต้องเกิดขึ้นหลังจาก และไม่ได้เกิดขึ้นพร้อมกันกับ การมอบหมายให้กับ A ในเทรด 1 ดังนั้นจึงไม่มีการแข่งขันด้านข้อมูลบน A การแข่งขันใน flag จะไม่นับเป็นการแข่งขันด้านข้อมูล เนื่องจากการเข้าถึงที่มีความผันผวน/ระดับอะตอมไม่ใช่ "การเข้าถึงหน่วยความจำแบบธรรมดา"

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

แม้ว่าตัวอย่างก่อนหน้านี้จะไม่มีการใช้ข้อมูล แต่การล็อกพร้อมกันกับ Object.wait() ใน Java หรือตัวแปรเงื่อนไขใน C/C++ มักจะ ให้โซลูชันที่ดีกว่าโดยที่ไม่ต้องเสียเวลารอ ทำให้พลังงานแบตเตอรี่หมดเร็ว

เมื่อมองเห็นการเรียงลำดับหน่วยความจำใหม่

ปกติแล้ว โปรแกรมแบบไม่ต้องเสียข้อมูล ช่วยให้เราไม่ต้องคอยจัดการ มีปัญหาเกี่ยวกับการเรียงลำดับการเข้าถึงหน่วยความจำใหม่ อย่างไรก็ตาม มีหลายกรณีดังนี้ การจัดเรียงใหม่ใดบ้างที่จะมองเห็นได้
  1. หากโปรแกรมมีข้อบกพร่องซึ่งส่งผลให้เกิดการแข่งขันด้านข้อมูลโดยไม่ได้ตั้งใจ การแปลงคอมไพเลอร์และฮาร์ดแวร์สามารถมองเห็นได้ ของ Google อาจทำให้คุณประหลาดใจ ตัวอย่างเช่น ถ้าเราลืมประกาศ flag มีความผันผวนในตัวอย่างก่อนหน้านี้ เทรดที่ 2 อาจเห็น A ที่ไม่ได้เริ่มต้น หรือคอมไพเลอร์อาจตัดสินว่า Flag ไม่ได้ อาจมีการเปลี่ยนแปลงในระหว่างลูปของเทรด 2 และเปลี่ยนโปรแกรมเป็น
    ชุดข้อความที่ 1 ชุดข้อความที่ 2
    A = ...
      flag = true
    reg0 = Flag; ขณะที่ (!reg0) {}
    ... = A
    เมื่อคุณแก้ไขข้อบกพร่อง คุณอาจเห็นการวนซ้ำอย่างต่อเนื่อง ข้อเท็จจริงที่ว่า flag เป็นจริง
  2. C++ มีสิ่งอำนวยความสะดวกเพื่อการผ่อนคลายอย่างชัดเจน ความสอดคล้องกันตามลำดับแม้ว่าจะไม่มีการแข่งขัน การดำเนินการของปรมาณู สามารถรับอาร์กิวเมนต์ memory_order_... ที่ชัดเจน ในทำนองเดียวกัน แพ็กเกจ java.util.concurrent.atomic มีการจำกัดมากกว่า ชุดสิ่งอำนวยความสะดวกที่คล้ายกัน โดยเฉพาะ lazySet() และ Java โปรแกรมเมอร์บางครั้งจะใช้การแข่งขันด้านข้อมูลโดยเจตนาเพื่อผลลัพธ์ที่คล้ายกัน ทั้งหมดนี้ช่วยปรับปรุงประสิทธิภาพได้ และความซับซ้อนของการเขียนโปรแกรม เราจะพูดคุยกันสั้นๆ เท่านั้น ด้านล่าง
  3. โค้ด C และ C++ บางโค้ดเขียนในรูปแบบเก่า ไม่ใช่ทั้งหมด สอดคล้องกับมาตรฐานภาษาปัจจุบัน ซึ่งvolatile ใช้ตัวแปรแทนตัว atomic ตัว และลำดับหน่วยความจำ ไม่ได้รับอนุญาตอย่างชัดเจนโดยการแทรกที่เรียกว่า fences หรือ อุปสรรค การดำเนินการนี้ต้องการเหตุผลที่ชัดเจนเกี่ยวกับการเข้าถึง การจัดเรียงใหม่และทำความเข้าใจ รุ่นหน่วยความจำของฮาร์ดแวร์ รูปแบบการเขียนโค้ด ตลอดบรรทัดเหล่านี้ ยังคงใช้ในเคอร์เนลของ Linux ไม่ควร จะใช้ในแอปพลิเคชัน Android ใหม่หรือไม่ และมีการอธิบายเพิ่มเติมในที่นี้

ลองฝึก

การแก้ไขปัญหาความสอดคล้องกันของหน่วยความจำอาจเป็นเรื่องยากมาก หากไฟล์ขาดหายไป ล็อก, atomic หรือสาเหตุของการประกาศ volatile เพื่ออ่านข้อมูลที่ไม่มีอัปเดต คุณอาจไม่สามารถ หาสาเหตุโดยการตรวจสอบหน่วยความจำที่ดัมพ์ด้วยโปรแกรมแก้ไขข้อบกพร่อง พอคุณตอบ ออกข้อความค้นหาโปรแกรมแก้ไขข้อบกพร่อง แกน CPU อาจสังเกตการณ์ชุดเต็มของ และเนื้อหาของหน่วยความจำและการลงทะเบียน CPU จะปรากฏใน สถานะที่ "เป็นไปไม่ได้"

สิ่งที่ไม่ควรทำใน C

เราขอนำเสนอตัวอย่างบางส่วนของโค้ดที่ไม่ถูกต้อง พร้อมด้วยวิธีง่ายๆ ในการ แก้ไข แต่ก่อนจะเริ่ม เราต้องพูดถึงการใช้ภาษาพื้นฐาน

C/C++ และ "ผันผวน"

การประกาศ C และ C++ volatile เป็นเครื่องมือสำหรับวัตถุประสงค์พิเศษมากๆ เพื่อป้องกันไม่ให้คอมไพเลอร์เรียงลำดับใหม่หรือนำความผันผวนออก สิทธิ์การเข้าถึง ซึ่งจะเป็นประโยชน์สำหรับการลงทะเบียนอุปกรณ์ฮาร์ดแวร์ที่มีรหัสเข้าถึง หน่วยความจำถูกแมปกับสถานที่มากกว่าหนึ่งแห่ง หรือโดยเชื่อมโยงกับ setjmp แต่ C และ C++ volatile ต่างจาก Java volatile ไม่ได้ออกแบบมาสำหรับการสื่อสารในชุดข้อความ

ใน C และ C++ มีสิทธิ์เข้าถึง volatile อาจมีการจัดเรียงใหม่โดยใช้ข้อมูลที่ไม่เปลี่ยนแปลง และไม่มี ให้การรับประกันแบบ Atomicity ดังนั้นจึงไม่สามารถใช้ volatile เพื่อแชร์ข้อมูลระหว่าง เทรดในโค้ดแบบพกพา แม้ใน Uniprocessor ก็ตาม C volatile มักจะไม่ ป้องกันการเข้าถึงการจัดลําดับใหม่โดยฮาร์ดแวร์ ดังนั้นในตัวเองแล้วมันก็มีประโยชน์น้อยกว่า สภาพแวดล้อม SMP แบบหลายเธรด นี่คือเหตุผลที่ทำให้ C11 และ C++11 สนับสนุน ออบเจ็กต์ atomic รายการ ควรใช้แอตทริบิวต์เหล่านั้นแทน

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

ตัวอย่าง

ในกรณีส่วนใหญ่ คุณควรใช้การล็อก (เช่น pthread_mutex_t หรือ C++11 std::mutex) แทนที่จะเป็น ปฏิบัติการระดับปรมาณู แต่เราจะใช้แนวทางหลังในการแสดงให้เห็นว่า ในสถานการณ์จริง

MyThing* gGlobalThing = NULL;  // Wrong!  See below.
void initGlobalThing()    // runs in Thread 1
{
    MyStruct* thing = malloc(sizeof(*thing));
    memset(thing, 0, sizeof(*thing));
    thing->x = 5;
    thing->y = 10;
    /* initialization complete, publish */
    gGlobalThing = thing;
}
void useGlobalThing()    // runs in Thread 2
{
    if (gGlobalThing != NULL) {
        int i = gGlobalThing->x;    // could be 5, 0, or uninitialized data
        ...
    }
}

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

ปัญหาคืออาจสังเกตเห็นร้านค้าไปยัง gGlobalThing ก่อนที่จะเริ่มต้นฟิลด์ โดยทั่วไปเป็นเพราะคอมไพเลอร์หรือ ผู้ประมวลผลข้อมูลสั่งซื้อ Store ใหม่เป็น gGlobalThing และ thing->x มีชุดข้อความอื่นซึ่งอ่านจาก thing->x ได้ ดูข้อมูล 5, 0 หรือแม้แต่ข้อมูลที่ยังไม่ได้กำหนดค่า

ปัญหาหลักสำหรับที่นี่คือการแข่งขันเกี่ยวกับข้อมูลบน gGlobalThing หากเทรด 1 เรียกใช้ initGlobalThing() ขณะที่เทรด 2 โทรหา useGlobalThing(), gGlobalThing อ่านได้ขณะเขียน

แก้ไขได้โดยการประกาศ gGlobalThing เป็น อะตอม ใน C++11:

atomic<MyThing*> gGlobalThing(NULL);

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

สิ่งที่ไม่ควรทำใน Java

เรายังไม่ได้พูดถึงคุณลักษณะบางอย่างที่เกี่ยวข้องกับภาษา Java ดังนั้นเราจะ ให้ดูอย่างรวดเร็วเหล่านั้นก่อน

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

สำหรับตอนนี้ เราจะยึดตามรูปแบบการไม่มีการแข่งขันด้านข้อมูล ซึ่ง Java ให้ ซึ่งเป็นการรับประกันแบบเดียวกับ C และ C++ ขอย้ำอีกครั้งว่าภาษานี้มี คำพื้นฐานบางส่วนที่ผ่อนคลายความต่อเนื่องตามลำดับอย่างชัดเจน โดยเฉพาะอย่างยิ่ง lazySet() และ weakCompareAndSet() สาย ใน java.util.concurrent.atomic แต่ในตอนนี้เราจะยังไม่สนใจตัวแปรเหล่านี้เช่นเดียวกับ C และ C++

"ซิงค์" ของ Java และ "ผันผวน" คีย์เวิร์ด

คีย์เวิร์ดที่ "ซิงค์แล้ว" จะให้การล็อกในตัวของภาษา Java Google Analytics ออบเจ็กต์ทุกรายการมี "ตรวจสอบ" เชื่อมโยงอยู่ซึ่งใช้เพื่อระบุ การเข้าถึงแบบเฉพาะร่วมกัน หากชุดข้อความ 2 รายการพยายาม "ซิงค์ข้อมูล" ใน ออบเจ็กต์เดียวกัน รายการหนึ่งจะรอจนกว่ารายการอื่นเสร็จสมบูรณ์

ตามที่เรากล่าวไปแล้ว volatile T ของ Java เป็นแบบแอนะล็อกของ atomic<T> ของ C++11 การเข้าถึงพร้อมกัน อนุญาต volatile ฟิลด์ และไม่ส่งผลให้มีการแข่งขันข้อมูล ละเว้น lazySet() และคณะ และการแข่งขันข้อมูลเป็นหน้าที่ของ Java VM ในการ ให้ตรวจสอบว่าผลลัพธ์ยังคงปรากฏตามลำดับ

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

มีข้อแตกต่างสำคัญอย่างหนึ่งจาก atomic ของ C++ ได้แก่ หากเราเขียน volatile int x; ใน Java ค่า x++ จะเหมือนกับ x = x + 1 รายการดังกล่าว ทำการโหลดอะตอม เพิ่มผลลัพธ์ แล้วทำเป็นอะตอม จำนวนที่เพิ่มขึ้นโดยรวมไม่ใช่ระดับอะตอม ซึ่งต่างจาก C++ การดำเนินการที่เพิ่มขึ้นแบบอะตอมได้มาจาก java.util.concurrent.atomic

ตัวอย่าง

การใช้งานตัวนับโมโนนิกอย่างง่ายและไม่ถูกต้อง มีดังนี้ (Java ทฤษฎีและแนวทางปฏิบัติ: การจัดการความผันผวน)

class Counter {
    private int mValue;
    public int get() {
        return mValue;
    }
    public void incr() {
        mValue++;
    }
}

สมมติว่า get() และ incr() ถูกเรียกจากหลายรายการ และเราต้องการมั่นใจว่าทุกชุดข้อความจะเห็นจำนวนปัจจุบันเมื่อ โทรหา get() ปัญหาที่ชัดเจนที่สุดคือ mValue++ จริงๆ แล้วเป็นการดำเนินการ 3 อย่าง ได้แก่

  1. reg = mValue
  2. reg = reg + 1
  3. mValue = reg

หากเทรด 2 รายการดำเนินการใน incr() พร้อมกัน ชุดใดชุดหนึ่ง การอัปเดตอาจสูญหายได้ หากต้องการเพิ่มจำนวนอะตอม เราต้องประกาศ incr() "ซิงค์แล้ว"

อย่างไรก็ตาม ก็ยังใช้งานไม่ได้อยู่ดี โดยเฉพาะใน SMP ยังคงมีการแข่งขันด้านข้อมูล ใน get() นั้นจะเข้าถึง mValue พร้อมกันได้ incr() ภายใต้กฎ Java คุณจะเรียกใช้ get() ได้ ถูกเรียงลำดับใหม่เมื่อเทียบกับโค้ดอื่นๆ เช่น ถ้าเราอ่าน ตัวนับในแถว ผลลัพธ์อาจไม่สอดคล้องกัน เพราะการโทร get() ที่เราจัดเรียงใหม่ ไม่ว่าจะด้วยฮาร์ดแวร์หรือ คอมไพเลอร์ เราแก้ไขปัญหาได้โดยประกาศว่า get() เป็น ทำให้ข้อมูลตรงกันแล้ว การเปลี่ยนแปลงครั้งนี้ถือว่าโค้ดถูกต้อง

น่าเสียดายที่เราได้เปิดตัวความเป็นไปได้ในการช่วงชิงล็อก ซึ่ง อาจขัดขวางประสิทธิภาพได้ แทนที่จะประกาศว่า get() เป็น ทำให้ตรงกันแล้ว เราอาจประกาศ mValue ที่มี “ความผันผวน” (หมายเหตุ incr() ยังคงต้องใช้ synchronize ตั้งแต่ แต่ mValue++ ก็ไม่ใช่เลขอะตอมเดียว) การดำเนินการนี้จะหลีกเลี่ยงการแข่งขันข้อมูลทั้งหมดเพื่อให้คงความสอดคล้องกันตามลำดับไว้ incr() จะค่อนข้างช้ากว่า เนื่องจากต้องทั้งเข้า/ออกของหน้าจอ ค่าใช้จ่ายในการดำเนินการ และค่าใช้จ่ายที่เชื่อมโยงกับร้านค้าที่ผันผวน แต่ get() จะสามารถดำเนินการได้เร็วกว่า ดังนั้นหากไม่มีการโต้แย้ง จะชนะหากอ่านได้มากกว่าการเขียนอย่างมาก (โปรดดู AtomicInteger เพื่อดูวิธี นำการบล็อกที่ซิงค์ไว้ออก)

ต่อไปนี้เป็นอีกตัวอย่างหนึ่งซึ่งมีลักษณะคล้ายกับตัวอย่าง C ก่อนหน้านี้

class MyGoodies {
    public int x, y;
}
class MyClass {
    static MyGoodies sGoodies;
    void initGoodies() {    // runs in thread 1
        MyGoodies goods = new MyGoodies();
        goods.x = 5;
        goods.y = 10;
        sGoodies = goods;
    }
    void useGoodies() {    // runs in thread 2
        if (sGoodies != null) {
            int i = sGoodies.x;    // could be 5 or 0
            ....
        }
    }
}

สิ่งนี้มีปัญหาเดียวกับโค้ด C กล่าวคือมี การแข่งขันด้านข้อมูลเมื่อวันที่ sGoodies ดังนั้นงาน sGoodies = goods อาจสังเกตเห็นได้ก่อนการเริ่มต้น ใน goods หากคุณประกาศ sGoodies ด้วยเมธอด คีย์เวิร์ด volatile รายการ ความสอดคล้องตามลำดับได้รับการกู้คืน แล้วทุกอย่างจะกลับมาทำงานตามปกติ ตามที่คาดไว้

โปรดทราบว่าเฉพาะการอ้างอิง sGoodies เท่านั้นที่มีความผันผวน แต่การเข้าถึงช่องข้อมูลเหล่านั้นกลับ ไม่สามารถเข้าถึงได้ เมื่อ sGoodies มีมูลค่า volatile และการจัดเรียงหน่วยความจำจะได้รับการเก็บรักษาอย่างเหมาะสม ไม่สามารถเข้าถึงพร้อมกันได้ คำสั่ง z = sGoodies.x จะดำเนินการโหลดที่มีความผันผวน MyClass.sGoodies ตามด้วยการโหลดที่ไม่ผันผวนของ sGoodies.x ถ้าคุณทำให้เป็น อ้างอิง MyGoodies localGoods = sGoodies แล้ว z = localGoods.x ที่ตามมาจะไม่แสดงการโหลดที่มีความผันผวน

สำนวนที่ใช้กันโดยทั่วไปในการเขียนโปรแกรม Java คือ "การตรวจสอบซ้ำสองเท่า Locking":

class MyClass {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized (this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }
}

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

รายการนี้มีการแข่งขันข้อมูลในช่อง helper สามารถทำได้ ตั้งค่าพร้อมกันกับ helper == null ในชุดข้อความอื่น

หากต้องการดูว่าวิธีนี้จะล้มเหลวได้อย่างไร ให้พิจารณา โค้ดที่เขียนขึ้นใหม่เล็กน้อย ให้เหมือนกับโค้ดที่คอมไพล์เป็นภาษา C-like (ฉันได้เพิ่มช่องที่เป็นจำนวนเต็ม 2 ช่องเพื่อแสดงแทน Helper’s กิจกรรมของเครื่องมือสร้าง)

if (helper == null) {
    synchronized() {
        if (helper == null) {
            newHelper = malloc(sizeof(Helper));
            newHelper->x = 5;
            newHelper->y = 10;
            helper = newHelper;
        }
    }
    return helper;
}

ไม่มีอะไรป้องกันทั้งฮาร์ดแวร์หรือคอมไพเลอร์ ตั้งแต่การเรียงลำดับร้านค้าเป็น helper ที่มีรายการเหล่านั้นไปยัง x/y ช่อง มีชุดข้อความอื่น helper ไม่ใช่ค่าว่าง แต่ยังไม่ได้ตั้งค่าช่องและพร้อมใช้งาน สำหรับรายละเอียดเพิ่มเติมและโหมดการทำงานล้มเหลวอื่นๆ โปรดดู" ลิงก์ "การประกาศว่า "ล็อกไม่ได้" ในภาคผนวกสำหรับรายละเอียดเพิ่มเติมหรือรายการ 71 ("ใช้การเริ่มต้นแบบ Lazy Loading อย่างเหมาะสม") ใน Effective Java, ของ Josh Bloch ฉบับที่ 2

มี 2 วิธีในการแก้ไขปัญหานี้ ได้แก่

  1. ก็ทำได้ง่ายๆ แล้วลบการตรวจสอบด้านนอกออก ซึ่งช่วยให้เราไม่มีทาง ตรวจสอบค่าของ helper นอกบล็อกที่ซิงค์
  2. ประกาศความผันผวนใน helper เพียงทำการเปลี่ยนแปลงเล็กน้อยเพียงครั้งเดียว ในตัวอย่าง J-3 จะทำงานอย่างถูกต้องบน Java 1.5 และรุ่นใหม่กว่า (คุณอาจต้องพิจารณา เพื่อโน้มน้าวตัวเองว่านี่เป็นเรื่องจริง)

นี่เป็นอีกภาพหนึ่งของลักษณะการทำงาน volatile

class MyClass {
    int data1, data2;
    volatile int vol1, vol2;
    void setValues() {    // runs in Thread 1
        data1 = 1;
        vol1 = 2;
        data2 = 3;
    }
    void useValues() {    // runs in Thread 2
        if (vol1 == 2) {
            int l1 = data1;    // okay
            int l2 = data2;    // wrong
        }
    }
}

เรากำลังตรวจสอบ useValues() หากเทรดที่ 2 ยังไม่พบ อัปเดตเป็น vol1 แล้ว จะไม่รู้ว่า data1 หรือ ตั้งค่า data2 แล้ว เมื่อเห็นการอัปเดต vol1 อุปกรณ์รู้ว่า data1 จะเข้าถึงได้อย่างปลอดภัย และอ่านได้อย่างถูกต้องโดยไม่ต้องแข่งข้อมูล อย่างไรก็ตาม ระบบไม่สามารถตั้งสมมติฐานเกี่ยวกับ data2 ได้เนื่องจากร้านค้านั้น ดำเนินการหลังร้านที่ผันผวน

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

สิ่งที่ต้องทำ

ใน C/C++ ต้องการใช้ C++11 คลาสการซิงค์ เช่น std::mutex หากไม่ ให้ใช้ การดำเนินการ pthread ที่เกี่ยวข้อง ซึ่งรวมถึงกรอบหน่วยความจำที่เหมาะสม การให้เนื้อหาที่ถูกต้อง (สอดคล้องกันตามลำดับ เว้นแต่จะระบุไว้เป็นอย่างอื่น) และลักษณะการทำงานที่มีประสิทธิภาพบนแพลตฟอร์ม Android ทุกเวอร์ชัน อย่าลืมใช้โซลูชันเหล่านั้น อย่างถูกต้อง ตัวอย่างเช่น อย่าลืมว่าตัวแปรเงื่อนไข "รอ" อาจเป็นการปลอมตัว กลับมาโดยไม่มีการส่งสัญญาณ และควรปรากฏขึ้นแบบวนซ้ำ

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

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

หลีกเลี่ยงการใช้ volatile สำหรับการสื่อสารของชุดข้อความใน C/C++

ใน Java ปัญหาการเกิดขึ้นพร้อมกันมักจะแก้ไขได้ดีที่สุดโดย โดยใช้คลาสยูทิลิตีที่เหมาะสมจาก แพ็กเกจ java.util.concurrent โค้ดเขียนได้ดีและมีคุณภาพ ใน SMP

บางทีวิธีที่ปลอดภัยที่สุดที่คุณสามารถทำได้ก็คือการทำให้วัตถุเปลี่ยนแปลงไม่ได้ วัตถุ จากคลาส เช่น ข้อมูลการคงไว้ชั่วคราวของสตริงและจำนวนเต็มของ Java ซึ่งเปลี่ยนแปลงไม่ได้เพียงครั้งเดียว จะปรากฏขึ้น โดยหลีกเลี่ยงโอกาสที่จะเกิดการแข่งขันด้านข้อมูลกับออบเจ็กต์เหล่านั้น หนังสือ มีประสิทธิภาพ Java, รุ่นที่ 2 มีวิธีการเฉพาะใน "รายการที่ 15: ลดความสามารถในการเปลี่ยนแปลง" หมายเหตุใน ความสำคัญของการประกาศช่อง Java เป็น "ขั้นสุดท้าย" (Bloch)

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

หากไม่ใช่ทั้งคลาสไลบรารีที่มีอยู่ และคลาสที่เปลี่ยนแปลงไม่ได้ คำสั่ง synchronized หรือ C++ ของ Java ควรใช้ lock_guard / unique_lock เพื่อป้องกัน เข้าถึงฟิลด์ที่สามารถเข้าถึงได้มากกว่า 1 ชุดข้อความ หาก Mutex ไม่เปิด เหมาะกับสถานการณ์ของคุณ คุณควรประกาศฟิลด์ที่ใช้ร่วมกัน volatileหรือatomic แต่คุณต้องใช้ความระมัดระวังอย่างยิ่งเพื่อ ทำความเข้าใจการโต้ตอบระหว่างชุดข้อความ การประกาศเหล่านี้จะไม่ ช่วยคุณประหยัดจากความผิดพลาดในการเขียนโปรแกรมที่พบได้บ่อย แต่สิ่งเหล่านี้จะช่วยคุณ หลีกเลี่ยงความล้มเหลวลึกลับที่เกี่ยวข้องกับการเพิ่มประสิทธิภาพคอมไพเลอร์และ SMP อุบัติเหตุ

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

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

C++11 และรุ่นที่ใหม่กว่ามีกลไกที่ชัดเจนสำหรับการผ่อนคลายลำดับ รับประกันความสอดคล้องสำหรับโปรแกรมปลอดการแข่งขันข้อมูล อาจไม่เหมาะสม memory_order_relaxed, memory_order_acquire (โหลด เท่านั้น) และ memory_order_release(จัดเก็บเท่านั้น) สำหรับอาร์กิวเมนต์อะตอม แต่ละรายการจะให้การรับประกันที่อ่อนกว่า โดยนัย memory_order_seq_cst memory_order_acq_rel ให้ทั้ง memory_order_acquire และ memory_order_release รับประกันการเขียนแบบอะตอมมิกอ่านและเขียน การดำเนินงาน memory_order_consumeยังไม่เพียงพอ ระบุไว้เป็นอย่างดีหรือนำไปใช้ให้เป็นประโยชน์ และยังไม่ต้องสนใจในตอนนี้

เมธอด lazySet ใน Java.util.concurrent.atomic คล้ายกับร้านค้า C++ memory_order_release ของ Java ในบางครั้ง ตัวแปรทั่วไปถูกใช้แทนที่ สิทธิ์การเข้าถึงของ memory_order_relaxed แม้ว่าความจริงแล้วจะเป็น ประสิทธิภาพไม่ดีนัก ไม่มีกลไกที่แท้จริงสำหรับการจัดเรียงคำสั่ง ซึ่งต่างจาก C++ เข้าถึงตัวแปรที่ประกาศเป็น volatile

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

อรรถศาสตร์ทั้งหมดของแอตโทมิกที่มีลำดับต่ำนั้นซับซ้อน โดยทั่วไป โซลูชันเหล่านี้ต้องการ กฎภาษาอย่างแม่นยำ ซึ่งเราจะ ไม่เข้าไปในนี้ เช่น

  • คอมไพเลอร์หรือฮาร์ดแวร์ย้าย memory_order_relaxed ได้ เข้าถึง (แต่ไม่ใช่) ส่วนสำคัญที่ล้อมรอบด้วยล็อก การได้ผู้ใช้ใหม่และการเปิดตัว ซึ่งหมายความว่า ร้านค้า memory_order_relaxed แห่งอาจแสดงผลไม่เป็นระเบียบ แม้จะถูกคั่นด้วยส่วนสำคัญ
  • ตัวแปร Java ทั่วไปอาจปรากฏขึ้นเมื่อมีการละเมิดเป็นตัวนับที่แชร์ ไปยังชุดข้อความอื่นเพื่อลดจำนวนลง แม้ว่าจะเพิ่มขึ้นเพียงบรรทัดเดียว ชุดข้อความอื่น ซึ่งไม่เป็นจริงสำหรับ C++ อะตอม memory_order_relaxed

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

การเข้าถึงที่ไม่ใช่การแข่งรถ

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

ไม่มีแอนะล็อกแบบนี้จริงๆ ใน Java

ผลลัพธ์ไม่ได้อ้างอิงความถูกต้อง

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

ทั่วไป ของตัวอย่างนี้คือการใช้ C++ compare_exchange เพื่อแทนที่ x ด้วย f(x) ในระดับอะตอม โหลดเริ่มต้นของ x เพื่อประมวลผล f(x) ไม่จำเป็นต้องมีความน่าเชื่อถือ ถ้าเราเข้าใจผิด compare_exchange จะล้มเหลวและเราจะลองอีกครั้ง สามารถใช้การโหลดเริ่มต้นของ x ได้ อาร์กิวเมนต์ memory_order_relaxed การเรียงลำดับหน่วยความจำเท่านั้น สำหรับ compare_exchange กรณีจริง

ข้อมูลที่แก้ไขโดยอัตโนมัติแต่ยังไม่อ่าน

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

ในกรณีนี้ ก็ไม่มีวิธีที่จะบอกว่าสิทธิ์เข้าถึงข้อมูลนี้ มีการจัดเรียงใหม่ ดังนั้นโค้ด C++ อาจใช้ memory_order_relaxed อาร์กิวเมนต์

ตัวนับเหตุการณ์แบบง่ายเป็นตัวอย่างที่พบได้บ่อย เนื่องจาก จึงควรตั้งข้อสังเกตบางอย่างเกี่ยวกับกรณีนี้ไว้

  • การใช้ memory_order_relaxed จะช่วยเพิ่มประสิทธิภาพ แต่อาจไม่ได้ช่วยแก้ปัญหาด้านประสิทธิภาพที่สำคัญที่สุด: การอัปเดตทุกครั้ง ต้องมีสิทธิ์พิเศษในการเข้าถึงบรรทัดแคชที่ใช้ตัวนับ ช่วงเวลานี้ ทำให้แคชหายไปทุกครั้งที่ชุดข้อความใหม่เข้าถึงตัวนับ หากอัปเดตบ่อยและสลับไปมาระหว่างชุดข้อความ ก็จะทำให้เร็วขึ้นมาก เพื่อหลีกเลี่ยงการอัปเดตตัวนับที่แชร์ทุกครั้ง เช่น การใช้ตัวนับภายในเทรดและสรุปผลลัพธ์ในตอนท้าย
  • เทคนิคนี้สามารถใช้ร่วมกับส่วนก่อนหน้า: อ่านค่าโดยประมาณและค่าที่ไม่น่าเชื่อถือพร้อมกันขณะอัปเดต กับการดำเนินการทั้งหมดที่ใช้ memory_order_relaxed แต่สิ่งสำคัญคือต้องทำให้ค่าที่ได้ไม่น่าเชื่อถือโดยสิ้นเชิง เพียงเพราะว่าจำนวนการเพิ่มขึ้นได้นับไม่ หมายความว่าระบบจะนับชุดข้อความอื่นว่าถึงจุดนั้นแล้ว ที่มีการดำเนินการเพิ่มขึ้นแล้ว จำนวนที่เพิ่มขึ้นอาจมี สั่งซื้ออีกครั้งด้วยรหัสก่อนหน้านี้ (สำหรับกรณีที่คล้ายกันที่เรากล่าวถึง ก่อนหน้านี้ C++ จะรับประกันว่าการโหลดครั้งที่สองของตัวนับจะไม่ แสดงผลค่าน้อยกว่าการโหลดก่อนหน้านี้ในชุดข้อความเดียวกัน ยกเว้นในกรณีต่อไปนี้ ตัวนับล้น)
  • เป็นเรื่องปกติที่จะพบโค้ดที่พยายามคำนวณค่าประมาณ ด้วยการดำเนินการอ่านแบบอะตอม (หรือไม่) แต่ละแบบ แต่ ไม่ได้ทำให้การเพิ่มขึ้นเป็นอะตอมทั้งหมด อาร์กิวเมนต์ตามปกติคือ นี่ "ใกล้พอแล้ว" สำหรับตัวนับประสิทธิภาพหรือที่คล้ายกัน ซึ่งปกติแล้วจะไม่เป็นเช่นนั้น เมื่อการอัปเดตบ่อยครั้งเพียงพอ (กรณี ก็อาจจะสนใจ) ส่วนใหญ่แล้ว จำนวนส่วนใหญ่มักเป็น แพ้ ในอุปกรณ์แบบ Quad Core จำนวนมากกว่าครึ่งหนึ่งอาจสูญหายได้ (แบบฝึกหัดที่ง่ายดาย: สร้างสถานการณ์เทรด 2 แบบโดยที่ตัวนับ อัปเดต 1 ล้านครั้ง แต่ค่าตัวนับสุดท้ายเท่ากับ 1)

การสื่อสารด้วย Flag ง่ายๆ

Store memory_order_release (หรือการดำเนินการอ่าน - แก้ไข - เขียน) ช่วยให้มั่นใจว่าหากการโหลด memory_order_acquire ในภายหลัง (หรือ Read-modify-Write) จะอ่านค่าที่เขียนแล้ว จากนั้นจะ และสังเกตร้านค้า (แบบปกติหรือแบบอะตอม) ที่นำหน้า ร้านค้า memory_order_release ในทางกลับกัน โหลดใดๆ ที่อยู่ก่อนหน้า memory_order_release จะไม่สังเกตเห็น ร้านค้าที่ติดตามการโหลด memory_order_acquire สิ่งที่ต่างจาก memory_order_relaxedคือจะอนุญาตให้ดำเนินการจากอะตอมดังกล่าว เพื่อใช้ในการสื่อสารความคืบหน้าของชุดข้อความหนึ่งไปยังอีกชุดหนึ่ง

ตัวอย่างเช่น เราสามารถเขียนตัวอย่างการล็อกที่ตรวจสอบอีกครั้งแล้ว จากด้านบนใน C++ เป็น

class MyClass {
  private:
    atomic<Helper*> helper {nullptr};
    mutex mtx;
  public:
    Helper* getHelper() {
      Helper* myHelper = helper.load(memory_order_acquire);
      if (myHelper == nullptr) {
        lock_guard<mutex> lg(mtx);
        myHelper = helper.load(memory_order_relaxed);
        if (myHelper == nullptr) {
          myHelper = new Helper();
          helper.store(myHelper, memory_order_release);
        }
      }
      return myHelper;
    }
};

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

โปรแกรมเมอร์ Java อาจมองว่า helper เป็น วันที่ java.util.concurrent.atomic.AtomicReference<Helper> และใช้ lazySet() เป็นบันทึกประจำรุ่น โหลด จะยังคงใช้การเรียก get() แบบธรรมดาต่อไป

ในทั้ง 2 กรณี การปรับเปลี่ยนประสิทธิภาพของเราจะมุ่งเน้นที่การเริ่มต้น ซึ่งคงไม่น่าจะมีความสำคัญอย่างยิ่งต่อประสิทธิภาพ การเจาะระบบที่อ่านง่ายกว่าอาจมีลักษณะดังต่อไปนี้

    Helper* getHelper() {
      Helper* myHelper = helper.load(memory_order_acquire);
      if (myHelper != nullptr) {
        return myHelper;
      }
      lock_guard&ltmutex> lg(mtx);
      if (helper == nullptr) {
        helper = new Helper();
      }
      return helper;
    }

ซึ่งมีเส้นทางที่รวดเร็วเหมือนกัน แต่ใช้ค่าเริ่มต้น การดำเนินการบนที่ช้าที่ไม่สำคัญซึ่งไม่เกี่ยวข้องกับประสิทธิภาพอย่างเป็นลำดับต่อเนื่อง เส้นทาง

ถึงอย่างนี้ helper.load(memory_order_acquire) ยัง มีแนวโน้มที่จะสร้างรหัสเดียวกันบนอุปกรณ์ที่รองรับ Android เวอร์ชันปัจจุบัน เป็นการอ้างอิงแบบธรรมดา (สอดคล้องกันตามลำดับ) ไปยัง helper การเพิ่มประสิทธิภาพที่มีประโยชน์มากที่สุดจริงๆ ที่นี่ อาจเป็นการแนะนำ myHelper เพื่อกำจัด แม้ว่าคอมไพเลอร์ในอนาคตอาจโหลดขึ้นโดยอัตโนมัติก็ตาม

การสั่งซื้อเพื่อรับ/เผยแพร่ไม่ได้ขัดขวางการแสดงร้านค้า ล่าช้า และไม่รับประกันว่าร้านค้าจะปรากฏให้ชุดข้อความอื่นๆ เห็น ตามลำดับที่สม่ำเสมอ ด้วยเหตุนี้ จึงไม่รองรับแท็ก รูปแบบการเขียนโค้ดที่ค่อนข้างพบได้บ่อย ซึ่งเห็นได้จากการที่ Dekker แยกออกจากกัน อัลกอริทึม: เทรดทั้งหมดจะได้รับการตั้งค่า Flag ก่อนเพื่อแสดงว่าพวกเขาต้องการทำ บางสิ่ง หากชุดข้อความ t สังเกตเห็นว่าไม่มีชุดข้อความอื่น จะพยายามทำอะไรสักอย่าง ก็จะดำเนินต่อไปได้อย่างปลอดภัย เมื่อรู้ว่า จะไม่ถูกรบกวน จะไม่มีชุดข้อความอื่น สามารถดำเนินการต่อได้ เนื่องจากยังคงมีการตั้งค่าแฟล็กของ t ดำเนินการไม่สำเร็จ หากมีการเข้าถึงแฟล็กโดยใช้ลำดับการได้มา/การเผยแพร่ เนื่องจากไม่ใช่ ป้องกันไม่ให้ผู้อื่นเห็นธงของชุดข้อความหลังจากที่ผู้ใช้ ดำเนินการผิดพลาด memory_order_seq_cst เริ่มต้น จะช่วยป้องกันสิ่งนั้น

ช่องที่เปลี่ยนแปลงไม่ได้

หากช่องออบเจ็กต์เริ่มต้นการใช้งานครั้งแรกแล้วไม่มีการเปลี่ยนแปลง อาจเป็นไปได้ที่จะเริ่ม เปิดและอ่านในภายหลังโดยใช้อย่างไม่มีประสิทธิภาพ การเข้าถึงที่เรียงลำดับ ใน C++ อาจได้รับการประกาศเป็น atomic และเข้าถึงโดยใช้ memory_order_relaxed หรือใน Java สามารถประกาศโดยไม่มี volatile และเข้าถึงโดยไม่มี มาตรการพิเศษต่างๆ ซึ่งกำหนดให้มีการระงับต่อไปนี้ทั้งหมด

  • สามารถระบุค่าจากช่องนี้ได้ เริ่มต้นแล้วหรือยัง วิธีเข้าถึงช่องนี้ ค่าการทดสอบและส่งกลับเส้นทางที่รวดเร็วควรจะอ่านค่าในฟิลด์เพียงครั้งเดียวเท่านั้น ใน Java ปัจจัยหลังเป็นสิ่งสำคัญ แม้ว่าการทดสอบภาคสนามจะเริ่มต้น การโหลดครั้งที่ 2 อาจอ่านค่าที่ยังไม่ได้กำหนดค่าเริ่มต้นก่อนหน้านี้ ใน C++ "อ่านครั้งเดียว" กฎเกณฑ์ก็เป็นเพียงการปฏิบัติที่ดี
  • ทั้งการเริ่มต้นและการโหลดครั้งต่อๆ ไปต้องเป็นแบบอะตอม ในการอัปเดตบางส่วนนั้นไม่ควรมองเห็นได้ สำหรับ Java ฟิลด์ ไม่ควรเป็น long หรือ double สำหรับ C++ ต้องมีการกำหนดอะตอม สร้างไว้แบบเดิมๆ ก็ใช้ไม่ได้เพราะ การสร้าง atomic ไม่ใช่แบบอะตอม
  • การเริ่มต้นซ้ำต้องปลอดภัย เนื่องจากเทรดหลายรายการ อาจอ่านค่าที่ยังไม่ได้เริ่มต้นพร้อมกัน ใน C++ โดยทั่วไปแล้ว ดังต่อไปนี้จากหัวข้อ "การคัดลอกได้ในบางครั้ง" ที่บังคับใช้กับ ประเภทของอะตอม ประเภทที่มีตัวชี้ที่เป็นเจ้าของที่ฝังไว้ การซื้อขายสินค้าใน เครื่องมือสร้างข้อความโฆษณา และ ไม่ควรคัดลอกเนื้อหามากเกินไป สำหรับ Java ประเภทข้อมูลอ้างอิงที่ยอมรับได้มีดังนี้
  • การอ้างอิง Java จำกัดเฉพาะประเภทที่เปลี่ยนแปลงไม่ได้ซึ่งมีเฉพาะรูปแบบสุดท้าย ด้วย ตัวสร้างของประเภทที่เปลี่ยนแปลงไม่ได้ไม่ควรเผยแพร่ การอ้างอิงไปยังออบเจ็กต์ ในกรณีนี้ กฎฟิลด์สุดท้ายของ Java หากผู้อ่านเห็นข้อมูลอ้างอิง ผู้อ่านจะเห็น ฟิลด์สุดท้ายที่สร้างขึ้น C++ ไม่มีการคล้ายกับกฎเหล่านี้และ ตัวชี้ไปยังวัตถุที่เป็นเจ้าของนั้นเป็นสิ่งที่ไม่สามารถยอมรับได้ด้วยเหตุผลนี้เช่นกัน (ใน นอกเหนือจากการละเมิด )

หมายเหตุปิดท้าย

แม้ว่าเอกสารฉบับนี้จะทำได้มากกว่าเพียงแค่รอยขีดข่วนบนพื้นผิวเท่านั้น จัดการได้มากกว่าการเจาะเลือดแบบตื้นๆ นี่เป็นหัวข้อที่กว้างและลึกมาก ใช้บ้าง ที่ควรทราบเพิ่มเติม

  • โมเดลหน่วยความจำ Java และ C++ จริงแสดงในรูปแบบ เกิดขึ้น-ก่อน ความสัมพันธ์ที่ระบุเมื่อมีการรับประกันการดำเนินการ 2 รายการ ให้เกิดขึ้นตามลำดับ ตอนที่เรากำหนดการแข่งขันด้านข้อมูล เรา เราพูดถึงการเข้าถึงหน่วยความจำ 2 รายการที่เกิดขึ้น "พร้อมกัน" ซึ่งอย่างเป็นทางการหมายถึงการที่ทั้ง 2 ฝ่ายไม่ได้เกิดขึ้นก่อนอีกฝ่ายหนึ่ง ซึ่งจะช่วยให้เรียนรู้คำจำกัดความที่แท้จริงของเกิดขึ้นก่อนได้ และ synsyncs-with ใน Java หรือ C++ Memory Model แม้ว่าความคิดที่สัญชาตญาณว่า "พร้อมๆ กัน" ดีโดยทั่วไป คำจำกัดความเหล่านี้มีประโยชน์มาก โดยเฉพาะอย่างยิ่งหากคุณ กำลังพิจารณาที่จะใช้การดำเนินการแบบอะตอมที่มีเรียงลำดับอย่างอ่อนใน C++ (ข้อกำหนดเฉพาะของ Java ปัจจุบันระบุเฉพาะ lazySet() อย่างไม่เป็นทางการ)
  • สำรวจว่าคอมไพเลอร์คืออะไรและไม่อนุญาตให้ทำเมื่อเรียงลำดับโค้ดใหม่ (ข้อกำหนดของ JSR-133 มีตัวอย่างที่ดีส่วนหนึ่งของการเปลี่ยนแปลงทางกฎหมายที่นำไปสู่ ผลลัพธ์ที่ไม่คาดคิด)
  • ดูวิธีเขียนชั้นเรียนที่เปลี่ยนแปลงไม่ได้ใน Java และ C++ (นอกจากนี้ ยังมีสิ่งต่างๆ มากมาย ไม่ใช่แค่ “ไม่มีการเปลี่ยนแปลงอะไรหลังการก่อสร้าง”)
  • ดำเนินการตามคำแนะนำในส่วนการเกิดขึ้นพร้อมกันของหัวข้อ Effective Java รุ่นที่ 2 (ตัวอย่างเช่น คุณควรหลีกเลี่ยงวิธีการเรียกใช้ที่ ถูกลบล้างขณะอยู่ในบล็อกที่ซิงค์)
  • อ่าน API ของ java.util.concurrent และ java.util.concurrent.atomic เพื่อดูฟีเจอร์ที่พร้อมใช้งาน ลองใช้ คำอธิบายประกอบการเกิดขึ้นพร้อมกัน เช่น @ThreadSafe และ @GuardedBy (จาก net.jcip.annotations)

ส่วนอ่านเพิ่มเติมในภาคผนวกมีลิงก์ไปยัง และเอกสารต่างๆ ในเว็บไซต์ซึ่งจะช่วยทำให้เห็นหัวข้อเหล่านี้ได้ดีขึ้น

ภาคผนวก

การใช้ที่เก็บการซิงค์ข้อมูล

(โปรแกรมเมอร์ส่วนใหญ่มักพบว่าตนเองไม่ได้ใช้สิ่งนี้ แต่การพูดคุยก็คล่องแคล่ว)

สำหรับประเภทในตัวขนาดเล็ก เช่น int และฮาร์ดแวร์ที่ Google รองรับ Android, วิธีการโหลดแบบปกติ และการจัดเก็บช่วยให้มั่นใจได้ว่าร้านค้า จะปรากฏให้เห็นทั้งหมดหรือไม่แสดงเลย โหลดตำแหน่งเดียวกัน ดังนั้นด้วยแนวคิดพื้นฐาน ของ "atomicity" ให้บริการฟรี

อย่างที่เราเห็นก่อนหน้านี้ แต่นั่นไม่เพียงพอ เพื่อเป็นการตรวจสอบว่าเป็นไปตามลำดับ ความสอดคล้องเรายังต้องป้องกันไม่ให้มีการเรียงลำดับใหม่ และเพื่อให้มั่นใจว่า กระบวนการหน่วยความจำจะปรากฏต่อกระบวนการอื่นๆ ภายใน คำสั่งซื้อ ดูเหมือนว่าเครื่องหลังจะรองรับ Android โดยอัตโนมัติ ในกรณีที่เราตัดสินใจอย่างรอบคอบในการบังคับใช้ เลยไม่ได้สนใจตรงนี้

ลำดับของการดำเนินการหน่วยความจำจะยังคงอยู่โดยการป้องกันการเรียงลำดับใหม่ โดยคอมไพเลอร์และป้องกันการเรียงลำดับใหม่ด้วยฮาร์ดแวร์ เราโฟกัสที่จุดนี้ ในภายหลัง

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

การรับประกันการสั่งซื้อขั้นพื้นฐานที่สุดคือให้บริการโดย C++ memory_order_acquire และ memory_order_release การดำเนินการระดับอะตอม: การดำเนินการกับหน่วยความจำก่อนที่เก็บการเผยแพร่ ควรมองเห็นได้หลังจากโหลดที่ได้รับมา ใน ARMv7 นี่คือ บังคับใช้โดย:

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

ข้อมูลทั้งหมดเหล่านี้เพียงพอสำหรับการสั่งซื้อ/เผยแพร่ใน C++ เนื่องจากเป็นสิ่งที่จำเป็น แต่ไม่เพียงพอสำหรับ Java volatile หรือ C++ สอดคล้องกันตามลำดับ atomic

หากต้องการดูว่าเราต้องใช้อะไรอีกบ้าง ลองพิจารณาส่วนย่อยของอัลกอริทึมของ Dekker ที่เราได้พูดถึงไปคร่าวๆ ก่อนหน้านี้ flag1 และ flag2 เป็น C++ atomic หรือ Java volatile ตัวแปรทั้งคู่เป็น "เท็จ" ในตอนแรก

ชุดข้อความที่ 1 ชุดข้อความที่ 2
flag1 = true
if (flag2 == false)
    critical-stuff
flag2 = true
if (flag1 == false)
    critical-stuff

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

แต่การฟันดาบที่จำเป็นสำหรับลำดับการเปิดตัว ล้อมกรอบที่จุดเริ่มต้นและจุดสิ้นสุดของแต่ละชุดข้อความ ซึ่งช่วยไม่ได้ ที่นี่ นอกจากนี้ เรายังต้องตรวจสอบให้แน่ใจว่า volatile/atomic ร้านตามด้วย การโหลด volatile/atomic จะทำให้ทั้งสองรายการไม่ได้รับการจัดเรียงใหม่ โดยปกติจะบังคับใช้โดยการเพิ่มรั้วก่อน ที่จัดเก็บที่สอดคล้องกันตามลำดับ และต่อจากนั้นด้วย (ซึ่งแข็งแรงกว่าที่จำเป็นมาก เนื่องจากรั้วนี้มักจะ การเข้าถึงหน่วยความจำก่อนหน้านี้ทั้งหมดเมื่อเทียบกับหน่วยความจำอื่นๆ ทั้งหมดในภายหลัง)

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

อย่างที่เราเห็นในส่วนก่อนหน้านี้ เราต้องใส่สิ่งกีดขวางที่เก็บ/โหลด ระหว่างการดำเนินการทั้งสอง โค้ดที่เรียกใช้ใน VM สำหรับการเข้าถึงที่มีความผันผวน จะมีลักษณะดังนี้

โหลดที่ผันผวน ร้านค้าที่มีความผันผวน
reg = A
fence for "acquire" (1)
fence for "release" (2)
A = reg
fence for later atomic load (3)

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

ในบางสถาปัตยกรรม โดยเฉพาะ x86 แอตทริบิวต์ "acquire" และ "ปล่อย" เป็นสิ่งไม่จำเป็น เนื่องจากฮาร์ดแวร์นั้น มีการสั่งซื้ออย่างเพียงพอ ดังนั้นเมื่อใช้ x86 เฉพาะรั้วสุดท้าย (3) จริงๆ แล้ว ในทำนองเดียวกันบน x86, Read-modify-write ระดับอะตอม การดำเนินงานนั้นรวมถึงการรั้วที่มั่นคงโดยปริยาย ด้วยเหตุนี้จึงไม่มีทาง ต้องใช้รั้วกั้น สำหรับ ARMv7 ทุกรั้วที่เราพูดถึงข้างต้นนั้น ต้องระบุ

ARMv8 ให้คำแนะนำ LDAR และ STLR ที่บอก บังคับใช้ข้อกำหนด Java ผันผวนหรือ C++ ตามลำดับ โหลดและจัดเก็บ เพื่อหลีกเลี่ยงข้อจำกัดในการจัดลำดับใหม่ที่ไม่จำเป็น ที่กล่าวถึงข้างต้น โค้ด Android 64 บิตบน ARM จะใช้ข้อมูลเหล่านี้ เราเลือกที่จะ เน้นที่ตำแหน่งรั้ว ARMv7 ที่นี่เพราะให้แสงมากกว่า ข้อกำหนดจริง

อ่านเพิ่มเติม

หน้าเว็บและเอกสารที่เจาะลึกหรือกว้างมากกว่า ยิ่งมีประโยชน์โดยทั่วไป จะอยู่บริเวณด้านบนสุดของรายการ

โมเดลความสอดคล้องของหน่วยความจำที่แชร์: บทแนะนำ
เขียนในปี 1995 โดย Adve และ Gharachorloo นี่คือจุดเริ่มต้นที่ดีหากคุณต้องการเจาะลึกเกี่ยวกับโมเดลความสอดคล้องของหน่วยความจำแบบเจาะลึก
http://www.hpl.hp.com/techreports/Compaq-DEC/WRL-95-7.pdf
อุปสรรคด้านหน่วยความจำ
บทความเล็กๆ ที่สรุปปัญหาได้ดี
https://en.wikipedia.org/wiki/Memory_barrier
ข้อมูลเบื้องต้นเกี่ยวกับชุดข้อความ
ข้อมูลเบื้องต้นเกี่ยวกับการเขียนโปรแกรมแบบหลายชุดข้อความใน C++ และ Java โดย Hans Boehm การสนทนาเกี่ยวกับการแข่งขันด้านข้อมูลและวิธีการซิงค์พื้นฐาน
http://www.hboehm.info/c++mm/threadsintro.html
การใช้งาน Java Concurrency ในทางปฏิบัติ
หนังสือเล่มนี้จัดพิมพ์ขึ้นในปี 2006 ครอบคลุมหัวข้อต่างๆ มากมายโดยละเอียด ขอแนะนำอย่างยิ่งสำหรับผู้ที่เขียนโค้ดแบบหลายเธรดใน Java
http://www.javaconcurrencyinpractice.com
คำถามที่พบบ่อยเกี่ยวกับ JSR-133 (รุ่นหน่วยความจำ Java)
ข้อมูลเบื้องต้นเกี่ยวกับโมเดลหน่วยความจำ Java รวมถึงคำอธิบายการซิงค์ ตัวแปรที่มีความผันผวน และการสร้างฟิลด์สุดท้าย (ล้าสมัยไปเล็กน้อย โดยเฉพาะเมื่อพูดถึงภาษาอื่น)
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html
ระยะเวลาที่ใช้ได้ของการเปลี่ยนรูปแบบโปรแกรมในโมเดลหน่วยความจำของ Java
คำอธิบายเชิงเทคนิคเกี่ยวกับปัญหาของ โมเดลหน่วยความจำ Java ปัญหาเหล่านี้ไม่มีผลกับการไม่ใช้อินเทอร์เน็ต โปรแกรม
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.112.1790&rep=rep1&type=pdf
ภาพรวมของแพ็กเกจ java.util.concurrent
เอกสารสำหรับแพ็กเกจ java.util.concurrent บริเวณด้านล่างของหน้ามีส่วนที่มีชื่อว่า "คุณสมบัติความสอดคล้องกันของหน่วยความจำ" ซึ่งอธิบายการรับประกันโดยคลาสต่างๆ
java.util.concurrent สรุปแพ็กเกจ
ทฤษฎีและการปฏิบัติของ Java: เทคนิคการก่อสร้างที่ปลอดภัยใน Java
บทความนี้จะอธิบายรายละเอียดเกี่ยวกับอันตรายของการอ้างอิงที่ใช้ Escape ระหว่างการสร้างออบเจ็กต์ และให้หลักเกณฑ์สำหรับตัวสร้างที่ปลอดภัยของเธรด
http://www.ibm.com/developerworks/java/library/j-jtp0618.html
ทฤษฎีและการปฏิบัติของ Java: การจัดการความผันผวน
บทความดีๆ ที่อธิบายสิ่งที่คุณทำได้และทำไม่ได้เมื่อใช้ช่องที่มีความผันผวนใน Java
http://www.ibm.com/developerworks/java/library/j-jtp06197.html
คำประกาศ "การล็อกแบบตรวจสอบซ้ำเสียหาย"
คำอธิบายโดยละเอียดของ Bill Pugh เกี่ยวกับวิธีต่างๆ ที่ทำให้การล็อกแบบตรวจสอบอีกครั้งเสียหายโดยไม่มีvolatileหรือatomic รวมถึง C/C++ และ Java
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleVerifyLocking.html
[ARM] Barrier Litmus Tests และตำราอาหาร
การพูดคุยเกี่ยวกับปัญหา SMP ของ ARM ที่มีการอธิบายด้วยตัวอย่างโค้ด ARM สั้นๆ หากพบว่าตัวอย่างในหน้านี้ไม่เจาะจงเกินไป หรือต้องการอ่านคำอธิบายอย่างเป็นทางการของคำสั่ง DMB โปรดอ่าน รวมถึงอธิบายวิธีที่ใช้สำหรับอุปสรรคหน่วยความจำเกี่ยวกับโค้ดสั่งการ (ซึ่งอาจเป็นประโยชน์หากคุณกำลังสร้างโค้ดแบบทันใจ) โปรดทราบว่าค่านี้เกิดขึ้นก่อน ARMv8 ซึ่งยัง รองรับวิธีการเรียงลำดับหน่วยความจำเพิ่มเติมและย้ายไปยังสถานะที่ค่อนข้างรัดกุม โมเดลหน่วยความจำ (โปรดดูรายละเอียดที่ "คู่มืออ้างอิงสถาปัตยกรรม ARMv8 สำหรับโปรไฟล์สถาปัตยกรรม ARMv8-A" )
http://infocenter.arm.com/help/topic/com.arm.doc.genc007826/Barrier_Litmus_Tests_and_Cookbook_A08.pdf
อุปสรรคหน่วยความจำเคอร์เนลของ Linux
เอกสารประกอบเกี่ยวกับอุปสรรคด้านหน่วยความจำของเคอร์เนลของ Linux มีตัวอย่างที่มีประโยชน์และศิลปะ ASCII
http://www.kernel.org/doc/Documentation/memory-barriers.txt
ISO/IEC JTC1 SC22 WG21 (มาตรฐาน C++) 14882 (ภาษาในการเขียนโปรแกรม C++) หัวข้อ 1.10 และข้อ 29 ("ไลบรารีการดำเนินการแบบปรมาณู")
ร่างมาตรฐานสำหรับฟีเจอร์การดำเนินการแบบอะตอมของ C++ เวอร์ชันนี้ ใกล้เคียงกับมาตรฐาน C++14 ซึ่งรวมถึงการเปลี่ยนแปลงเล็กน้อยในส่วนนี้ จาก C++11
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4527.pdf
(บทนำ: http://www.hpl.hp.com/techreports/2008/HPL-2008-56.pdf)
ISO/IEC JTC1 SC22 WG14 (มาตรฐาน C) 9899 (ภาษาโปรแกรม C) บทที่ 7.16 ("Atomics <stdatomic.h>")
ร่างมาตรฐานสำหรับฟีเจอร์การใช้งานแบบอะตอมตามมาตรฐาน ISO/IEC 9899-201x C โปรดตรวจสอบรายงานข้อบกพร่องในภายหลังเพื่อดูรายละเอียดเพิ่มเติม
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
การแมป C/C++11 กับโปรเซสเซอร์ (University of Cambridge)
คอลเล็กชันคำแปลของ Jaroslav Sevcik และ Peter Sewell ของ C++ atomics ไปยังชุดคำสั่งสำหรับตัวประมวลผลทั่วไปที่หลากหลาย
http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html
อัลกอริทึมของ Dekker
"วิธีแก้ปัญหาที่ถูกต้องซึ่งเป็นที่รู้จักครั้งแรกในการแก้ปัญหาการยกเว้นซึ่งกันและกันในการจัดโปรแกรมพร้อมกัน" บทความใน Wikipedia มีอัลกอริทึมที่สมบูรณ์ ซึ่งมีการพูดคุยเกี่ยวกับวิธีอัปเดตเพื่อให้ใช้งานกับคอมไพเลอร์และฮาร์ดแวร์ SMP ที่มีประสิทธิภาพสูงสุดในปัจจุบัน
https://en.wikipedia.org/wiki/Dekker's_algorithm
ความคิดเห็นใน ARM กับทรัพยากร Dependency ในเวอร์ชันอัลฟ่าและที่อยู่
อีเมลในรายชื่ออีเมล Arm-kernel จาก Catalin Marinas มีข้อมูลสรุปที่อยู่ดีๆ และการควบคุมการอ้างอิง
http://linux.derkeiler.com/Mailing-Lists/Kernel/2009-05/msg11811.html
สิ่งที่โปรแกรมเมอร์ทุกคนควรรู้เกี่ยวกับหน่วยความจำ
บทความโดย Ulrich Drepper บทความที่มีรายละเอียดมากและยาวมากเกี่ยวกับหน่วยความจำประเภทต่างๆ โดยเฉพาะแคช CPU
http://www.akkadia.org/drepper/cpumemory.pdf
เหตุผลเกี่ยวกับโมเดลหน่วยความจำที่มีความเสถียรต่ำของ ARM
เอกสารนี้เขียนโดย Chong และ Ishtiaq จาก ARM, Ltd. ซึ่งพยายามอธิบายโมเดลหน่วยความจำ ARM SMP อย่างเคร่งครัดแต่เข้าถึงได้ คำจำกัดความของ "ความสามารถในการสังเกต" ที่ใช้ที่นี่มาจากเอกสารนี้ นี่เอง เกิดก่อน ARMv8
http://portal.acm.org/ft_gateway.cfm?id=1353528&type=pdf&coll=&dl=&CFID=96099715&CFTOKEN=57505711
ตำราอาหาร JSR-133 สำหรับผู้เขียนคอมไพเลอร์
Doug Lea เขียนข้อความนี้ร่วมกับเอกสารประกอบ JSR-133 (Java Memory Model) ซึ่งประกอบด้วยหลักเกณฑ์การใช้งานชุดแรก สำหรับโมเดลหน่วยความจำของ Java ที่ใช้โดยผู้เขียนคอมไพเลอร์จำนวนมาก และ ยังคงมีการอ้างอิงอย่างกว้างขวางและมีแนวโน้มที่จะให้ข้อมูลเชิงลึก น่าเสียดายที่แนวรั้ว 4 แบบที่กล่าวถึงในที่นี้ไม่ใช่เรื่องที่ดี จับคู่กับสถาปัตยกรรมที่สนับสนุน Android และการแมป C++11 ข้างต้น กลายเป็นแหล่งสูตรอาหารที่แม่นยำกว่าเดิม แม้แต่สำหรับ Java
http://g.oswego.edu/dl/jmm/cookbook.html
x86-TSO: โมเดลของโปรแกรมเมอร์ที่เข้มงวดและใช้งานได้สำหรับมัลติโปรเซสเซอร์ x86
คำอธิบายโดยละเอียดเกี่ยวกับโมเดลหน่วยความจำ x86 คำอธิบายที่แม่นยำของ แต่น่าเสียดายที่โมเดลหน่วยความจำของ ARM จะซับซ้อนกว่ามาก
http://www.cl.cam.ac.uk/~pes20/VASTmemory/cacm.pdf