เต็มไปด้วยข้อบกพร่องแบบมัลติเธรด


26

ในทีมใหม่ของฉันที่ฉันจัดการรหัสส่วนใหญ่ของเราคือแพลตฟอร์มซ็อกเก็ต TCP และรหัสเครือข่าย http C ++ ทั้งหมด ส่วนใหญ่มาจากผู้พัฒนารายอื่นที่ออกจากทีมไป นักพัฒนาปัจจุบันของทีมนั้นฉลาดมาก แต่ส่วนใหญ่เป็นรุ่นรองในแง่ของประสบการณ์

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

สิ่งที่ทำให้ปัญหาเหล่านี้ยากขึ้นคือความพยายามแก้ไขมักไม่ถูกต้อง ข้อผิดพลาดบางประการที่ฉันสังเกตเห็นว่าทีมพยายาม (หรือภายในรหัสเดิม) รวมถึงสิ่งต่อไปนี้:

ข้อผิดพลาดทั่วไป # 1 - แก้ไขปัญหาการเกิดพร้อมกันโดยเพียงแค่ใส่ล็อกรอบ ๆ ข้อมูลที่ใช้ร่วมกัน แต่ลืมเกี่ยวกับสิ่งที่เกิดขึ้นเมื่อวิธีการไม่ได้รับการเรียกในลำดับที่คาดหวัง นี่เป็นตัวอย่างง่ายๆ:

void Foo::OnHttpRequestComplete(statuscode status)
{
    m_pBar->DoSomethingImportant(status);
}

void Foo::Shutdown()
{
    m_pBar->Cleanup();
    delete m_pBar;
    m_pBar=nullptr;
}

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

void Foo::OnHttpRequestComplete(statuscode status)
{
    AutoLock lock(m_cs);
    m_pBar->DoSomethingImportant(status);
}

void Foo::Shutdown()
{
    AutoLock lock(m_cs);
    m_pBar->Cleanup();
    delete m_pBar;
    m_pBar=nullptr;
}

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

สามัญข้อผิดพลาด # 2 - แก้ไขปัญหาการหยุดชะงักโดยสุ่มล็อกออกจากล็อกรอให้เธรดอื่นเสร็จสิ้นจากนั้นจึงล็อคอีกครั้ง

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

มีกรณีขอบอื่น ๆ แต่บรรทัดล่างคือ:

การเขียนโปรแกรมแบบมัลติเธรดเป็นเรื่องยากธรรมดาแม้แต่คนฉลาด

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

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

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

ก่อนที่ฉันจะลงไปที่เส้นทางนั้นฉันก็สนใจเช่นกันถ้ามีเทคนิคมาตรฐานอื่น ๆ หรือรูปแบบการออกแบบสำหรับจัดการกับปัญหาแบบมัลติเธรด และฉันต้องเน้น - บางสิ่งบางอย่างนอกเหนือจากหนังสือที่อธิบายพื้นฐานของ mutexes และ semaphores คุณคิดอย่างไร?

ฉันยังสนใจในวิธีการอื่นเพื่อนำไปสู่กระบวนการฟื้นฟู รวมถึงสิ่งต่อไปนี้:

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

  2. วิธีทำแผนภาพเกลียวของส่วนประกอบต่าง ๆ เพื่อให้ง่ายต่อการศึกษาและพัฒนาวิธีแก้ปัญหา (นั่นคือเทียบเท่า UML สำหรับการพูดคุยหัวข้อข้ามวัตถุและชั้นเรียน)

  3. ให้ความรู้แก่ทีมพัฒนาของคุณเกี่ยวกับปัญหาด้วยรหัสหลายเธรด

  4. คุณจะทำอย่างไร


23
บางคนเมื่อเผชิญกับปัญหาคิดว่าฉันจะใช้หลายเธรด ตอนนี้พวกเขามีโพรบ Twolm
Tom Squires

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

3
ยินดีต้อนรับสู่นรกแบบมัลติเธรด ฉันเขียนโปรแกรมหลายเธรด / paralell เป็นเวลา> 20 ปีใน Ada, Occam, C ++ มันไม่ง่ายเลยทุกอย่างต้องใช้ความคิดอย่างระมัดระวังและใครก็ตามที่บอกว่า "มันง่ายแค่ทำ X" คือคนโง่ที่ไม่เข้าใจสิ่งที่เกิดขึ้นจริง โชคดี.
quick_now

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

