เหตุใดความผันผวนจึงไม่ถือว่ามีประโยชน์ในการเขียนโปรแกรม C หรือ C ++ แบบมัลติเธรด


165

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

volatileความเข้าใจของฉันนี้เวลาใดตัวแปรอาจมีการเปลี่ยนแปลงนอกไหลของการควบคุมของชิ้นส่วนของรหัสเข้าถึงมันว่าตัวแปรที่ควรได้รับการประกาศให้เป็น ตัวจัดการสัญญาณการลงทะเบียน I / O และตัวแปรที่แก้ไขโดยเธรดอื่นทั้งหมดล้วนเป็นสถานการณ์ดังกล่าว

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

ฉันผิดและอยู่ที่ไหน


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

4
และอีกครั้งคำถามที่ไม่สมควรได้รับและถูกถามที่นี่หลายครั้งก่อนที่จะลุกขึ้น คุณช่วยหยุดทำสิ่งนี้ได้ไหม

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

1
สำหรับการศึกษาเชิงลึกเกี่ยวกับสิ่งที่ CPU ทำกับข้อมูล (ผ่านแคช) ตรวจสอบ: rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf
Sassafras_wot

1
@curtguy นั่นคือสิ่งที่ฉันหมายถึง "ไม่ใช่กรณีใน C" ซึ่งสามารถใช้ในการเขียนลงทะเบียนฮาร์ดแวร์ ฯลฯ และไม่ได้ใช้สำหรับการมัลติเธรดเหมือนที่ใช้กันทั่วไปใน Java
Monstieur

คำตอบ:


213

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

อย่างไรก็ตามพื้นฐานที่เราต้องใช้สำหรับคุณสมบัติที่เหลือยังให้สิ่งที่volatileทำดังนั้นจึงไม่จำเป็นอย่างมีประสิทธิภาพ

สำหรับการเข้าถึงที่ปลอดภัยของเธรดเพื่อเข้าถึงข้อมูลที่แชร์เราจำเป็นต้องรับประกันว่า:

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

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

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

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

ตั้งแต่ C ++ 11 ตัวแปรอะตอมมิก ( std::atomic<T>) ให้การรับรองที่เกี่ยวข้องกับเราทั้งหมด


5
@jbcreix: คุณถามว่า "มัน" อันไหน? อุปสรรคหรือหน่วยความจำที่ระเหยง่าย? ไม่ว่าในกรณีใดคำตอบก็ค่อนข้างเหมือนกัน พวกเขาทั้งคู่ต้องทำงานทั้งในคอมไพเลอร์และซีพียูเนื่องจากพวกเขาอธิบายพฤติกรรมที่สังเกตได้ของโปรแกรมดังนั้นพวกเขาจึงต้องมั่นใจว่า CPU ไม่ได้เรียงลำดับใหม่ทุกอย่างเปลี่ยนพฤติกรรมที่พวกเขารับประกัน แต่ปัจจุบันคุณไม่สามารถเขียนการซิงโครไนซ์เธรดแบบพกพาได้เนื่องจากอุปสรรคหน่วยความจำไม่ได้เป็นส่วนหนึ่งของมาตรฐาน C ++ (ดังนั้นจึงไม่ใช่แบบพกพา) และvolatileไม่แข็งแรงพอที่จะเป็นประโยชน์
jalf

4
ตัวอย่างของ MSDN ทำสิ่งนี้และอ้างว่าไม่สามารถจัดลำดับคำสั่งใหม่ได้ผ่านการเข้าถึงแบบเปลี่ยนแปลงได้: msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx
OJW

27
@OJW: แต่คอมไพเลอร์ของ Microsoft นิยามใหม่volatileเป็นอุปสรรคหน่วยความจำเต็ม (ป้องกันการเรียงลำดับใหม่) นั่นไม่ใช่ส่วนหนึ่งของมาตรฐานดังนั้นคุณจึงไม่สามารถพึ่งพาพฤติกรรมนี้ในรหัสแบบพกพาได้
jalf

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

