cout ซิงโครไนซ์ / เธรดปลอดภัยหรือไม่?


112

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

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

ขึ้นอยู่กับผู้ขายรายนี้หรือไม่? gcc ทำอะไร?


สำคัญ : โปรดระบุข้อมูลอ้างอิงสำหรับคำตอบของคุณหากคุณตอบว่า "ใช่" เนื่องจากฉันต้องการหลักฐานบางอย่างเกี่ยวกับเรื่องนี้

ข้อกังวลของฉันไม่ได้เกี่ยวกับการเรียกระบบพื้นฐานซึ่งเป็นสิ่งที่ดี แต่สตรีมเพิ่มชั้นของการบัฟเฟอร์ที่ด้านบน


2
ขึ้นอยู่กับผู้ขาย C ++ (ก่อน C ++ 0x) ไม่มีความคิดของหลายเธรด
Sven

2
แล้ว c ++ 0x ล่ะ? มันกำหนดแบบจำลองหน่วยความจำและเธรดคืออะไรดังนั้นบางทีสิ่งเหล่านี้อาจหยดลงในเอาต์พุต?
rubenvb

2
มีผู้ขายรายใดบ้างที่ทำให้เธรดปลอดภัย?
edA-qa mort-ora-y

ใครมีลิงค์ไปยังมาตรฐานล่าสุดที่เสนอ C ++ 2011?
edA-qa mort-ora-y

4
ในบางแง่นี่คือจุดที่printfส่องแสงเมื่อเอาต์พุตสมบูรณ์ถูกเขียนลงstdoutในช็อตเดียว เมื่อใช้std::coutการเชื่อมโยงของห่วงโซ่การแสดงออกแต่ละคนจะถูกส่งออกแยกstdout; ในระหว่างนั้นอาจมีการเขียนเธรดอื่น ๆstdoutเนื่องจากคำสั่งสุดท้ายของเอาต์พุตจะยุ่งเหยิง
legends2k

คำตอบ:


106

มาตรฐาน C ++ 03 ไม่ได้พูดอะไรเกี่ยวกับเรื่องนี้ เมื่อคุณไม่มีการรับประกันเกี่ยวกับความปลอดภัยของด้ายคุณควรปฏิบัติว่าไม่ปลอดภัยต่อด้าย

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

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

// in one thread
cout << "The operation took " << result << " seconds.";

// in another thread
cout << "Hello world! Hello " << name << "!";

คุณอาจต้องการให้แต่ละบรรทัดที่นี่ทำหน้าที่ในการยกเว้นซึ่งกันและกัน แต่การนำไปใช้งานจะรับประกันได้อย่างไร?

ใน C ++ 11 เรามีการค้ำประกันบางอย่าง FDIS กล่าวว่าสิ่งต่อไปนี้ใน§27.4.1 [iostream.objects.overview]:

การเข้าถึงพร้อมกันเพื่อซิงโครไนซ์ (§27.5.3.4) อินพุตมาตรฐานของอ็อบเจ็กต์ iostream ที่จัดรูปแบบและไม่ได้จัดรูปแบบ (§27.7.2.1) และเอาต์พุต (§27.7.3.1) หรือสตรีม C มาตรฐานโดยหลายเธรดจะไม่ส่งผลให้เกิดการแย่งชิงข้อมูล (§ 1.10) [หมายเหตุ: ผู้ใช้ยังคงต้องซิงโครไนซ์การใช้อ็อบเจ็กต์และสตรีมเหล่านี้พร้อมกันโดยใช้เธรดหลายเธรดหากต้องการหลีกเลี่ยงอักขระที่แทรกสลับกัน - หมายเหตุ]

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


2
ในทางเทคนิคจริงสำหรับ C ++ 98 / C ++ 03 แต่ฉันคิดว่าทุกคนรู้ดี แต่สิ่งนี้ไม่ตอบคำถามที่น่าสนใจสองคำถาม: แล้ว C ++ 0x ล่ะ? สิ่งใดที่ใช้งานทั่วไปจริงทำ ?
Nemo

1
@ edA-qa mort-ora-y: ไม่คุณคิดผิด C ++ 11 กำหนดไว้อย่างชัดเจนว่าออบเจ็กต์สตรีมมาตรฐานสามารถซิงโครไนซ์และคงพฤติกรรมที่กำหนดไว้อย่างดีไม่ใช่ว่าเป็นค่าเริ่มต้น
ildjarn