3
@DeadMG ฉันจะยืนยันว่าภาวะพร้อมกันของรัฐที่ใช้ร่วมกันมีแนวโน้มที่จะเกิดกรณีมุมแปลก ๆ และควรหลีกเลี่ยง แต่เฮ้ฉันเขียนหนังสือเกี่ยวกับ Erlang
Zachary K

คำตอบ:


27

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

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

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

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

std::shared_ptrใช้ เมื่อเธรดทั้งหมดถูกปล่อยออกมา (และไม่มีใครเลยสามารถเรียกใช้ฟังก์ชันได้เนื่องจากไม่มีตัวชี้ไปที่มัน) ดังนั้น destructor จะถูกเรียกใช้ รับประกันความปลอดภัย

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


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

5
@ Koncurrency: เครื่องมือที่ไม่น่าเชื่อถือเป็นสิ่งที่ต้องการจัดการหน่วยความจำด้วยตนเองหรือการประสานของคุณเองในทางทฤษฎีมันแก้ปัญหา X แต่ในความเป็นจริงมันแย่มากที่คุณสามารถรับประกันความผิดพลาดขนาดใหญ่และวิธีเดียวที่จะแก้ปัญหาได้ ในระดับที่สมเหตุสมผลคือการลงทุนครั้งใหญ่และไม่สมส่วนซึ่งเป็นสิ่งที่คุณมีอยู่ในตอนนี้
DeadMG

9

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

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

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

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

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


+1 สำหรับข้อเสนอแนะระดับกลางที่สมเหตุสมผลในการผ่านความท้าทายที่มีอยู่

ใช่นี่เป็นวิธีที่ฉันกำลังตรวจสอบ คุณเพิ่มคะแนนที่ดีเกี่ยวกับประสิทธิภาพ
koncurrency

การเปลี่ยนสิ่งต่าง ๆ ให้ผ่านไปในเธรดเดี่ยวนั้นไม่ได้ฟังเหมือนแพทช์สั้น ๆ แต่เป็นตัวปรับเปลี่ยนครั้งใหญ่ให้ฉัน
เซบาสเตียนเรดล

8

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

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


แค่อ่านตอนนี้หลังจากคำตอบของฉันได้รับการโหวตแล้ว แค่อยากจะบอกว่าผมรักประโยคเบื้องต้น :)
back2dos

7

หากคุณมีเวลาที่จะอุทิศใบสมัครของคุณอีกครั้งฉันขอแนะนำให้คุณดูที่โมเดลนักแสดง (ดูเช่นTheron , Casablanca , libcppa , CAFสำหรับการใช้งาน C ++)

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

  1. รับข้อความ
  2. ทำการคำนวณ
  3. ส่งข้อความ / สร้าง / ฆ่านักแสดงคนอื่น

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

ฉันใช้รุ่นนี้ในโครงการของฉันมาหลายเดือนแล้วและฉันก็รู้สึกทึ่งกับความแข็งแกร่งของมัน


1
ห้องสมุด Akka สำหรับ Scala เป็นการดำเนินการที่ดีซึ่งคิดอย่างมากเกี่ยวกับวิธีการฆ่านักแสดงผู้ปกครองเมื่อเด็กตายหรือกลับกัน ฉันรู้ว่ามันไม่ใช่ C ++ แต่คุ้มค่ากับการดู: akka.io
GlenPeterson

1
@GlenPeterson: ขอบคุณฉันรู้เกี่ยวกับ akka (ซึ่งฉันพิจารณาโซลูชันที่น่าสนใจที่สุดในขณะนี้และทำงานได้ทั้งกับ Java และ Scala) แต่คำถามอยู่เฉพาะ C ++ มิฉะนั้นเหตุการณ์หนึ่งอาจพิจารณา Erlang ฉันเดาว่า Erlang อาการปวดหัวทั้งหมดของการเขียนโปรแกรมแบบมัลติเธรดหายไปได้ดี แต่บางทีเฟรมเวิร์กอย่าง Akka อาจเข้ามาใกล้มาก
Giorgio

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

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

ผู้ลงคะแนนโปรดแสดงความคิดเห็นและแนะนำว่าฉันจะปรับปรุงคำตอบนี้ได้อย่างไร?
Giorgio

6

ข้อผิดพลาดทั่วไป # 1 - แก้ไขปัญหาการเกิดพร้อมกันโดยเพียงแค่ใส่ล็อกรอบ ๆ ข้อมูลที่ใช้ร่วมกัน แต่ลืมเกี่ยวกับสิ่งที่เกิดขึ้นเมื่อวิธีการไม่ได้รับการเรียกในลำดับที่คาดหวัง นี่เป็นตัวอย่างง่ายๆ:

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

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

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


2

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

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

