1. 安全的結束 thread
現代軟體除了系統給予的 main thread 外,通常還會自己產生一些 thread(s),如何安全的結束這些額外產生的 thread(s) 就成了一項重要的課題。最常見的方法就是用旗標,就算您不懂 Qt,下面的程式也能懂個大概(這樣的程式大概也出現在很多軟體公司):
//mythread.h #ifndef MYTHREAD_H #define MYTHREAD_H #include <QThread> class MyThread : public QThread { Q_OBJECT public: explicit MyThread(QObject *parent = 0); void stop(); signals: public slots: protected: void run(); private: bool m_stop; //the flag for stop thread }; #endif // MYTHREAD_H
//mythread.cpp #include "mythread.h" MyThread::MyThread(QObject *parent) : QThread(parent), m_stop(false) { } void MyThread::run() { while(!m_stop) { //...... } } void MyThread::stop() { m_stop = true; }
這樣的作法只能算勉強及格,因為以現代 CPU 的性能來說,通常 thread 不會卡在計算裡,多半是被阻塞(blocking)在 I/O 裡,所以 thread 要偵測到旗標得等到 I/O 完成才有機會,以 Winsock 來說就是要等到 send()/recv() 完成或是 timeout。
既然我們的問題是 thread 要等到 send()/recv() 完成或 timeout 才能往下執行,那如果有一組可以偵測多個 socket handle(s) 的 API(select),那我們就可以開啟一個專用通道通知 thread 去領便當了。
不過 Windows 麻煩的地方就是 select() 只能用在 winsock 上,如果要用在其他種類的 I/O 上還是得回頭用 WaitForMultipleObjects,好在站長已經寫了超清楚的文章解釋用法與陷阱,請多加利用。
2. 限制資源存取
假設你有一個專案,一開始只有一個 thread 負責讀寫檔案:
隨著專案成長,thread 開始變多,也想存取這個檔案(這很常見,比方說這個檔案是種檔案型資料庫如 SQLite)。比較妥當的方式是透過原來的 thread A 來存取:
為什麼不乾脆用個簡單的 mutex 把檔案保護算了?有幾個理由:
- 不想讓檔案的每一部份都變成可見(不過可靠精心設計的 API 達成)
- 無論讀寫都使用同一把鎖(mutex),如果大部分的 thread(s) 只有讀將會效率不彰。
理由 2. 有一個作法是使用 Multi-Read/Single Write 的技巧迴避(例如 Qt 的 QReadWriteLock )這個技巧會允許多個 thread 同時讀取,但寫入時只允許同一個 thread 進入,但這個作法有一個風險就是可能那個想寫入的 thread 陷入飢餓的狀態(一直輪不到他)。
但 select() 沒有這樣的風險,除非你刻意忽略 OS 傳來的訊號(由 select() fd_set 參數),否則一定會處理(誰先誰後而已),而且因為沒有 mutex 與其他奇門遁甲的上鎖機制,犯錯空間極少。
但這邊還是沒講清楚如何讓 thread 同時讀取 File,畢竟 select() 回傳後還是要循序處理。對此問題我們可以加上快取層,thread B/C/D 只有在 cache 找不到資料時才會請求 thread A 處理,而 thread A 一旦取得資料就放入 cache,於是 thread B/C/D 下次就不用透過通訊的方式查詢資料。
事實上,流行的 PHP + MySQL + Memcached 就這樣玩的,PHP 把欲查詢 MySQL database 的 SQL 指令先以單向雜湊函數(如 MD5)演算過後成為一把獨一無二的 key,丟進 Memcached 看看有沒有已經快取的資料,如果沒有才真的跟 MySQL 要。
Memcached 最大用戶其實就是大家每天在用的 Facebook,光是 2008 的資料就顯示 fb 用了 800 台伺服器、28 TB 記憶體(光是這 800 台伺服器如何維護、備援、同步就是不小的學問)。
另外快取層顯然還是要用到 mutex,任何一本講多工程式撰寫的書都會告訴你在被 mutex 保護的 critical section 在裡面待越短暫越好,那電腦上只有一種裝置可以滿足這種特性:那就是主記憶體,其他恐怕通通不及格,至於通訊更是要不得,這也是 nginx、lighttp、node.js 幾乎全盤放棄 thread 的原因。
Hi 版大你好,
回覆刪除另外快取層顯然還是要用到 mutex
=> 這邊是不是指說Thread A 更新至Cache之前也是需要做Mutex,避免Thread B ~ C 會有read錯誤的風險?
謝謝,
GJ 2018/01/02
可以參考一下這篇,我建議使用 Cache Aside Pattern
刪除https://coolshell.cn/articles/17416.html
如國要完全保持同步,個人知道的方法就是
Single Write/Multiple Read Lock/Unlock機制
不過碰上大量thread同時寫入時又會回到效率下降的問題,恐怕唯一解法就是如 Cache Aside Pattern,用資料不同的機率做代價交換