12
@ildjarn - ไม่ใช่ @ edA-qa mort-ora-y ถูกต้อง ตราบใดที่cout.sync_with_stdio()เป็นจริงการใช้coutเพื่อส่งออกอักขระจากหลายเธรดโดยไม่มีการซิงโครไนซ์เพิ่มเติมนั้นมีการกำหนดไว้อย่างดี แต่จะกำหนดเฉพาะในระดับของแต่ละไบต์เท่านั้น ดังนั้นcout << "ab";และcout << "cd"ดำเนินการในเธรดที่แตกต่างกันอาจส่งออกacdbตัวอย่างเช่น แต่อาจไม่ทำให้เกิดพฤติกรรมที่ไม่ได้กำหนด
JohannesD

4
@JohannesD: เราอยู่ในข้อตกลงที่นั่น - ซิงโครไนซ์กับ C API ที่เป็นพื้นฐาน ประเด็นของฉันคือมันไม่ "ซิงโครไนซ์" ในลักษณะที่เป็นประโยชน์กล่าวคือยังต้องมีการซิงโครไนซ์ด้วยตนเองหากพวกเขาไม่ต้องการข้อมูลขยะ
ildjarn

2
@ildjarn ฉันสบายดีกับข้อมูลขยะบิตที่ฉันเข้าใจ ฉันสนใจแค่สภาพการแย่งชิงข้อมูลซึ่งดูเหมือนจะชัดเจนแล้ว
edA-qa mort-ora-y

16

นี่เป็นคำถามที่ดี

ประการแรก C ++ 98 / C ++ 03 ไม่มีแนวคิดเรื่อง "เธรด" ดังนั้นในโลกนั้นคำถามจึงไม่มีความหมาย

แล้ว C ++ 0x ล่ะ? ดูคำตอบของ Martinho (ซึ่งฉันยอมรับว่าทำให้ฉันประหลาดใจ)

การใช้งานเฉพาะก่อน C ++ 0x เป็นอย่างไร ตัวอย่างเช่นนี่คือซอร์สโค้ดสำหรับbasic_streambuf<...>:sputcจาก GCC 4.5.2 (ส่วนหัว "streambuf"):

 int_type
 sputc(char_type __c)
 {
   int_type __ret;
   if (__builtin_expect(this->pptr() < this->epptr(), true)) {
       *this->pptr() = __c;
        this->pbump(1);
        __ret = traits_type::to_int_type(__c);
      }
    else
        __ret = this->overflow(traits_type::to_int_type(__c));
    return __ret;
 }

เห็นได้ชัดว่าการดำเนินการนี้ไม่มีการล็อก และไม่ทำxsputnเช่นกัน และนี่คือประเภทของ streambuf ที่ cout ใช้

เท่าที่ฉันสามารถบอกได้ libstdc ++ ไม่มีการล็อกการดำเนินการสตรีมใด ๆ และฉันจะไม่คาดหวังใด ๆ เพราะมันจะช้า

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

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

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

บทสรุปสำหรับผู้บริหาร: "ฉันจะไม่ทำ" สร้างคลาสการบันทึกที่ทำการล็อกที่เหมาะสมหรือย้ายไปที่ C ++ 0x

คุณสามารถตั้งค่า cout เป็น unbuffered มีแนวโน้มว่า (แม้ว่าจะไม่รับประกัน) ที่จะข้ามตรรกะทั้งหมดที่เกี่ยวข้องกับบัฟเฟอร์และการโทรwriteโดยตรง แม้ว่านั่นอาจจะช้าไปบ้าง


1
คำตอบที่ดี แต่ดูคำตอบของ Martinho ซึ่งแสดงให้เห็นว่า C ++ 11 กำหนดการซิงโครไนซ์สำหรับcout.
edA-qa mort-ora-y

7

มาตรฐาน C ++ ไม่ได้ระบุว่าการเขียนไปยังสตรีมนั้นปลอดภัยสำหรับเธรดหรือไม่ แต่โดยปกติแล้วจะไม่ใช่

www.techrepublic.com/article/use-stl-streams-for-easy-c-plus-plus-thread-safe-logging

และ: สตรีมเอาต์พุตมาตรฐานในเธรด C ++ ปลอดภัยหรือไม่ (cout, cerr, Clog)

UPDATE

โปรดดูคำตอบของ @Martinho Fernandes เพื่อทราบว่า C ++ 11 มาตรฐานใหม่บอกอะไรเกี่ยวกับเรื่องนี้


3
ฉันเดาว่าเนื่องจาก C ++ 11 เป็นมาตรฐานแล้วคำตอบนี้ผิดจริงในขณะนี้
edA-qa mort-ora-y

6