หากคุณแทนที่ Autolock ด้วย wrapper และแมโครคุณสามารถทำได้

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

นอกจากนี้คุณยังจะต้องมีกราฟ dominator คงที่

ตอนนี้อยู่ในล็อคคุณต้องอัปเดตกราฟ dominator และหากคุณได้รับการเปลี่ยนแปลงการสั่งซื้อคุณยืนยันข้อผิดพลาดและยกเลิก

หลังจากการทดสอบอย่างละเอียดคุณอาจกำจัดการหยุดชะงักส่วนใหญ่

รหัสถูกทิ้งไว้เป็นแบบฝึกหัดสำหรับนักเรียน

ปัญหา # 2 จะหายไป (ส่วนใหญ่)

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

  • ผ่านวัตถุที่ไม่เปลี่ยนรูปหรือทำสำเนาของพวกเขาก่อนที่จะผ่าน
  • อย่าแชร์ข้อมูลผ่านตัวแปรสาธารณะหรือผู้ได้รับ

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

  • การเปลี่ยนแปลงข้อมูลที่ข้ามเธรดกลายเป็น qeuue ที่ปลอดภัยต่อเธรดจัดการโดยหนึ่งเธรด ทำการสมัครสมาชิก ตอนนี้คุณสามารถเรียงลำดับเหตุผลเกี่ยวกับการไหลของข้อมูล

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

นี่เป็นนางแบบนักแสดงที่ราคาถูก ลิงก์ของ Giorgio จะช่วย

ในที่สุดปัญหาของคุณกับวัตถุปิด

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


2

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