13
@Skizz: เธรดเองมักเป็นส่วนขยายที่ขึ้นอยู่กับแพลตฟอร์มก่อน C ++ 11 และ C11 เสมอ ตามความรู้ของฉันทุกสภาพแวดล้อม C และ C ++ ที่ให้ส่วนขยายเธรดยังให้ส่วนขยาย "อุปสรรคหน่วยความจำ" โดยไม่คำนึงถึงvolatileไม่มีประโยชน์เสมอสำหรับการเขียนโปรแกรมแบบมัลติเธรด (ยกเว้นภายใต้ Visual Studio โดยที่ volatile คือส่วนขยายกำแพงหน่วยความจำ)
Nemo

49

นอกจากนี้คุณยังอาจพิจารณานี้จากเอกสาร Linux Kernel

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

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

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

พิจารณาบล็อกทั่วไปของรหัสเคอร์เนล:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

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

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

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

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

while (my_variable != what_i_want)
    cpu_relax();

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

ยังมีสถานการณ์ที่หายากอยู่เล็กน้อยที่ความผันผวนทำให้เกิดความรู้สึกในเคอร์เนล:

  • ฟังก์ชั่นการเข้าถึงดังกล่าวอาจใช้ความผันผวนในสถาปัตยกรรมที่การเข้าถึงหน่วยความจำโดยตรง I / O ทำงาน โดยพื้นฐานแล้วการเรียกใช้การเข้าถึงแต่ละครั้งจะกลายเป็นส่วนสำคัญเล็กน้อยในตัวเองและทำให้มั่นใจได้ว่าการเข้าถึงเกิดขึ้นตามที่โปรแกรมเมอร์คาดไว้

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

  • ตัวแปร jiffies มีความพิเศษที่สามารถมีค่าที่แตกต่างกันทุกครั้งที่มีการอ้างอิง แต่สามารถอ่านได้โดยไม่มีการล็อกพิเศษ ดังนั้น jiffies สามารถเปลี่ยนแปลงได้ แต่การเพิ่มตัวแปรอื่น ๆ ของประเภทนี้จะขมวดคิ้วอย่างมาก จิฟฟี่ถือว่าเป็นปัญหา "โง่เขลา" (คำพูดของไลนัส) ในเรื่องนี้; แก้ไขมันจะเป็นปัญหามากกว่าที่มันคุ้มค่า

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

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


3
@currguy: ใช่ ดูเพิ่มเติมgcc.gnu.org/onlinedocs/gcc-4.0.4/gcc/Extended-Asm.html
เซบาสเตียนมัค

1
spin_lock () ดูเหมือนการเรียกใช้ฟังก์ชันปกติ มีอะไรพิเศษเกี่ยวกับเรื่องนี้ที่คอมไพเลอร์จะปฏิบัติต่อมันเป็นพิเศษเพื่อให้โค้ดที่สร้างขึ้นจะ "ลืม" ค่าใด ๆ ของ shared_data ที่ถูกอ่านก่อน spin_lock () และเก็บไว้ในรีจิสเตอร์เพื่อให้ค่านั้นอ่านใหม่ใน do_something_on () หลังจาก spin_lock ()?
ตรงกัน

1
@underscore_d จุดของฉันคือฉันไม่สามารถบอกได้จากชื่อฟังก์ชั่น spin_lock () ว่ามันทำอะไรที่พิเศษ ฉันไม่รู้ว่ามีอะไรอยู่ในนั้น โดยเฉพาะอย่างยิ่งฉันไม่รู้ว่ามีอะไรในการนำไปใช้งานที่ป้องกันไม่ให้คอมไพเลอร์ปรับการอ่านในภายหลังให้เหมาะสม
Syncopated