ดังที่คำตอบอื่น ๆ กล่าวถึงนี่เป็นเรื่องเฉพาะสำหรับผู้ขายเนื่องจากมาตรฐาน C ++ ไม่ได้กล่าวถึงเธรด (การเปลี่ยนแปลงนี้ใน C ++ 0x)

GCC ไม่ได้ให้คำมั่นสัญญามากมายเกี่ยวกับความปลอดภัยของเธรดและ I / O แต่เอกสารสำหรับสิ่งที่สัญญาอยู่ที่นี่:

สิ่งสำคัญน่าจะเป็น:

ประเภท __basic_file เป็นเพียงชุดของ wrapper ขนาดเล็กรอบ ๆ เลเยอร์ C stdio (ดูลิงค์ใต้โครงสร้างอีกครั้ง) เราไม่ได้ล็อคตัวเอง แต่เพียงแค่ส่งต่อไปยัง fopen, fwrite และอื่น ๆ

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

(ดังตัวอย่างเช่นมาตรฐาน POSIX กำหนดให้การดำเนินการ C stdio FILE * เป็นอะตอมไลบรารี C ที่สอดคล้องกับ POSIX (เช่นบน Solaris และ GNU / Linux) มี mutex ภายในเพื่อทำให้การดำเนินการเป็นอนุกรมบน FILE * s อย่างไรก็ตามคุณยังต้อง อย่าทำอะไรโง่ ๆ เช่นเรียก fclose (fs) ในเธรดหนึ่งตามด้วยการเข้าถึง fs ในอีกเธรด)

ดังนั้นหากไลบรารี C ของแพลตฟอร์มของคุณปลอดภัยสำหรับเธรดการดำเนินการ fstream I / O ของคุณจะปลอดภัยในระดับต่ำสุด สำหรับการดำเนินการในระดับที่สูงขึ้นเช่นการจัดการข้อมูลที่อยู่ในคลาสการจัดรูปแบบสตรีม (เช่นการตั้งค่าการเรียกกลับภายใน std :: ofstream) คุณต้องป้องกันการเข้าถึงดังกล่าวเช่นเดียวกับทรัพยากรที่ใช้ร่วมกันที่สำคัญอื่น ๆ

ฉันไม่รู้ว่ามีอะไรเปลี่ยนแปลงหรือไม่ในกรอบเวลา 3.0 ที่กล่าวถึง

เอกสารความปลอดภัยเธรดของ MSVC iostreamsสามารถพบได้ที่นี่: http://msdn.microsoft.com/en-us/library/c9ceah3b.aspx :

อ็อบเจ็กต์เดียวคือเธรดที่ปลอดภัยสำหรับการอ่านจากเธรดหลายเธรด ตัวอย่างเช่นเมื่อกำหนดอ็อบเจ็กต์ A การอ่าน A จากเธรด 1 และเธรด 2 พร้อมกันนั้นปลอดภัย

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

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

...

คลาส iostream

คลาส iostream เป็นไปตามกฎเดียวกันกับคลาสอื่น ๆ โดยมีข้อยกเว้นหนึ่งข้อ ปลอดภัยในการเขียนไปยังวัตถุจากหลายเธรด ตัวอย่างเช่นเธรด 1 สามารถเขียนไปยัง cout ในเวลาเดียวกันกับเธรด 2 อย่างไรก็ตามสิ่งนี้สามารถส่งผลให้เอาต์พุตจากเธรดสองเธรดถูกผสมกัน

หมายเหตุ: การอ่านจากบัฟเฟอร์สตรีมไม่ถือเป็นการดำเนินการอ่าน ควรถือเป็นการดำเนินการเขียนเนื่องจากจะเปลี่ยนสถานะของคลาส

โปรดทราบว่าข้อมูลนี้เป็นข้อมูลสำหรับ MSVC เวอร์ชันล่าสุด (ปัจจุบันสำหรับ VS 2010 / MSVC 10 / cl.exe16.x) คุณสามารถเลือกข้อมูลสำหรับ MSVC เวอร์ชันเก่าได้โดยใช้การควบคุมแบบเลื่อนลงบนหน้า (และข้อมูลจะแตกต่างกันสำหรับเวอร์ชันเก่า)


1
"ฉันไม่รู้ว่ามีอะไรเปลี่ยนแปลงหรือไม่ในกรอบเวลา 3.0 ที่กล่าวถึง" มันทำได้แน่นอน ในช่วงหลายปีที่ผ่านมาการใช้งานสตรีม g ++ ได้ทำการบัฟเฟอร์ของตัวเอง
Nemo
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.