บทความที่ขาดหายไปเกี่ยวกับ Qt Multithreading ใน C ++

เผยแพร่แล้ว: 2022-03-11

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

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

– วิกิพีเดีย

เป้าหมายของบทความนี้คือการรวมความรู้ที่จำเป็นเกี่ยวกับการเขียนโปรแกรมพร้อมกันกับเฟรมเวิร์ก Qt โดยเฉพาะหัวข้อที่เข้าใจผิดมากที่สุด ผู้อ่านคาดว่าจะมีพื้นฐานก่อนหน้านี้ใน Qt และ C ++ เพื่อทำความเข้าใจเนื้อหา

การเลือกระหว่างการใช้ QThreadPool และ QThread

กรอบงาน Qt มีเครื่องมือมากมายสำหรับการทำมัลติเธรด การเลือกเครื่องมือที่เหมาะสมอาจเป็นเรื่องที่ท้าทายในตอนแรก แต่อันที่จริง โครงสร้างการตัดสินใจประกอบด้วยสองตัวเลือก: คุณต้องการให้ Qt จัดการเธรดให้กับคุณ หรือคุณต้องการจัดการเธรดด้วยตัวเอง อย่างไรก็ตาม มีเกณฑ์สำคัญอื่นๆ:

  1. งานที่ไม่ต้องการวนรอบเหตุการณ์ โดยเฉพาะงานที่ไม่ได้ใช้กลไกสัญญาณ/สล็อตระหว่างการดำเนินการงาน
    ใช้: QtConcurrent และ QThreadPool + QRunnable

  2. งานที่ใช้สัญญาณ/สล็อตและดังนั้นจึงจำเป็นต้องมีการวนรอบเหตุการณ์
    ใช้: ย้ายวัตถุของผู้ปฏิบัติงานไปที่ + QThread

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

 class MyTask : public QObject, public QRunnable { Q_OBJECT public: void MyTask::run() { _loop.exec(); } public slots: // you need a signal connected to this slot to exit the loop, // otherwise the thread running the loop would remain blocked... void finishTask() { _loop.exit(); } private: QEventLoop _loop; }

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

ข้อความแสดงแทน

คุณยังสามารถเรียกใช้ QThread โดยไม่มีเหตุการณ์วนซ้ำได้ด้วยการแทนที่ QThread::run() และวิธีนี้ก็ใช้ได้อย่างสมบูรณ์ ตราบใดที่คุณรู้ว่าคุณกำลังทำอะไรอยู่ ตัวอย่างเช่น อย่าคาดหวังให้ method quit() ทำงานในกรณีดังกล่าว

เรียกใช้อินสแตนซ์งานครั้งละหนึ่งรายการ

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

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

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

 void logEvent(const QString & event) { static QMutex lock; QMutexLocker locker(& lock); // high contention! logStream << event; // exclusive resource }

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

  • เราสามารถใช้ QThreadPool ได้เนื่องจากมีคิวงานที่ค้างอยู่

หรือ

  • เราสามารถใช้ QThread::run() เริ่มต้นได้เพราะมี QEventLoop

ตัวเลือกแรกคือการใช้ QThreadPool เราสามารถสร้างอินสแตนซ์ QThreadPool และใช้ QThreadPool::setMaxThreadCount(1) จากนั้นเราสามารถใช้ QtConcurrent::run() เพื่อกำหนดเวลาคำขอ:

 class Logger: public QObject { public: explicit Logger(QObject *parent = nullptr) : QObject(parent) { threadPool.setMaxThreadCount(1); } void logEvent(const QString &event) { QtConcurrent::run(&threadPool, [this, event]{ logEventCore(event); }); } private: void logEventCore(const QString &event) { logStream << event; } QThreadPool threadPool; };

โซลูชันนี้มีข้อดีอย่างหนึ่ง: QThreadPool::clear() ช่วยให้คุณ ยกเลิก คำขอที่รอดำเนินการทั้งหมดได้ทันที เช่น เมื่อแอปพลิเคชันของคุณต้องปิดตัวลงอย่างรวดเร็ว อย่างไรก็ตาม ยังมีข้อเสียที่สำคัญที่เชื่อมต่อกับ thread-affinity : ฟังก์ชัน logEventCore มีแนวโน้มที่จะดำเนินการในเธรดต่างๆ จากการเรียกหนึ่งไปยังอีกการโทรหนึ่ง และเรารู้ว่า Qt มีคลาสบางคลาสที่ต้องการ thread-affinity : QTimer , QTcpSocket และอาจมีบางคลาสอื่นๆ

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

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

 // the worker that will be moved to a thread class LogWorker: public QObject { Q_OBJECT public: explicit LogWorker(QObject *parent = nullptr); public slots: // this slot will be executed by event loop (one call at a time) void logEvent(const QString &event); };

การใช้งานตัวสร้าง LogWorker และ logEvent นั้นตรงไปตรงมาและไม่ได้ระบุไว้ที่นี่ ตอนนี้เราต้องการบริการที่จะจัดการเธรดและอินสแตนซ์ของผู้ปฏิบัติงาน:

 // interface class LogService : public QObject { Q_OBJECT public: explicit LogService(QObject *parent = nullptr); ~LogService(); signals: // to use the service, just call this signal to send a request: // logService->logEvent("event"); void logEvent(const QString &event); private: QThread *thread; LogWorker *worker; }; // implementation LogService::LogService(QObject *parent) : QObject(parent) { thread = new QThread(this); worker = new LogWorker; worker->moveToThread(thread); connect(this, &LogService::logEvent, worker, &LogWorker::logEvent); connect(thread, &QThread::finished, worker, &QObject::deleteLater); thread->start(); } LogService::~LogService() { thread->quit(); thread->wait(); } 

ข้อความแสดงแทน

มาพูดคุยกันถึงวิธีการทำงานของรหัสนี้:

  • ในตัวสร้าง เราสร้างเธรดและอินสแตนซ์ของผู้ปฏิบัติงาน ขอให้สังเกตว่าผู้ปฏิบัติงานไม่ได้รับพาเรนต์ เนื่องจากจะถูกย้ายไปยังเธรดใหม่ ด้วยเหตุนี้ Qt จึงไม่สามารถปล่อยหน่วยความจำของผู้ปฏิบัติงานได้โดยอัตโนมัติ ดังนั้นเราจึงจำเป็นต้องทำสิ่งนี้โดยเชื่อมต่อสัญญาณ QThread::finished กับช่อง deleteLater นอกจากนี้เรายังเชื่อมต่อวิธีพร็อกซี LogService::logEvent() กับ LogWorker::logEvent() ซึ่งจะใช้โหมด Qt::QueuedConnection เนื่องจากเธรดต่างกัน
  • ใน destructor เราใส่เหตุการณ์ quit ลงในคิวของลูปเหตุการณ์ กิจกรรมนี้จะได้รับการจัดการ หลังจาก จัดการกิจกรรมอื่นทั้งหมดแล้ว ตัวอย่างเช่น ถ้าเราทำการ logEvent() หลายร้อยครั้งก่อนการเรียก destructor ตัวบันทึกจะจัดการทั้งหมดก่อนที่จะเรียกเหตุการณ์ quit แน่นอนว่าต้องใช้เวลา ดังนั้นเราต้อง wait() จนกว่าวงรอบเหตุการณ์จะออก เป็นเรื่องที่ควรค่าแก่การกล่าวถึงว่าคำขอบันทึกทั้งหมดในอนาคตที่โพสต์ หลังจาก เหตุการณ์การออกจากระบบจะไม่ได้รับการประมวลผล
  • การบันทึกตัวเอง ( LogWorker::logEvent ) จะทำในเธรดเดียวกันเสมอ ดังนั้น วิธีการนี้จึงทำงานได้ดีสำหรับคลาสที่ต้องการ thread-affinity ในเวลาเดียวกัน คอนสตรัคเตอร์และตัวทำลาย LogWorker จะถูกดำเนินการในเธรดหลัก (โดยเฉพาะเธรดที่ LogService กำลังทำงานอยู่) ดังนั้น คุณต้องระวังให้มากเกี่ยวกับโค้ดที่คุณรันที่นั่น โดยเฉพาะอย่าหยุดตัวจับเวลาหรือใช้ซ็อกเก็ตใน destructor ของผู้ปฏิบัติงาน เว้นแต่คุณจะสามารถเรียกใช้ destructor ในเธรดเดียวกันได้!

ดำเนินการ destructor ของผู้ปฏิบัติงานในเธรดเดียวกัน

หากพนักงานของคุณจัดการกับตัวจับเวลาหรือซ็อกเก็ต คุณต้องแน่ใจว่าตัวทำลายทำงานอยู่ในเธรดเดียวกัน (เธรดที่คุณสร้างสำหรับผู้ปฏิบัติงานและตำแหน่งที่คุณย้ายผู้ปฏิบัติงานไป) วิธีที่ชัดเจนในการสนับสนุนสิ่งนี้คือซับคลาส QThread และ delete ผู้ปฏิบัติงานภายใน QThread::run() พิจารณาเทมเพลตต่อไปนี้:

 template <typename TWorker> class Thread : QThread { public: explicit Thread(TWorker *worker, QObject *parent = nullptr) : QThread(parent), _worker(worker) { _worker->moveToThread(this); start(); } ~Thread() { quit(); wait(); } TWorker worker() const { return _worker; } protected: void run() override { QThread::run(); delete _worker; } private: TWorker *_worker; };

โดยใช้เทมเพลตนี้ เรากำหนด LogService ใหม่จากตัวอย่างก่อนหน้านี้:

 // interface class LogService : public Thread<LogWorker> { Q_OBJECT public: explicit LogService(QObject *parent = nullptr); signals: void **logEvent**(const QString &event); }; // implementation LogService::**LogService**(QObject *parent) : Thread<LogWorker>(new LogWorker, parent) { connect(this, &LogService::logEvent, worker(), &LogWorker::logEvent); }

มาพูดคุยกันว่ามันควรจะทำงานอย่างไร:

  • เราทำให้ LogService เป็นอ็อบเจ็กต์ QThread เนื่องจากเราจำเป็นต้องใช้ฟังก์ชัน custom run() เราใช้คลาสย่อยส่วนตัวเพื่อป้องกันการเข้าถึงฟังก์ชันของ QThread เนื่องจากเราต้องการควบคุมวงจรชีวิตของเธรดภายใน
  • ในฟังก์ชัน Thread::run() เราเรียกใช้การวนซ้ำเหตุการณ์โดยเรียกใช้การ QThread::run() ที่เป็นค่าเริ่มต้น และทำลายอินสแตนซ์ของผู้ปฏิบัติงานทันทีหลังจากที่วนรอบเหตุการณ์ออก โปรดทราบว่า destructor ของผู้ปฏิบัติงานถูกดำเนินการในเธรดเดียวกัน
  • LogService::logEvent() เป็นฟังก์ชันพร็อกซี (สัญญาณ) ที่จะโพสต์เหตุการณ์การบันทึกไปยังคิวเหตุการณ์ของเธรด

การหยุดชั่วคราวและเปิดเธรดต่อ

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

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

  • suspend() จะเป็นการบล็อกการโทรที่กำหนดเธรดที่รอในเงื่อนไขการรอ ซึ่งทำได้โดยการโพสต์คำขอระงับลงในคิวและรอจนกว่าจะได้รับการจัดการ ค่อนข้างคล้ายกับ QThread::quit() + wait()
  • resume() จะส่งสัญญาณถึงเงื่อนไขการรอเพื่อปลุกเธรดที่หลับอยู่เพื่อดำเนินการต่อไป

มาทบทวนอินเทอร์เฟซและการใช้งาน:

 // interface class SuspendableWorker : public QObject { Q_OBJECT public: explicit SuspendableWorker(QObject *parent = nullptr); ~SuspendableWorker(); // resume() must be called from the outer thread. void resume(); // suspend() must be called from the outer thread. // the function would block the caller's thread until // the worker thread is suspended. void suspend(); private slots: void suspendImpl(); private: QMutex _waitMutex; QWaitCondition _waitCondition; };
 // implementation SuspendableWorker::SuspendableWorker(QObject *parent) : QObject(parent) { _waitMutex.lock(); } SuspendableWorker::~SuspendableWorker() { _waitCondition.wakeAll(); _waitMutex.unlock(); } void SuspendableWorker::resume() { _waitCondition.wakeAll(); } void SuspendableWorker::suspend() { QMetaObject::invokeMethod(this, &SuspendableWorker::suspendImpl); // acquiring mutex to block the calling thread _waitMutex.lock(); _waitMutex.unlock(); } void SuspendableWorker::suspendImpl() { _waitCondition.wait(&_waitMutex); }

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

ข้อความแสดงแทน

 template <typename TWorker> class Thread : QThread { public: explicit Thread(TWorker *worker, QObject *parent = nullptr) : QThread(parent), _worker(worker) { _worker->moveToThread(this); start(); } ~Thread() { resume(); quit(); wait(); } void suspend() { auto worker = qobject_cast<SuspendableWorker*>(_worker); if (worker != nullptr) { worker->suspend(); } } void resume() { auto worker = qobject_cast<SuspendableWorker*>(_worker); if (worker != nullptr) { worker->resume(); } } TWorker worker() const { return _worker; } protected: void run() override { QThread::*run*(); delete _worker; } private: TWorker *_worker; };

ด้วยการเปลี่ยนแปลงเหล่านี้ เราจะกลับมาตั้งกระทู้ต่อก่อนที่จะโพสต์กิจกรรมการเลิก นอกจากนี้ Thread<TWorker> ยังคงอนุญาตให้ส่งผ่านผู้ปฏิบัติงานประเภทใดก็ได้โดยไม่คำนึงว่าจะเป็น SuspendableWorker หรือไม่

การใช้งานจะเป็นดังนี้:

 LogService logService; logService.logEvent("processed event"); logService.suspend(); logService.logEvent("queued event"); logService.resume(); // "queued event" is now processed.

ระเหยกับอะตอม

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

ลองพิจารณาตัวอย่างที่เป็นจริง: คลาสการเชื่อมต่อ TcpConnection ที่ทำงานในเธรดเฉพาะ และเราต้องการให้คลาสนี้ส่งออกวิธีการที่ปลอดภัยสำหรับเธรด: bool isConnected() ภายใน คลาสจะฟังเหตุการณ์ซ็อกเก็ต: connected และ disconnected เพื่อรักษาแฟล็กบูลีนภายใน:

 // pseudo-code, won't compile class TcpConnection : QObject { Q_OBJECT public: // this is not thread-safe! bool isConnected() const { return _connected; } private slots: void handleSocketConnected() { _connected = true; } void handleSocketDisconnected() { _connected = false; } private: bool _connected; }

การทำให้สมาชิก _connected volatile ไม่ได้ช่วยแก้ปัญหาและไม่ทำให้ isConnected() เธรดปลอดภัย วิธีแก้ปัญหานี้จะได้ผล 99% ของเวลา แต่อีก 1% ที่เหลือจะทำให้ชีวิตคุณกลายเป็นฝันร้าย ในการแก้ไขปัญหานี้ เราจำเป็นต้องป้องกันการเข้าถึงตัวแปรจากหลายเธรด ลองใช้ QReadWriteLocker เพื่อจุดประสงค์นี้:

 // pseudo-code, won't compile class TcpConnection : QObject { Q_OBJECT public: bool isConnected() const { QReadLocker locker(&_lock); return _connected; } private slots: void handleSocketConnected() { QWriteLocker locker(&_lock); _connected = true; } void handleSocketDisconnected() { QWriteLocker locker(&_lock); _connected = false; } private: QReadWriteLocker _lock; bool _connected; }

สิ่งนี้ทำงานได้อย่างน่าเชื่อถือ แต่ไม่เร็วเท่ากับการใช้การดำเนินการปรมาณูแบบ "ปราศจากการล็อก" โซลูชันที่สามนั้นทั้งรวดเร็วและปลอดภัยต่อเธรด (ตัวอย่างใช้ std::atomic แทน QAtomicInt แต่ความหมายเหมือนกัน):

 // pseudo-code, won't compile class TcpConnection : QObject { Q_OBJECT public: bool isConnected() const { return _connected; } private slots: void handleSocketConnected() { _connected = true; } void handleSocketDisconnected() { _connected = false; } private: std::atomic<bool> _connected; }

บทสรุป

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

หากคุณสนใจที่จะสำรวจ Qmake ฉันยังเพิ่งเผยแพร่ The Vital Guide to Qmake เป็นการอ่านที่ดี!