1
Syncopated มีจุดดี สิ่งนี้หมายความว่าโปรแกรมเมอร์ควรรู้ถึงการใช้งานภายในของ "ฟังก์ชั่นพิเศษ" หรืออย่างน้อยก็ได้รับการแจ้งอย่างดีเกี่ยวกับพฤติกรรมของพวกเขา สิ่งนี้ทำให้เกิดคำถามเพิ่มเติมเช่น - ฟังก์ชั่นพิเศษเหล่านี้เป็นมาตรฐานและรับประกันว่าจะทำงานในลักษณะเดียวกันกับสถาปัตยกรรมและคอมไพเลอร์ทั้งหมดหรือไม่ มีรายการของฟังก์ชั่นดังกล่าวหรือไม่อย่างน้อยมีแบบแผนในการใช้ความคิดเห็นของรหัสเพื่อส่งสัญญาณให้นักพัฒนาฟังก์ชั่นดังกล่าวป้องกันรหัสจากการถูก "ปรับให้เหมาะสม" หรือไม่?
JustAMartin

1
@Tuntable: คงที่ส่วนตัวสามารถสัมผัสได้โดยรหัสใด ๆ ผ่านตัวชี้ และที่อยู่ของมันก็ถูกพาไป บางทีการวิเคราะห์ดาต้า - โฟลว์สามารถพิสูจน์ได้ว่าตัวชี้ไม่เคยหนีออกไปได้ แต่โดยทั่วไปแล้วเป็นปัญหาที่ยากมากคือ superlinear ขนาดโปรแกรม หากคุณมีวิธีการรับประกันว่าไม่มีนามแฝงอยู่การย้ายการเข้าถึงข้ามสปินล็อคควรเป็นจริง แต่ถ้าไม่มีนามแฝงอยู่volatileก็ไม่มีจุดหมายเช่นกัน ในทุกกรณีพฤติกรรม "เรียกใช้ฟังก์ชันที่ไม่สามารถมองเห็นร่างกาย" จะถูกต้อง
Ben Voigt

11

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

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

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

โดยส่วนตัวแล้วการใช้งาน main (only?) ของฉันสำหรับธงแบบระเหยนั้นเป็นแบบบูล "PleaseGoAwayNow" หากฉันมีเธรดคนงานที่วนซ้ำกันอย่างต่อเนื่องฉันจะให้มันตรวจสอบบูลีนที่ระเหยได้ในการวนซ้ำแต่ละครั้งของลูปและออกหากบูลีนเป็นจริง เธรดหลักสามารถล้างเธรดผู้ปฏิบัติงานได้อย่างปลอดภัยโดยการตั้งค่าบูลีนเป็นจริงจากนั้นเรียก pthread_join () เพื่อรอจนกว่าเธรดผู้ปฏิบัติงานจะหายไป


2
ธงบูลีนของคุณอาจไม่ปลอดภัย คุณรับประกันได้อย่างไรว่าผู้ปฏิบัติงานทำงานจนเสร็จสิ้นและแฟล็กยังคงอยู่ในขอบเขตจนกว่าจะถูกอ่าน (ถ้าอ่าน) นั่นคืองานสำหรับสัญญาณ ความผันผวนนั้นดีสำหรับการใช้งานสปิลล็อคง่ายถ้าไม่มีการเกี่ยวข้องกับ mutex เนื่องจากความปลอดภัยนามแฝงหมายความว่าคอมไพเลอร์จะถือว่าmutex_lock(และฟังก์ชั่นห้องสมุดอื่น ๆ ) อาจเปลี่ยนแปลงสถานะของตัวแปรแฟล็ก
Potatoswatter

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

2
เธรดผู้ปฏิบัติงานไม่รับประกันว่าจะทำงานให้เสร็จสมบูรณ์ก่อนที่บูลีนจะเป็นจริง - ในความเป็นจริงมันเกือบจะแน่นอนอยู่กลางหน่วยงานเมื่อบูลถูกตั้งค่าเป็นจริง แต่ไม่สำคัญว่าเมื่อเธรดผู้ปฏิบัติงานเสร็จสิ้นหน่วยการทำงานเนื่องจากเธรดหลักจะไม่ทำอะไรเลยยกเว้นการบล็อกภายใน pthread_join () จนกว่าเธรดผู้ปฏิบัติงานจะออกในทุกกรณี ดังนั้นลำดับการปิดระบบจึงได้รับการจัดอันดับอย่างดี - บูลที่ระเหยได้ (และข้อมูลที่แชร์อื่น ๆ ) จะไม่ได้รับการปลดปล่อยจนกว่าหลังจาก pthread_join () กลับมาและ pthread_join () จะไม่กลับจนกว่าเธรดผู้ปฏิบัติงานจะหายไป
Jeremy Friesner

