โค้ดตัวอย่างของ IBM ฟังก์ชั่นที่ไม่ใช่ผู้เข้าร่วมใหม่ไม่ทำงานในระบบของฉัน


11

ฉันกำลังศึกษาเรื่องการเขียนโปรแกรมอีกครั้ง บนเว็บไซต์ของ IBM (อันนี้ดีจริงๆ) ฉันได้ก่อตั้งรหัสแล้วคัดลอกด้านล่าง มันเป็นรหัสแรกที่นำมาลงเว็บไซต์

รหัสพยายามแสดงปัญหาที่เกี่ยวข้องกับการเข้าถึงตัวแปรในการพัฒนาเชิงเส้นของโปรแกรมข้อความ (asynchronicity) โดยการพิมพ์ค่าสองค่าที่เปลี่ยนแปลงตลอดเวลาใน "บริบทอันตราย"

#include <signal.h>
#include <stdio.h>

struct two_int { int a, b; } data;

void signal_handler(int signum){
   printf ("%d, %d\n", data.a, data.b);
   alarm (1);
}

int main (void){
   static struct two_int zeros = { 0, 0 }, ones = { 1, 1 };

   signal (SIGALRM, signal_handler); 
   data = zeros;
   alarm (1);
   while (1){
       data = zeros;
       data = ones;
   }
}

ปัญหาปรากฏขึ้นเมื่อฉันพยายามเรียกใช้รหัส (หรือดีกว่าไม่ปรากฏขึ้น) ฉันใช้ gcc รุ่น 6.3.0 20170516 (Debian 6.3.0-18 + deb9u1) ในการกำหนดค่าเริ่มต้น เอาต์พุตผิดพลาดจะไม่เกิดขึ้น ความถี่ในการรับค่าคู่ "ผิด" คือ 0!

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


1
ตรวจสอบให้แน่ใจว่าการเพิ่มประสิทธิภาพคอมไพเลอร์ทั้งหมดถูกปิดใช้งานและลองอีกครั้ง
roaima