มหาวิทยาลัย Kent มีการติดตั้ง C ++ ( https://www.cs.kent.ac.uk/projects/ofa/c++csp/ ) ซึ่งโคลนที่https://github.com/themasterchef/cppcsp2 )


1

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

ฉันกำลังอ่านสิ่งนี้อยู่และอธิบายปัญหาทั้งหมดที่คุณจะได้รับและวิธีหลีกเลี่ยงใน C ++ (โดยใช้ไลบรารีเธรดใหม่ แต่ฉันคิดว่าการอธิบายทั่วโลกใช้ได้กับกรณีของคุณ): http: //www.amazon co.th / C-Concurrency-กระทำการปฏิบัติ-Multithreading / DP / 1933988770 / โทษ = sr_1_1? เช่น = UTF8 & qid = 1337934534 & sr = 8-1

วิธีทำแผนภาพเกลียวของส่วนประกอบต่าง ๆ เพื่อให้ง่ายต่อการศึกษาและพัฒนาวิธีแก้ปัญหา (นั่นคือเทียบเท่า UML สำหรับการพูดคุยหัวข้อข้ามวัตถุและชั้นเรียน)

ฉันใช้ UML ที่เรียบง่ายเป็นการส่วนตัวและแค่คิดว่าข้อความนั้นทำแบบอะซิงโครนัส นอกจากนี้ยังเป็นจริงระหว่าง "โมดูล" แต่ภายในโมดูลที่ฉันไม่ต้องการรู้

ให้ความรู้แก่ทีมพัฒนาของคุณเกี่ยวกับปัญหาด้วยรหัสหลายเธรด

หนังสือเล่มนี้จะช่วยได้ แต่ฉันคิดว่าการออกกำลังกาย / การสร้างต้นแบบและที่ปรึกษาที่มีประสบการณ์น่าจะดีกว่า

คุณจะทำอย่างไร

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


ขอบคุณสำหรับคำแนะนำหนังสือ ฉันอาจจะหยิบมันขึ้นมา
koncurrency

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

@GlenPeterson เห็นด้วยตอนนี้ฉันมีประสบการณ์มากขึ้น (ตั้งแต่คำตอบนี้) ฉันพบว่าเราต้องการ abstractions ที่ดีกว่าเพื่อให้สามารถจัดการได้และกีดกันการแบ่งปันข้อมูล โชคดีที่นักออกแบบภาษาดูเหมือนจะทำงานอย่างหนักในเรื่องนี้
Klaim

ฉันประทับใจ Scala มากโดยเฉพาะการนำผลประโยชน์ด้านการเขียนโปรแกรมที่ใช้งานไม่ได้มาปรับใช้กับ Java ซึ่งเป็นผู้สืบทอดโดยตรงของ C ++ มันรันบน Java Virtual Machine ดังนั้นอาจไม่มีประสิทธิภาพที่คุณต้องการ หนังสือของ Joshua Bloch "Effective Java" เป็นข้อมูลเกี่ยวกับการลดความไม่แน่นอนให้น้อยที่สุดสร้างอินเตอร์เฟสที่ปลอดภัย แม้ว่าจะเป็นจาวาก็ตาม แต่ฉันคิดว่าคุณสามารถใช้ 80-90% ของ C ++ ได้ การตั้งคำถามกับสถานะความไม่แน่นอนและสถานะที่แชร์ (หรือความไม่แน่นอนของสถานะที่แชร์) ในการตรวจสอบรหัสของคุณอาจเป็นขั้นตอนแรกที่ดีสำหรับคุณ
GlenPeterson

1

คุณกำลังไปแล้วโดยรับทราบปัญหาและหาวิธีแก้ไขอย่างแข็งขัน นี่คือสิ่งที่ฉันจะทำ:

  • นั่งลงและออกแบบแบบจำลองเกลียวสำหรับแอปพลิเคชันของคุณ นี่คือเอกสารที่ตอบคำถามเช่น: คุณมีเธรดชนิดใด สิ่งที่ควรทำในหัวข้อใด คุณควรใช้รูปแบบการซิงโครไนซ์ประเภทใด กล่าวอีกนัยหนึ่งควรอธิบาย "กฎการมีส่วนร่วม" เมื่อต้องต่อสู้กับปัญหามัลติเธรด
  • ใช้เครื่องมือวิเคราะห์เธรดเพื่อตรวจสอบข้อผิดพลาดของ codebase Valgrind มีตัวตรวจสอบเธรดชื่อHelgrindซึ่งสามารถตรวจจับสิ่งต่าง ๆ เช่นสถานะที่ใช้ร่วมกันได้โดยไม่ต้องทำการซิงโครไนซ์ที่เหมาะสม มีเครื่องมือที่ดีอื่น ๆ ออกไปให้ค้นหาพวกเขา
  • พิจารณาการย้ายออกจาก C ++ C ++ เป็นฝันร้ายที่จะเขียนโปรแกรมที่เกิดขึ้นพร้อมกันตัวเลือกส่วนตัวของฉันคือErlangแต่นั่นเป็นเรื่องของรสนิยม

8
-1 แน่นอนสำหรับบิตสุดท้าย ดูเหมือนว่ารหัสของ OP ใช้เครื่องมือดั้งเดิมที่สุดไม่ใช่เครื่องมือ C ++ จริง
DeadMG

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

4
@JesperE - ขอโทษ แต่ไม่ การเห็นพ้องด้วยกันใน C ++ เป็นเพียงฝันร้ายหากคุณทำให้มันเกิดขึ้นโดยไปที่ระดับต่ำเกินไป ใช้เธรดนามธรรมที่เหมาะสมและมันก็ไม่ได้แย่ไปกว่าภาษาหรือรันไทม์อื่น ๆ และด้วยโครงสร้างแอปพลิเคชันที่เหมาะสมจริงๆแล้วมันค่อนข้างง่ายเหมือนอย่างอื่นที่ฉันเคยเห็น
Michael Kohne

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

1
@JesperE: ฉันเห็นด้วยกับคุณ รูปแบบ Erlang (ซึ่งมีการใช้งานสำหรับ Scala / Java, Ruby และเท่าที่ฉันรู้สำหรับ C ++) มีประสิทธิภาพมากกว่าการเขียนโค้ดโดยตรงด้วยเธรด
จอร์โจ

1

ดูตัวอย่างของคุณ: ทันทีที่ Foo :: Shutdown เริ่มดำเนินการคุณจะไม่สามารถเรียก OnHttpRequestComplete ให้ทำงานได้อีกต่อไป มันไม่สามารถทำงานได้เลย

นอกจากนี้คุณยังสามารถยืนยันได้ว่า Foo :: Shutdown ไม่ควรเรียกในขณะที่การเรียกไปยัง OnHttpRequestComplete กำลังทำงานอยู่ (จริง ๆ แน่นอน) และอาจไม่หากการเรียกไปยัง OnHttpRequestComplete ยังคงโดดเด่นอยู่

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

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

คุณต้องทำอะไร: เพิ่มรายละเอียดให้กับฟังก์ชั่นของคุณโดยบอกว่าพวกเขาจะทำอะไร (ตัวอย่างเช่นการเริ่มต้นคำขอ http อาจล้มเหลวหลังจากปิดใช้งาน hase แล้ว) จากนั้นเขียนฟังก์ชันของคุณเพื่อให้ตรงตามข้อกำหนด

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


0

คุณจะทำอย่างไร

พูดตามตรง ฉันวิ่งหนีเร็ว

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

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

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

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