10
@ Jeremy คุณถูกต้องในทางปฏิบัติ แต่ในทางทฤษฎีมันอาจยังพังได้ บนระบบหลักสองระบบหนึ่งแกนหนึ่งกำลังดำเนินการเธรดผู้ปฏิบัติงานของคุณอย่างต่อเนื่อง แกนอื่น ๆ ตั้งค่าบูลเป็นจริง อย่างไรก็ตามไม่มีการรับประกันว่าแกนของเธรดของผู้ปฏิบัติงานจะเคยเห็นการเปลี่ยนแปลงนั่นคือมันอาจไม่หยุดแม้ว่ามันจะตรวจสอบบูลซ้ำ ๆ พฤติกรรมนี้ได้รับอนุญาตจากรุ่น c ++ 0x, java และ c # memory ในทางปฏิบัติสิ่งนี้จะไม่เกิดขึ้นเนื่องจากเธรดไม่ว่างมักแทรกสิ่งกีดขวางหน่วยความจำที่ไหนสักแห่งหลังจากนั้นมันจะเห็นการเปลี่ยนแปลงของบูล
deft_code

4
ใช้ระบบ POSIX ใช้นโยบายการตั้งเวลาตามเวลาจริงSCHED_FIFOลำดับความสำคัญคงที่สูงกว่ากระบวนการ / เธรดอื่น ๆ ในระบบคอร์ที่เพียงพอควรเป็นไปได้อย่างสมบูรณ์แบบ ใน Linux คุณสามารถระบุว่ากระบวนการแบบเรียลไทม์สามารถใช้เวลา CPU 100% พวกเขาจะไม่สลับบริบทหากไม่มีเธรด / กระบวนการที่มีลำดับความสำคัญสูงกว่าและไม่เคยบล็อกโดย I / O แต่ประเด็นก็คือ C / C ++ volatileไม่ได้มีไว้สำหรับการบังคับใช้การแบ่งปันข้อมูล / ซีแมนทิกส์การซิงโครไนซ์ที่เหมาะสม ฉันค้นหาการค้นหากรณีพิเศษเพื่อพิสูจน์ว่ารหัสที่ไม่ถูกต้องบางทีในบางครั้งอาจใช้งานได้คือ
FooF

7

volatileจะเป็นประโยชน์ (แม้จะไม่เพียงพอ) สำหรับการดำเนินการสร้างพื้นฐานของ mutex spinlock แต่เมื่อคุณมี (หรือบางสิ่งบางอย่างที่เหนือกว่า) volatileคุณไม่จำเป็นอีก

วิธีทั่วไปของการเขียนโปรแกรมแบบมัลติเธรดไม่ได้ป้องกันตัวแปรที่แชร์กันทุกตัวที่ระดับเครื่อง แต่ควรแนะนำตัวแปรยามที่แนะนำโปรแกรมโฟลว์ แทนที่จะเป็นvolatile bool my_shared_flag;คุณ

pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;

สิ่งนี้ไม่เพียง แต่แค็ปซูล "ส่วนที่ยาก" ซึ่งเป็นสิ่งจำเป็นพื้นฐาน: C ไม่ได้รวมการดำเนินงานของอะตอมที่จำเป็นในการใช้ mutex เพียงแค่volatileให้การรับประกันเพิ่มเติมเกี่ยวกับการดำเนินงานปกติเท่านั้น

ตอนนี้คุณมีสิ่งนี้:

pthread_mutex_lock( &flag_guard_mutex );
my_local_state = my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
my_shared_flag = ! my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

