แพลตฟอร์ม Android 3.0 ขึ้นไปได้รับการเพิ่มประสิทธิภาพให้รองรับ แบบมัลติโปรเซสเซอร์ เอกสารนี้จะกล่าวถึงปัญหาที่อาจเกิดขึ้นเมื่อเขียนโค้ดแบบหลายเธรดสำหรับระบบแบบหลายโปรเซสเซอร์แบบสมมาตรในภาษา C, C++ และ Java (ต่อไปนี้จะเรียกสั้นๆ ว่า "Java") ซึ่งมีจุดประสงค์เพื่อเป็นเกริ่นนำสำหรับนักพัฒนาแอป Android ไม่ใช่เพื่อให้ การสนทนาในเรื่องนี้
ข้อมูลเบื้องต้น
SMP เป็นตัวย่อของ “Symmetric Multi-Processor” โดยจะอธิบายการออกแบบใน ซึ่งแกน CPU ที่เหมือนกัน 2 แกนขึ้นไปแชร์สิทธิ์เข้าถึงหน่วยความจำหลัก อุปกรณ์ Android ทั้งหมดเป็น UP (Uni-Processor) จนถึงเมื่อไม่กี่ปีที่ผ่านมา
อุปกรณ์ Android ส่วนใหญ่ (หากไม่ใช่ทั้งหมด) มี CPU หลายตัวเสมอ แต่ที่ผ่านมามีเพียงตัวเดียวที่ใช้เรียกใช้แอปพลิเคชัน ส่วนที่เหลือจะจัดการฮาร์ดแวร์ส่วนต่างๆ ของอุปกรณ์ (เช่น วิทยุ) CPU อาจมีสถาปัตยกรรมที่แตกต่างกัน และ ที่กำลังทำงานบนหน่วยเหล่านั้นจะไม่สามารถใช้หน่วยความจำหลักในการสื่อสารกับ อื่นๆ
อุปกรณ์ Android ส่วนใหญ่ที่ขายอยู่ในทุกวันนี้สร้างขึ้นจากการออกแบบ SMP ทำให้การทำงานซับซ้อนขึ้น สำหรับนักพัฒนาซอฟต์แวร์ เงื่อนไขการแข่งขันในโปรแกรมแบบหลายเธรดอาจไม่ก่อให้เกิดปัญหาที่เห็นได้ชัดในโปรเซสเซอร์แบบ 1 แกน แต่อาจทำงานไม่สำเร็จเป็นประจำเมื่อเธรดตั้งแต่ 2 รายการขึ้นไปทำงานพร้อมกันในคอร์ต่างๆ ยิ่งไปกว่านั้น โค้ดอาจมีแนวโน้มมากขึ้นหรือน้อยลงที่จะล้มเหลวเมื่อเรียกใช้ สถาปัตยกรรมโปรเซสเซอร์ หรือแม้กระทั่งในการปรับใช้ สถาปัตยกรรม โค้ดที่ได้รับการทดสอบอย่างละเอียดใน x86 อาจเสียหายอย่างมากใน ARM โค้ดอาจเริ่มล้มเหลวเมื่อคอมไพเลอร์ใหม่ด้วยคอมไพเลอร์ที่ทันสมัยกว่า
ส่วนที่เหลือของเอกสารนี้จะอธิบายสาเหตุ และบอกสิ่งที่คุณต้องทำ เพื่อให้แน่ใจว่าโค้ดทำงานได้อย่างถูกต้อง
รูปแบบความสอดคล้องของหน่วยความจำ: เหตุใด SMP จึงแตกต่างออกไปเล็กน้อย
นี่เป็นภาพรวมที่มีความเร็วและมันวาวของวัตถุที่ซับซ้อน บางพื้นที่จะ ไม่สมบูรณ์ แต่ก็ไม่ควรมีเนื้อหาที่ทำให้เข้าใจผิดหรือผิด ขณะที่คุณ จะเห็นในส่วนถัดไป ซึ่งโดยทั่วไปจะไม่สำคัญรายละเอียดในส่วนนี้
ดูอ่านเพิ่มเติมในตอนท้ายของเอกสารสำหรับ จะชี้ให้เห็นถึงการรักษา ในระดับที่ละเอียดขึ้น
รูปแบบความสอดคล้องของหน่วยความจำ หรือมักจะเรียกสั้นๆ ว่า “โมเดลหน่วยความจำ” อธิบายถึง รับประกันภาษาโปรแกรมหรือสถาปัตยกรรมฮาร์ดแวร์ เกี่ยวกับการเข้าถึงหน่วยความจำ ตัวอย่างเช่น หากคุณเขียนค่าเพื่อระบุที่อยู่ A แล้วเขียนค่าเพื่อระบุที่อยู่ B โมเดลอาจรับประกันว่าทุกแกน CPU จะเห็นว่ามีการเขียนเหล่านั้นเกิดขึ้นตามลำดับดังกล่าว
โมเดลที่โปรแกรมเมอร์ส่วนใหญ่คุ้นเคยคือ ความสอดคล้อง ซึ่งมีคำอธิบายเช่นนี้ (โฆษณา Gharachorloo):
- ดูเหมือนว่าการดำเนินการหน่วยความจำทั้งหมดจะทำงานทีละรายการ
- การดำเนินการทั้งหมดในชุดข้อความเดียวจะดูเหมือนจะดำเนินการตามลำดับที่อธิบายโดยโปรแกรมของโปรเซสเซอร์นั้น
สมมติว่าชั่วคราวเรามีคอมไพเลอร์หรือล่ามแบบง่าย ซึ่งไม่น่าแปลกใจเลย โดยแปลว่า ในซอร์สโค้ดเพื่อโหลดและจัดเก็บคำสั่งใน คำสั่งซื้อที่เกี่ยวข้อง หนึ่งคำสั่งต่อหนึ่งการเข้าถึง เราจะสมมติเพื่อความง่ายว่าแต่ละเธรดจะทำงานบนตัวประมวลผลของตัวเอง
หากคุณดูโค้ดบางส่วนและพบว่าโค้ดดังกล่าวมีการอ่านและเขียนจาก ในสถาปัตยกรรม CPU ที่สอดคล้องกัน คุณทราบดีว่าโค้ด จะอ่านและเขียนตามลำดับที่คาดไว้ เป็นไปได้ว่า ที่จริงแล้ว CPU เรียงลำดับวิธีการใหม่และทำให้การอ่านและเขียนล่าช้า ไม่ใช่วิธีที่โค้ดที่ทำงานในอุปกรณ์จะบอกได้ว่า CPU กำลังทำอะไรอยู่ นอกเหนือจากคำสั่งให้ดำเนินการในลักษณะที่ตรงไปตรงมา (เราจะไม่สนใจ I/O ของไดรเวอร์อุปกรณ์ที่แมปหน่วยความจำ)
ในการอธิบายประเด็นเหล่านี้ การพิจารณาตัวอย่างโค้ดสั้นๆ นั้นมีประโยชน์ โดยทั่วไปเรียกว่าชุดตรวจลิตัส
ต่อไปนี้เป็นตัวอย่างง่ายๆ โดยมีโค้ดที่ทำงานในเทรด 2 ชุดข้อความ
ชุดข้อความที่ 1 | ชุดข้อความที่ 2 |
---|---|
A = 3 |
reg0 = B |
ในตัวอย่างลิทัสนี้และในอนาคตทั้งหมด ตำแหน่งความทรงจำจะแสดงด้วย ตัวอักษรพิมพ์ใหญ่ (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 จะสอดคล้องกันตามลำดับ ดูเหมือนว่าเธรดจะทำงานแบบสลับกัน เนื่องจากเคอร์เนลระบบปฏิบัติการสลับไปมาระหว่างเธรด ระบบ SMP ส่วนใหญ่ รวมถึง x86 และ ARM ไม่สม่ำเสมอตามลำดับ ตัวอย่างเช่น ฮาร์ดแวร์มักจะบัฟเฟอร์ร้านค้าระหว่างทางไปยังหน่วยความจำ เพื่อไม่ให้เข้าถึงหน่วยความจำในทันทีและแสดงให้แกนอื่นๆ เห็น
รายละเอียดแตกต่างกันมาก เช่น x86 แม้ว่าจะไม่เรียงตามลำดับ สอดคล้องกัน ยังคงรับประกันว่า reg0 = 5 และ reg1 = 0 เป็นไปไม่ได้ ระบบจะบัฟเฟอร์ร้านค้าไว้ แต่ยังคงรักษาลำดับของร้านค้าไว้ ในทางกลับกัน ARM ไม่ได้ทำเช่นนั้น ระบบจะไม่รักษาลําดับของร้านค้าที่บัฟเฟอร์ไว้ และร้านค้าอาจเข้าถึงแกนอื่นๆ ทั้งหมดพร้อมกันไม่ได้ ความแตกต่างเหล่านี้สำคัญต่อการประกอบโปรแกรมเมอร์ แต่อย่างที่เราเห็นด้านล่างนี้ โปรแกรมเมอร์ C, C++ หรือ Java สามารถ และควรตั้งโปรแกรมในลักษณะที่ปิดบังความแตกต่างทางสถาปัตยกรรมดังกล่าว
ที่ผ่านมา เราเข้าใจผิดว่าฮาร์ดแวร์เป็นอุปกรณ์เดียวที่เรียงลำดับคำสั่งใหม่ ในความเป็นจริง คอมไพเลอร์ยังเรียงลำดับวิธีการใหม่เป็น เพื่อปรับปรุงประสิทธิภาพ ในตัวอย่างของเรา คอมไพเลอร์อาจตัดสินใจว่า โค้ดในเทรดที่ 2 ต้องการค่า reg1 ก่อนที่จะต้อง reg0 จึงโหลด reg1 ก่อน หรือบางโค้ดก่อนหน้านี้อาจโหลด A ไว้แล้ว และคอมไพเลอร์ คุณอาจตัดสินใจใช้ค่านั้นซ้ำแทนการโหลด A อีกครั้ง ไม่ว่าในกรณีใด การโหลดไปยัง reg0 และ reg1 อาจมีการจัดเรียงใหม่
เรียงลำดับการเข้าถึงตำแหน่งหน่วยความจำใหม่ ในฮาร์ดแวร์หรือในคอมไพเลอร์ เนื่องจากไม่ส่งผลกระทบต่อการดำเนินการของชุดข้อความเดียว และ เครื่องมือนี้จะช่วยปรับปรุงประสิทธิภาพได้ อย่างมาก ซึ่งเราจะเห็นด้วยความระมัดระวัง เรายังสามารถป้องกันไม่ให้การอัปเดตดังกล่าวส่งผลกระทบต่อผลลัพธ์ของโปรแกรมแบบหลายชุดข้อความ
เนื่องจากคอมไพเลอร์สามารถเรียงลำดับการเข้าถึงหน่วยความจำใหม่ได้ด้วย จริงๆ แล้วปัญหานี้คือ ไม่ใช่เรื่องใหม่สำหรับ SMP แม้แต่บนยูนิโปรเซสเซอร์ คอมไพเลอร์ยังสามารถเรียงลำดับการโหลดใหม่เป็น reg0 และ reg1 ในตัวอย่างของเรา และ Thread 1 อาจมีกำหนดการระหว่าง วิธีการเรียงลําดับใหม่ แต่ถ้าคอมไพเลอร์ของเราไม่ได้จัดเรียงใหม่ เราอาจ ไม่เคยสังเกตเห็นปัญหานี้ ใน SMP ของ ARM ส่วนใหญ่ แม้จะไม่มีการเรียงลําดับใหม่ของคอมไพเลอร์ คุณก็อาจเห็นการเรียงลําดับใหม่หลังจากการดําเนินการที่สําเร็จจํานวนมาก โดยทั่วไปแล้ว 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>
รายการหนึ่งในเธรดหนึ่งขณะที่อ่านรายการอื่นในเธรดอื่นจะไม่ทำให้เกิดการแข่งขันข้อมูล เนื่องจากไลบรารีสัญญาว่าจะไม่ทำให้เกิดการแข่งขันข้อมูล (ระดับต่ำ) ในกรณีดังกล่าว
โดยปกติจะเข้าถึงช่องต่างๆ ได้พร้อมกันในโครงสร้างข้อมูล ไม่สามารถเริ่มการแข่งข้อมูล แต่มีข้อยกเว้นที่สำคัญอย่างหนึ่งคือ กฎนี้: แถวที่ต่อเนื่องกันของบิตฟิลด์ใน C หรือ C++ จะถือว่าเป็น "ตำแหน่งหน่วยความจำ" เพียงตำแหน่งเดียว การเข้าถึงฟิลด์บิตต่างๆ ในลำดับดังกล่าว ถือเป็นการเข้าถึงเว็บไซต์ ทั้งหมด เพื่อวัตถุประสงค์ในการพิจารณา การมีอยู่ของการแข่งขันด้านข้อมูล ข้อมูลนี้สะท้อนให้เห็นว่าฮาร์ดแวร์ทั่วไปไม่สามารถใช้งานได้ เพื่ออัปเดตบิตแต่ละรายการโดยไม่ต้องอ่านและเขียนบิตที่อยู่ติดกันอีกครั้ง โปรแกรมเมอร์ Java ไม่มีข้อกังวลที่เทียบเคียงกันได้
การหลีกเลี่ยงการแข่งขันด้านข้อมูล
ภาษาโปรแกรมสมัยใหม่มีการซิงค์ข้อมูลจำนวนมาก เพื่อหลีกเลี่ยงการแข่งขันด้านข้อมูล เครื่องมือพื้นฐานที่สุด ได้แก่
- ล็อกหรือปิดเสียง
- ปิดเสียง (C++11
std::mutex
หรือpthread_mutex_t
) หรือ สามารถใช้บล็อกsynchronized
ใน Java เพื่อให้แน่ใจว่า โค้ดไม่ทำงานพร้อมกันกับส่วนอื่นๆ ข้อมูลเดียวกัน โดยทั่วไปแล้ว เราจะพูดถึงบริการเหล่านี้และสิ่งอำนวยความสะดวกอื่นๆ ที่คล้ายกัน เป็น "ล็อก" รับล็อกอย่างสม่ำเสมอก่อนที่จะเข้าถึงล็อกที่แชร์ โครงสร้างข้อมูล และปล่อยข้อมูลนั้นในภายหลัง ซึ่งจะป้องกันการแข่งขันด้านข้อมูลเมื่อเข้าถึงข้อมูล โครงสร้างข้อมูล และยังช่วยให้มั่นใจว่าการอัปเดตและการเข้าถึงนั้นเป็นแบบอะตอม กล่าวคือ ไม่ใช่ การอัปเดตอื่นๆ ในโครงสร้างข้อมูล จะเกิดขึ้นในตอนกลาง สมควรที่ เป็นเครื่องมือที่นิยมใช้กันมากที่สุดในการป้องกันการแข่งขันด้านข้อมูล การใช้ Javasynchronized
บล็อก หรือ 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 = ...
|
while (!flag) {}
|
เนื่องจากเทรด 2 รอให้ตั้งค่า flag
สิทธิ์เข้าถึง
A
ในชุดข้อความที่ 2 ต้องเกิดขึ้นหลังจาก และไม่ได้เกิดขึ้นพร้อมกันกับ
การมอบหมายให้กับ A
ในเทรด 1 ดังนั้นจึงไม่มีการแข่งขันด้านข้อมูลบน
A
การแข่งขันใน flag
จะไม่นับเป็นการแข่งขันด้านข้อมูล
เนื่องจากการเข้าถึงที่มีความผันผวน/ระดับอะตอมไม่ใช่ "การเข้าถึงหน่วยความจำแบบธรรมดา"
จำเป็นต้องมีการติดตั้งใช้งานเพื่อป้องกันหรือซ่อนการจัดเรียงหน่วยความจำใหม่ เพียงพอที่จะทำให้โค้ดอย่างเช่น การทดสอบลิทัสก่อนหน้านี้ทำงานตามที่คาดไว้ ซึ่งมักเป็นการเข้าถึงหน่วยความจำอะตอมที่มีความผันผวน ราคาแพงกว่าการเข้าถึงทั่วไปอย่างมาก
แม้ว่าตัวอย่างก่อนหน้านี้จะไม่มีการใช้ข้อมูล แต่การล็อกพร้อมกันกับ
Object.wait()
ใน Java หรือตัวแปรเงื่อนไขใน C/C++ มักจะ
ให้โซลูชันที่ดีกว่าโดยที่ไม่ต้องเสียเวลารอ
ทำให้พลังงานแบตเตอรี่หมดเร็ว
เมื่อการเรียงลำดับความทรงจำปรากฏขึ้น
ปกติแล้ว โปรแกรมแบบไม่ต้องเสียข้อมูล ช่วยให้เราไม่ต้องคอยจัดการ มีปัญหาเกี่ยวกับการเรียงลำดับการเข้าถึงหน่วยความจำใหม่ อย่างไรก็ตาม มีหลายกรณีดังนี้ การจัดเรียงใหม่ใดบ้างที่จะมองเห็นได้- หากโปรแกรมมีข้อบกพร่องซึ่งส่งผลให้เกิดการแข่งขันด้านข้อมูลโดยไม่ได้ตั้งใจ
การแปลงคอมไพเลอร์และฮาร์ดแวร์สามารถมองเห็นได้
ของ Google อาจทำให้คุณประหลาดใจ ตัวอย่างเช่น ถ้าเราลืมประกาศ
flag
มีความผันผวนในตัวอย่างก่อนหน้านี้ เทรดที่ 2 อาจเห็นA
ที่ไม่ได้เริ่มต้น หรือคอมไพเลอร์อาจตัดสินใจว่า Flag ไม่สามารถเปลี่ยนแปลงได้ในระหว่างลูปของเธรด 2 และเปลี่ยนรูปแบบโปรแกรมเป็นชุดข้อความที่ 1 ชุดข้อความที่ 2 A = ...
flag = truereg0 = flag; while (!reg0) {}
... = Aflag
เป็นจริง - C++ มีเครื่องมือสำหรับการผ่อนปรนความสอดคล้องตามลำดับอย่างชัดเจน แม้ว่าจะไม่มีการแข่งขันก็ตาม การดำเนินการของปรมาณู
สามารถรับอาร์กิวเมนต์
memory_order_
... ที่ชัดเจน ในทำนองเดียวกัน แพ็กเกจjava.util.concurrent.atomic
มีการจำกัดมากกว่า ชุดสิ่งอำนวยความสะดวกที่คล้ายกัน โดยเฉพาะlazySet()
และ Java โปรแกรมเมอร์บางครั้งจะใช้การแข่งขันด้านข้อมูลโดยเจตนาเพื่อผลลัพธ์ที่คล้ายกัน ทั้งหมดนี้ช่วยปรับปรุงประสิทธิภาพได้ และความซับซ้อนของการเขียนโปรแกรม เราจะพูดคุยกันสั้นๆ เท่านั้น ด้านล่าง - โค้ด C และ C++ บางโค้ดเขียนในรูปแบบเก่า ไม่ใช่ทั้งหมด
สอดคล้องกับมาตรฐานภาษาปัจจุบัน ซึ่ง
volatile
ใช้ตัวแปรแทนตัวatomic
ตัว และลำดับหน่วยความจำ ไม่ได้รับอนุญาตอย่างชัดเจนโดยการแทรกที่เรียกว่า fences หรือ อุปสรรค การดำเนินการนี้ต้องการเหตุผลที่ชัดเจนเกี่ยวกับการเข้าถึง การจัดเรียงใหม่และทำความเข้าใจ รุ่นหน่วยความจำของฮาร์ดแวร์ รูปแบบการเขียนโค้ดในลักษณะนี้ยังคงใช้ในเคอร์เนล Linux ไม่ควร จะใช้ในแอปพลิเคชัน Android ใหม่หรือไม่ และมีการอธิบายเพิ่มเติมในที่นี้
ลองฝึก
การแก้ไขปัญหาความสอดคล้องกันของหน่วยความจำอาจเป็นเรื่องยากมาก หากการประกาศ lock, atomic
หรือ volatile
ขาดหายไปทําให้โค้ดบางส่วนอ่านข้อมูลเก่า คุณอาจหาสาเหตุไม่ได้จากการดูการดัมพ์หน่วยความจําด้วยโปรแกรมแก้ไขข้อบกพร่อง พอคุณตอบ
ออกข้อความค้นหาโปรแกรมแก้ไขข้อบกพร่อง
แกน CPU อาจสังเกตการณ์ชุดเต็มของ
และเนื้อหาของหน่วยความจำและการลงทะเบียน CPU จะปรากฏใน
สถานะที่ "เป็นไปไม่ได้"
สิ่งที่ไม่ควรทำใน C
เราขอนำเสนอตัวอย่างบางส่วนของโค้ดที่ไม่ถูกต้อง พร้อมด้วยวิธีง่ายๆ ในการ แก้ไข แต่ก่อนจะเริ่ม เราต้องพูดถึงการใช้ภาษาพื้นฐาน
C/C++ และ "ผันผวน"
การประกาศ C และ C++ volatile
เป็นเครื่องมือสำหรับวัตถุประสงค์พิเศษมากๆ
เพื่อป้องกันไม่ให้คอมไพเลอร์เรียงลำดับใหม่หรือนำความผันผวนออก
สิทธิ์การเข้าถึง ซึ่งจะเป็นประโยชน์สำหรับการลงทะเบียนอุปกรณ์ฮาร์ดแวร์ที่มีรหัสเข้าถึง
หน่วยความจำถูกแมปกับสถานที่มากกว่าหนึ่งแห่ง หรือโดยเชื่อมโยงกับ
setjmp
แต่ C และ C++ volatile
ต่างจาก Java
volatile
ไม่ได้ออกแบบมาสำหรับการสื่อสารในชุดข้อความ
ใน C และ C++ การเข้าถึงvolatile
ข้อมูลอาจได้รับการจัดเรียงใหม่ด้วยการเข้าถึงข้อมูลที่ไม่มีการผันแปร และไม่มีการรับประกันความเป็นอะตอม ดังนั้นจึงไม่สามารถใช้ 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
ก่อนการเริ่มต้นค่าให้กับฟิลด์ ซึ่งมักเกิดจากคอมไพเลอร์หรือตัวประมวลผลจัดเรียงการใส่ข้อมูลลงใน 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()
et al. และ Data Races หน้าที่ของ Java VM คือการตรวจสอบว่าผลลัพธ์ยังคงปรากฏตามลำดับอย่างสอดคล้องกัน
โดยเฉพาะอย่างยิ่ง หากเทรด 1 เขียนลงในช่อง volatile
และ
ชุดข้อความที่ 2 อ่านมาจากฟิลด์เดียวกันนั้น และเห็นชุดข้อความที่เขียนใหม่
เทรดที่ 2 ก็ได้รับการรับประกันว่าจะเห็นการเขียนทั้งหมดที่ทำก่อนหน้านี้โดย
ชุดข้อความที่ 1 ในแง่ของเอฟเฟกต์ความทรงจำ การเขียนถึง
ความผันผวนนั้นก็เทียบเท่ากับการเปิดตัวมอนิเตอร์ และ
ที่อ่านได้จากช่วงเวลาที่ผันผวน ก็เหมือนกับการได้มาซึ่งหน้าจอ
มีข้อแตกต่างสำคัญอย่างหนึ่งจาก atomic
ของ C++ ได้แก่
หากเราเขียน volatile int x;
ใน Java ค่า x++
จะเหมือนกับ x = x + 1
รายการดังกล่าว
ทำการโหลดอะตอม เพิ่มผลลัพธ์ แล้วทำเป็นอะตอม
จำนวนที่เพิ่มขึ้นโดยรวมไม่ใช่ระดับอะตอม ซึ่งต่างจาก C++
การดำเนินการแบบเพิ่มแบบอะตอมมิกจะมาจาก java.util.concurrent.atomic
แทน
ตัวอย่าง
ตัวอย่างการใช้งานตัวนับแบบ Monotonic ที่ไม่ถูกต้องแบบง่ายคือ (ทฤษฎีและแนวทางปฏิบัติของ Java: การจัดการความผันผวน)
class Counter { private int mValue; public int get() { return mValue; } public void incr() { mValue++; } }
สมมติว่ามีการเรียก get()
และ incr()
จากหลายเธรด และเราต้องการให้แน่ใจว่าทุกเธรดจะเห็นจํานวนปัจจุบันเมื่อมีการเรียก get()
ปัญหาที่ชัดเจนที่สุดคือ
mValue++
จริงๆ แล้วเป็นการดำเนินการ 3 อย่าง ได้แก่
reg = mValue
reg = reg + 1
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
ไม่ใช่ค่า Null แต่ฟิลด์ของเทรดดังกล่าวยังไม่ได้รับการตั้งค่าและพร้อมใช้งาน
สำหรับรายละเอียดเพิ่มเติมและโหมดการทำงานล้มเหลวอื่นๆ โปรดดู"
ลิงก์ "การประกาศว่า "ล็อกไม่ได้" ในภาคผนวกสำหรับรายละเอียดเพิ่มเติมหรือรายการ
71 ("ใช้การเริ่มต้นแบบ Lazy Loading อย่างเหมาะสม") ใน Effective Java, ของ Josh Bloch
ฉบับที่ 2
มี 2 วิธีในการแก้ไขปัญหานี้ ได้แก่
- ก็ทำได้ง่ายๆ แล้วลบการตรวจสอบด้านนอกออก วิธีนี้ช่วยให้มั่นใจได้ว่าเราจะไม่ตรวจสอบค่าของ
helper
นอกบล็อกที่ซิงค์ - ประกาศความผันผวนใน
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 mutex ต้องใช้การดำเนินการแบบอะตอมครั้งเดียวในแต่ละครั้ง และมักจะมีต้นทุนน้อยกว่าการแคชที่พลาดครั้งเดียว หากไม่มีการแย่งชิง ดังนั้นคุณจึงประหยัดค่าใช้จ่ายได้เพียงเล็กน้อยจากการแทนที่การเรียกใช้ mutex ด้วยการดำเนินการแบบอะตอม การออกแบบแบบไม่มีการล็อกสำหรับโครงสร้างข้อมูลที่ซับซ้อนนั้นต้องมีความระมัดระวังมากกว่ามากเพื่อให้การดำเนินการในระดับที่สูงขึ้นในโครงสร้างข้อมูลมีลักษณะเป็นอะตอม (ทั้งโครงสร้าง ไม่ใช่แค่ชิ้นส่วนที่เป็นอะตอมอย่างชัดเจน)
หากคุณใช้การดำเนินการแบบอะตอม การผ่อนคลายในการจัดเรียงด้วย
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
จะล้มเหลวและเราจะลองอีกครั้ง
การใช้อาร์กิวเมนต์ memory_order_relaxed
ในการโหลด x
ครั้งแรกนั้นไม่มีปัญหา เฉพาะการจัดเรียงหน่วยความจำสำหรับ compare_exchange
จริงเท่านั้นที่สำคัญ
ข้อมูลที่แก้ไขโดยอัตโนมัติแต่ยังไม่อ่าน
ในบางครั้งข้อมูลมีการแก้ไขพร้อมกันหลายเธรด แต่
จะไม่ถูกตรวจสอบจนกว่าการคำนวณแบบขนานจะเสร็จสมบูรณ์ ตัวอย่างที่ดีของกรณีนี้คือตัวนับที่เพิ่มขึ้นแบบอะตอม (เช่น ใช้ fetch_add()
ใน C++ หรือ atomic_fetch_add_explicit()
ใน C) โดยหลายเธรดพร้อมกัน แต่ระบบจะละเว้นผลลัพธ์ของการเรียกเหล่านี้เสมอ ระบบจะอ่านค่าผลลัพธ์เมื่ออ่านตอนท้ายเท่านั้น
หลังจากการอัปเดตทั้งหมดเสร็จสมบูรณ์
ในกรณีนี้ ก็ไม่มีวิธีที่จะบอกว่าสิทธิ์เข้าถึงข้อมูลนี้
มีการจัดเรียงใหม่ ดังนั้นโค้ด C++ อาจใช้ memory_order_relaxed
อาร์กิวเมนต์
ตัวนับเหตุการณ์แบบง่ายเป็นตัวอย่างที่พบได้บ่อย เนื่องจาก จึงควรตั้งข้อสังเกตบางอย่างเกี่ยวกับกรณีนี้ไว้
- การใช้
memory_order_relaxed
จะช่วยเพิ่มประสิทธิภาพ แต่อาจไม่ได้ช่วยแก้ปัญหาด้านประสิทธิภาพที่สำคัญที่สุด: การอัปเดตทุกครั้ง ต้องมีสิทธิ์พิเศษในการเข้าถึงบรรทัดแคชที่ใช้ตัวนับ ช่วงเวลานี้ ทำให้แคชหายไปทุกครั้งที่ชุดข้อความใหม่เข้าถึงตัวนับ หากอัปเดตบ่อยและสลับไปมาระหว่างชุดข้อความ ก็จะทำให้เร็วขึ้นมาก เพื่อหลีกเลี่ยงการอัปเดตตัวนับที่แชร์ทุกครั้ง เช่น การใช้ตัวนับภายในเทรดและสรุปผลลัพธ์ในตอนท้าย - เทคนิคนี้สามารถใช้ร่วมกับส่วนก่อนหน้า:
อ่านค่าโดยประมาณและค่าที่ไม่น่าเชื่อถือพร้อมกันขณะอัปเดต
กับการดำเนินการทั้งหมดที่ใช้
memory_order_relaxed
แต่คุณควรถือว่าค่าที่ได้ไม่น่าเชื่อถืออย่างสมบูรณ์ การที่จำนวนดูเหมือนจะเพิ่มขึ้น 1 ครั้งไม่ได้หมายความว่าอีกเธรดหนึ่งจะนับได้จนถึงจุดที่ดำเนินการเพิ่ม จำนวนที่เพิ่มขึ้นอาจมี สั่งซื้ออีกครั้งด้วยรหัสก่อนหน้านี้ (สำหรับกรณีที่คล้ายกันที่เรากล่าวถึง ก่อนหน้านี้ 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
ที่ไม่ใช่ค่า Null เราจะเห็นช่องของ 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<mutex> 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 รายการที่เกิดขึ้น "พร้อมกัน"
อย่างเป็นทางการแล้ว เหตุการณ์นี้หมายถึงเหตุการณ์ที่ไม่มีการเกิดขึ้นก่อนกัน
ซึ่งจะช่วยให้เรียนรู้คำจำกัดความที่แท้จริงของเกิดขึ้นก่อนได้
และ synsyncs-with ใน Java หรือ C++ Memory Model
แม้ว่าความคิดที่สัญชาตญาณว่า "พร้อมๆ กัน" ดีโดยทั่วไป
คำจำกัดความเหล่านี้มีประโยชน์มาก โดยเฉพาะอย่างยิ่งหากคุณ
กำลังพิจารณาที่จะใช้การดำเนินการแบบอะตอมที่มีเรียงลำดับอย่างอ่อนใน C++
(ข้อกำหนดเฉพาะของ Java ปัจจุบันระบุเฉพาะ
lazySet()
อย่างไม่เป็นทางการ) - สำรวจว่าคอมไพเลอร์คืออะไรและไม่อนุญาตให้ทำเมื่อเรียงลำดับโค้ดใหม่ (ข้อกำหนดของ JSR-133 มีตัวอย่างที่ดีส่วนหนึ่งของการเปลี่ยนแปลงทางกฎหมายที่นำไปสู่ ผลลัพธ์ที่ไม่คาดคิด)
- ดูวิธีเขียนคลาสแบบคงที่ใน Java และ C++ (มีรายละเอียดมากกว่าแค่ "อย่าเปลี่ยนแปลงอะไรหลังจากการสร้าง")
- ศึกษาคําแนะนําในส่วนการทํางานพร้อมกันของ Effective Java, 2nd Edition (เช่น คุณควรหลีกเลี่ยงการเรียกใช้เมธอดที่มีไว้สำหรับการลบล้างขณะที่อยู่ในบล็อกที่มีการซิงค์)
- อ่าน 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 นี่คือ
บังคับใช้โดย:
- ใช้วิธีดูแลรั้วที่เหมาะสมก่อนคำสั่งของร้าน ซึ่งจะช่วยป้องกันไม่ให้มีการจัดเรียงการเข้าถึงหน่วยความจำทั้งหมดก่อนหน้านี้ใหม่ด้วยคำสั่ง STORE (นอกจากนี้ยังทำให้ไม่สามารถสั่งซื้อใหม่ด้วย จัดเก็บคำแนะนำในภายหลัง)
- ตามคำสั่งการโหลดด้วยคำสั่งรั้วที่เหมาะสม ซึ่งป้องกันไม่ให้การโหลดมีการจัดเรียงใหม่ด้วยการเข้าถึงที่ตามมา (และอีกครั้งให้มีการจัดลำดับที่ไม่จำเป็นโดยมีการโหลดก่อนหน้านี้เป็นอย่างน้อย)
ข้อมูลทั้งหมดเหล่านี้เพียงพอสำหรับการสั่งซื้อ/เผยแพร่ใน C++
รายการเหล่านี้จำเป็นแต่ไม่เพียงพอสำหรับ Java volatile
หรือ C++ ที่เป็นไปตามลําดับatomic
หากต้องการดูว่าเราต้องใช้อะไรอีกบ้าง ลองพิจารณาส่วนย่อยของอัลกอริทึมของ Dekker
ที่เราได้พูดถึงไปคร่าวๆ ก่อนหน้านี้
flag1
และ flag2
เป็น C++ atomic
หรือ Java volatile
ตัวแปรทั้งคู่เป็น "เท็จ" ในตอนแรก
ชุดข้อความที่ 1 | ชุดข้อความที่ 2 |
---|---|
flag1 = true |
flag2 = true |
ความสอดคล้องตามลำดับจะบอกเป็นนัยว่าต้องดำเนินการกับหนึ่งในการกำหนดค่าให้กับ flag
n ก่อน และค่าดังกล่าวต้องแสดงในเทรดอื่น ด้วยเหตุนี้ เราจะไม่เห็นว่า
เทรดเหล่านี้ดำเนินการ "สิ่งที่มีความสำคัญ" พร้อมกัน
แต่การฟันดาบที่จำเป็นสำหรับลำดับการเปิดตัว
ล้อมกรอบที่จุดเริ่มต้นและจุดสิ้นสุดของแต่ละชุดข้อความ ซึ่งช่วยไม่ได้
ที่นี่ นอกจากนี้ เรายังต้องตรวจสอบให้แน่ใจว่า
volatile
/atomic
ร้านตามด้วย
การโหลด volatile
/atomic
จะทำให้ทั้งสองรายการไม่ได้รับการจัดเรียงใหม่
โดยปกติจะบังคับใช้โดยการเพิ่มรั้วก่อน
ที่จัดเก็บที่สอดคล้องกันตามลำดับ
และต่อจากนั้นด้วย
(ซึ่งแข็งแรงกว่าที่จำเป็นมาก เนื่องจากรั้วนี้มักจะ
การเข้าถึงหน่วยความจำก่อนหน้านี้ทั้งหมดเมื่อเทียบกับหน่วยความจำอื่นๆ ทั้งหมดในภายหลัง)
เราสามารถเชื่อมโยงรั้วเพิ่มเติมกับ การโหลดที่สม่ำเสมอ และเนื่องจากร้านค้ามีความถี่ในการเดินทางน้อยกว่า ที่เราอธิบายนั้นพบได้ทั่วไป และใช้กับ Android มากกว่า
ตามที่ได้กล่าวไว้ก่อนหน้านี้ เราต้องแทรกตัวบล็อกการจัดเก็บ/การโหลดระหว่างการดำเนินการ 2 รายการ โค้ดที่เรียกใช้ใน VM สำหรับการเข้าถึงที่มีความผันผวน จะมีลักษณะดังนี้
ภาระงานที่มีความผันผวน | ร้านค้าที่มีความผันผวน |
---|---|
reg = A |
fence for "release" (2) |
สถาปัตยกรรมของเครื่องจักรจริงมักจะแสดง รั้ว ซึ่งเรียงลำดับสิทธิ์เข้าถึงประเภทต่างๆ และอาจมี ที่แตกต่างกันไป การเลือกระหว่างตัวเลือกเหล่านี้นั้นละเอียดอ่อนและได้รับอิทธิพลจากความจำเป็นในการตรวจสอบว่าร้านค้าจะแสดงให้แกนอื่นๆ เห็นในลําดับที่สอดคล้องกันและการจัดลําดับหน่วยความจําที่เกิดจากการรวมรั้วหลายรายการเข้าด้วยกันนั้นถูกต้อง สำหรับรายละเอียดเพิ่มเติม โปรดดูหน้าของมหาวิทยาลัยเคมบริดจ์ด้วย และรวบรวมการแมปของอะโตมิกส์กับตัวประมวลผลจริง
ในบางสถาปัตยกรรม โดยเฉพาะ 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
(intro: 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 กับโปรเซสเซอร์ (มหาวิทยาลัยเคมบริดจ์)
- คอลเล็กชันการแปลของ Jaroslav Sevcik และ Peter Sewell เกี่ยวกับภาษา C++ แบบอะตอมเป็นชุดคำสั่งของโปรเซสเซอร์ทั่วไปต่างๆ
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 - สิ่งที่โปรแกรมเมอร์ทุกคนควรรู้เกี่ยวกับหน่วยความจำ
- บทความที่ยาวมากและละเอียดเกี่ยวกับหน่วยความจำประเภทต่างๆ โดยเฉพาะอย่างยิ่งแคชของ CPU โดย Ulrich Drepper
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