ฉันควรที่ ... แต่ฉันจะเปลี่ยนตัวเลือกใด ฉันไม่รู้. :-(
Daniel Bandeira

5
ดูเหมือนว่าคำถามการเขียนโปรแกรม (ล้นล้น) มันดูไม่ค่อยดีเท่าไหร่ (ขออภัยฉันมีไซต์ย่อยน้อยกว่ามันถูกตัดออกไป แต่นั่นเป็นวิธีที่มันเป็น)
ctrl-alt-delor

1
รหัสการเข้าใช้งานใหม่ที่ง่ายที่สุดนั้นไม่เปลี่ยนรูป
ctrl-alt-delor

ในตอนแรกฉันคิดว่าคำถามจะเกี่ยวข้องกับสภาพแวดล้อม gcc และ Linux ตัวอย่างเช่นการพัฒนาการจัดตารางเวลาของ OS (การรันข้อความโปรแกรมเพิ่มเติมหลังจากสัญญาณขัดจังหวะก่อนที่จะเรียกรูทีน handler) ตัวอย่างเช่น
Daniel Bandeira

คำตอบ:


12

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

นี่เป็นเพียงธรรมดาวานิลลาข้อมูลการแข่งขัน UB (พฤติกรรมที่ไม่ได้กำหนด) ระหว่างจัดการสัญญาณและหัวข้อหลัก: เพียงsig_atomic_tมีการประกันความปลอดภัยสำหรับการนี้ คนอื่นอาจทำงานเช่นในกรณีของคุณที่สามารถโหลดวัตถุ 8 ไบต์หรือเก็บไว้ด้วยคำสั่งเดียวใน x86-64 และคอมไพเลอร์เกิดขึ้นเพื่อเลือก asm (ตามคำตอบของ @ icarus แสดงให้เห็น)

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

กรณีทดสอบของคุณจากการฉีกขาดเกิดขึ้นจริงเนื่องจาก UB ของ data-race อาจถูกพัฒนา / ทดสอบในโหมด 32 บิตหรือคอมไพเลอร์โง่กว่ารุ่นเก่าที่โหลดสมาชิก struct แยกต่างหาก

ในกรณีของคุณคอมไพเลอร์สามารถเพิ่มประสิทธิภาพร้านค้าออกจากวงวนไม่สิ้นสุดเนื่องจากไม่มีโปรแกรมฟรี UB ที่สามารถสังเกตได้ dataไม่ใช่_Atomicหรือvolatileและไม่มีผลข้างเคียงอื่นในลูป ดังนั้นจึงไม่มีวิธีที่ผู้อ่านสามารถซิงโครไนซ์กับผู้เขียนคนนี้ ในความเป็นจริงนี้เกิดขึ้นหากคุณคอมไพล์ด้วยการเปิดใช้งานการเพิ่มประสิทธิภาพ ( Godboltแสดงการวนซ้ำที่ด้านล่างของหลัก) ฉันยังเปลี่ยน struct เป็นสองlong longและ gcc ใช้movdqa16-byte store เดียวก่อน loop (สิ่งนี้ไม่ได้รับประกันว่าเป็นอะตอมมิก แต่มันใช้กับซีพียูเกือบทั้งหมดโดยถือว่าอยู่ในแนวเดียวกันหรือใน Intel เพียง แต่ไม่ข้ามขอบเขตแคช - ไลน์ เหตุใดการกำหนดค่าจำนวนเต็มบนตัวแปรอะตอมมิกที่จัดเรียงตามธรรมชาติบน x86 )

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

volatile struct two_intก็จะบังคับให้คอมไพเลอร์ไม่ปรับพวกเขาออกไป แต่จะไม่บังคับให้โหลด / เก็บโครงสร้างทั้งหมดของอะตอม (มันจะไม่หยุดมันจากการทำเช่นนั้นทั้งสองแม้ว่า.) หมายเหตุที่volatileไม่ได้หลีกเลี่ยงข้อมูลการแข่งขัน UB แต่ในทางปฏิบัติก็เพียงพอสำหรับการสื่อสารระหว่างด้ายและเป็นวิธีการที่คนสร้างขึ้น Atomics มือรีด (พร้อมกับ asm อินไลน์) ก่อน C11 / C ++ 11 สำหรับสถาปัตยกรรมซีพียูปกติ พวกเขากำลังแคชเชื่อมโยงกันเพื่อให้volatileเป็นในทางปฏิบัติส่วนใหญ่คล้ายกับ_Atomicที่มีmemory_order_relaxedสำหรับบริสุทธิ์โหลดและบริสุทธิ์ร้านถ้าใช้สำหรับประเภทแคบพอที่คอมไพเลอร์จะใช้คำสั่งเดียวเพื่อให้คุณไม่ได้รับการฉีกขาด และแน่นอนว่าvolatileไม่มีการรับประกันใด ๆ จากมาตรฐาน ISO C เทียบกับการเขียนโค้ดที่คอมไพล์กับ asm เดียวกันโดยใช้_Atomicและ mo_relaxed


หากคุณมีฟังก์ชั่นที่ทำglobal_var++;บนintหรือlong longที่คุณเรียกใช้จากหลักและแบบอะซิงโครนัสจากตัวจัดการสัญญาณนั่นจะเป็นวิธีที่จะใช้การป้อนข้อมูลใหม่เพื่อสร้าง UB ของ data-race

ขึ้นอยู่กับว่ามันรวบรวม (ไปยังหน่วยความจำปลายทาง inc หรือเพิ่มหรือแยกโหลด / inc / store) มันจะเป็นอะตอมหรือไม่เกี่ยวกับตัวจัดการสัญญาณในหัวข้อเดียวกัน ดูที่num ++ สามารถเป็นอะตอมสำหรับ 'int NUM' สำหรับข้อมูลเพิ่มเติมเกี่ยวกับ atomicity บน x86 และใน C ++ (C11's stdatomic.hและ_Atomicคุณลักษณะให้ฟังก์ชันที่เทียบเท่ากับstd::atomic<T>แม่แบบของ C ++ 11 )

ข้อขัดจังหวะหรือข้อยกเว้นอื่น ๆ ไม่สามารถเกิดขึ้นได้ในระหว่างการเรียนการสอนดังนั้นการเพิ่มหน่วยความจำปลายทางจึงเป็นอะตอมมิก wrt context switch บน CPU แบบ single-core ตัวเขียน DMA (ที่สอดคล้องกัน) เท่านั้นที่สามารถ "เพิ่ม" การเพิ่มขึ้นจากการadd [mem], 1ไม่มีlockคำนำหน้าบน CPU แบบคอร์เดียว ไม่มีคอร์อื่นใดที่เธรดอื่นสามารถทำงานได้

ดังนั้นจึงคล้ายกับกรณีของสัญญาณ: ตัวจัดการสัญญาณทำงานแทนการดำเนินการตามปกติของเธรดที่จัดการสัญญาณดังนั้นจึงไม่สามารถจัดการได้ในช่วงกลางของคำสั่งเดียว


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

17

เมื่อมองไปที่ผู้รวบรวมคอมไพเลอร์godbolt (หลังจากเพิ่มในส่วนที่ขาดหายไป#include <unistd.h>) เราเห็นว่าสำหรับคอมไพเลอร์ x86_64 เกือบทุกตัวที่สร้างรหัสด้วยการใช้ QWORD ย้ายเพื่อโหลดonesและzerosในคำสั่งเดียว

        mov     rax, QWORD PTR main::ones[rip]
        mov     QWORD PTR data[rip], rax

เว็บไซต์ IBM บอกว่าOn most machines, it takes several instructions to store a new value in data, and the value is stored one word at a time.อาจเป็นจริงสำหรับ cpus ทั่วไปในปี 2005 แต่เป็นรหัสที่แสดงไม่เป็นจริงในขณะนี้ การเปลี่ยน struct ให้มีความยาวสองตัวแทนที่จะเป็นสอง ints จะแสดงปัญหา

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

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


3
ลองเปลี่ยนประเภทข้อมูลจากintเป็นlong longและเรียบเรียงเป็น 32 บิต บทเรียนคือคุณไม่เคยรู้ว่าจะทำลายหรือไม่
ctrl-alt-delor

2
ซึ่งหมายความว่าในเครื่องของฉันการกำหนดค่าสองค่านี้เป็นการทำงานแบบปรมาณู (พิจารณาการรวบรวมสำหรับสถาปัตยกรรม x86_64)
Daniel Bandeira

1
long longยังรวบรวมคำสั่งหนึ่งสำหรับ x86-64: movdqa16 นอกจากว่าคุณจะปิดการใช้งานการเพิ่มประสิทธิภาพเช่นในลิงค์ Godbolt ของคุณ (ค่าเริ่มต้นของ GCC คือ-O0โหมดแก้ไขข้อบกพร่องซึ่งเต็มไปด้วยเสียงร้านค้า / โหลดซ้ำและมักจะไม่น่าสนใจที่จะดู)
Peter Cordes

ฉันเปลี่ยนประเภทเป็น "long long" หลังจากอ่านความคิดเห็นทั้งหมด ผลลัพธ์นั้นน่าสนใจ: ผลลัพธ์ที่รอได้รับและการตั้งค่าตัวนับบางอย่างสามารถปรับปรุงแนวคิดอื่น ๆ ได้ว่าอัตราข้อมูลที่ไม่ตรงกันนั้นได้รับอิทธิพลจากส่วนที่เหลือของรหัสอย่างไร ขอบคุณสำหรับความช่วยเหลือ!
Daniel Bandeira
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.