my_shared_flag ไม่จำเป็นต้องระเหยแม้จะเป็นสิ่งที่ไม่สามารถลืมได้เพราะ

  1. อีกเธรดสามารถเข้าถึงได้
  2. หมายถึงการอ้างอิงถึงมันจะต้องได้รับบางครั้ง (กับ&ผู้ประกอบการ)
    • (หรือการอ้างอิงถูกนำไปยังโครงสร้างที่มี)
  3. pthread_mutex_lock เป็นฟังก์ชั่นห้องสมุด
  4. ความหมายของคอมไพเลอร์ไม่สามารถบอกpthread_mutex_lockได้ว่าจะได้รับการอ้างอิงนั้นหรือไม่
  5. ความหมายของคอมไพเลอร์จะต้องสมมติว่าpthread_mutex_lockแก้ไขค่าสถานะที่ใช้ร่วมกัน !
  6. ดังนั้นตัวแปรจะต้องโหลดซ้ำจากหน่วยความจำ volatileในขณะที่มีความหมายในบริบทนี้เป็นสิ่งภายนอก

6

ความเข้าใจของคุณเป็นสิ่งที่ผิด

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

int volatile* reg=IO_MAPPED_REGISTER_ADDRESS;
*reg=1; // turn the fuel on
*reg=2; // ignition
*reg=3; // release
int x=*reg; // fire missiles

ปัญหาคือนี่ไม่ใช่คุณสมบัติที่เราต้องการจากสิ่งที่ปลอดภัยเธรด

ตัวอย่างเช่นตัวนับ thread-safe จะเป็นเพียงแค่ (โค้ดคล้าย linux-kernel-ไม่รู้ว่า c ++ 0x เทียบเท่า):

atomic_t counter;

...
atomic_inc(&counter);

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

atomic_inc(&counter);
atomic_inc(&counter);

ยังสามารถปรับให้เหมาะกับ

atomically {
  counter+=2;
}

หากเครื่องมือเพิ่มประสิทธิภาพฉลาดพอ (มันจะไม่เปลี่ยนซีแมนทิกส์ของรหัส)


6

เพื่อให้ข้อมูลของคุณสอดคล้องกันในสภาพแวดล้อมที่เกิดขึ้นพร้อมกันคุณต้องมีเงื่อนไขสองข้อในการใช้:

1) Atomicity เช่นถ้าฉันอ่านหรือเขียนข้อมูลบางอย่างไปยังหน่วยความจำข้อมูลนั้นจะได้รับการอ่าน / เขียนในหนึ่งรอบและไม่สามารถถูกขัดจังหวะหรือโต้แย้งเนื่องจากเช่นบริบทสลับ

2) ความสอดคล้องคือคำสั่งของการอ่าน / เขียน Ops จะต้องเห็นจะเหมือนกันระหว่างสภาพแวดล้อมพร้อมกันหลาย - เป็นไปได้ว่ากระทู้, เครื่อง ฯลฯ

สารระเหยไม่เหมาะกับสิ่งที่กล่าวมาข้างต้น - หรือมากกว่าโดยเฉพาะอย่างยิ่งมาตรฐาน c หรือ c ++ ว่าพฤติกรรมที่ระเหยได้นั้นควรรวมถึงสิ่งใดข้างต้น

มันยิ่งแย่ลงในทางปฏิบัติเนื่องจากคอมไพเลอร์บางตัว (เช่น Intel Itanium คอมไพเลอร์) พยายามที่จะใช้องค์ประกอบของพฤติกรรมการเข้าถึงที่ปลอดภัยพร้อมกัน (เช่นโดยการทำให้แน่ใจว่ารั้วหน่วยความจำ) แต่ไม่มีความสอดคล้องกันในการใช้งานคอมไพเลอร์ ของการดำเนินการในสถานที่แรก

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

c # และ java AFAIK ทำการแก้ไขสิ่งนี้โดยการระเหยไปตาม 1) และ 2) อย่างไรก็ตามไม่สามารถพูดได้เหมือนกันสำหรับคอมไพเลอร์ c / c ++ ดังนั้นโดยพื้นฐานแล้วทำได้ตามที่เห็นสมควร

สำหรับการสนทนาเพิ่มเติมในเชิงลึก (แม้ว่าจะไม่เป็นกลาง) ในหัวข้อที่อ่านนี้


3
+1 - รับประกันอะตอมมิกซิตี้เป็นอีกส่วนหนึ่งของสิ่งที่ฉันพลาดไป ฉันสมมติว่าการโหลด int นั้นเป็นอะตอมมิกดังนั้นการป้องกันความผันผวนของการสั่งซื้อใหม่จึงเป็นคำตอบที่สมบูรณ์ในด้านการอ่าน ฉันคิดว่ามันเป็นสมมติฐานที่ดีสำหรับสถาปัตยกรรมส่วนใหญ่ แต่ก็ไม่รับประกัน
Michael Ekstrand

แต่ละคนจะอ่านและเขียนข้อมูลไปยังหน่วยความจำที่อินเตอร์รัปต์และไม่ปรมาณู มีประโยชน์อะไรบ้าง?
batbrat

5

คำถามที่พบบ่อย comp.programming.threads มีคำอธิบายที่คลาสสิกโดย Dave Butenhof:

คำถามที่ 5: ทำไมฉันไม่ต้องการประกาศตัวแปรที่แชร์ VOLATILE

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

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

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

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

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

/ --- [Dave Butenhof] ----------------------- [butenhof@zko.dec.com] --- \
| Digital Equipment Corporation 110 Spit Brook Rd ZKO2-3 / Q18 |
| 603.881.2218 FAX 603.881.0120 Nashua NH 03062-2698 |
----------------- [ชีวิตที่ดีขึ้นด้วยการเห็นพ้องด้วย] ---------------- /

Mr Butenhof ครอบคลุมพื้นที่เดียวกันมากในการโพสต์ Usenet นี้ :

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

ดังนั้นดังที่ไบรอันอธิบายไว้การใช้ตัวระเหยไม่ได้ทำอะไรนอกจากเพื่อป้องกันไม่ให้คอมไพเลอร์ทำการเพิ่มประสิทธิภาพที่เป็นประโยชน์และเป็นที่ต้องการโดยไม่ช่วยอะไรเลยในการสร้างโค้ด แน่นอนว่าคุณยินดีที่จะประกาศทุกสิ่งที่คุณต้องการในฐานะ "เปลี่ยนแปลงได้" - เป็นคุณลักษณะที่เก็บข้อมูล ANSI C ตามกฎหมายหลังจากนี้ อย่าคาดหวังว่ามันจะแก้ปัญหาการซิงโครไนซ์เธรดสำหรับคุณ

ทั้งหมดนี้ใช้ได้กับ C ++ อย่างเท่าเทียมกัน


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

3

นี่คือทั้งหมดที่ "volatile" กำลังดำเนินการ: "เฮ้คอมไพเลอร์ตัวแปรนี้สามารถเปลี่ยนช่วงเวลาใดก็ได้ (บนนาฬิกาติ๊กใด ๆ ) แม้ว่าจะไม่มีคำแนะนำในท้องถิ่นทำงานอยู่ก็ตามอย่าแคชค่านี้ในการลงทะเบียน"

อย่างนั้นแหละ. มันบอกคอมไพเลอร์ว่าค่าของคุณคือดีระเหย - ค่านี้อาจเปลี่ยนแปลงได้ตลอดเวลาโดยตรรกะภายนอก (เธรดอื่นกระบวนการอื่นเคอร์เนล ฯลฯ ) มีอยู่ไม่มากก็น้อยที่จะระงับการปรับให้เหมาะสมของคอมไพเลอร์ที่จะทำการแคชค่าในการลงทะเบียนโดยที่มันไม่ปลอดภัยสำหรับแคช EVER

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


3

ตามมาตรฐานของฉัน C เก่า“สิ่งที่ถือว่าเข้าถึงวัตถุที่มีชนิดที่มีคุณสมบัติเหมาะสม volatile- เป็นนักการคือการดำเนินการที่กำหนดไว้” ดังนั้นนักเขียน C คอมไพเลอร์จะได้เลือกที่จะมี "ความผันผวน" หมายถึง"หัวข้อการเข้าถึงที่ปลอดภัยในสภาพแวดล้อมแบบหลายขั้นตอน" แต่พวกเขาไม่ได้

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

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


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

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