线程池设计中的惊群问题
2019獨角獸企業重金招聘Python工程師標準>>>
? ? ? 多線程編程已經是現在網絡編程中常用的編程技術,設計一個良好的線程池庫顯得尤為重要。在 UNIX(WIN32下可以采用類似的方法,acl 庫中的線程池是跨平臺的) 環境下設計線程池庫主要是如何用好如下系統 API:
? ? ? 1、pthread_cond_signal/pthread_cond_broadcast:生產者線程通知線程池中的某個或一些消費者線程池,接收處理任務;
? ? ? 2、pthread_cond_wait:線程池中的消費者線程等待線程條件變量被通知;
? ? ? 3、pthread_mutex_lock/pthread_mutex_unlock:線程互斥鎖的加鎖及解鎖函數。
?
? ? ? 下面的代碼示例是大家常見的線程池的設計方式:
?
// 線程任務類型定義 struct thread_job {struct thread_job *next; // 指向下一個線程任務void (*func)(void*); // 應用回調處理函數 void *arg; // 回調函數的參數... };// 線程池類型定義 struct thread_pool {int max_threads; // 線程池中最大線程數限制int curr_threads; // 當前線程池中總的線程數int idle_threads; // 當前線程池中空閑的線程數pthread_mutex_t mutex; // 線程互斥鎖pthread_cond_t cond; // 線程條件變量thread_job *first; // 線程任務鏈表的表頭thread_job *last; // 線程任務鏈表的表尾... }// 線程池中的消費者線程處理過程 static void *consumer_thread(void *arg) {struct thread_pool *pool = (struct thread_pool*) arg;struct thread_job *job;int status;// 該消費者線程需要先加鎖pthread_mutex_lock(&pool->mutex);while (1) {if (pool->first != NULL) {// 有線程任務時,則取出并在下面進行處理job = pool->first;pool->first = job->next;if (pool->last == job)pool->last = NULL;// 解鎖,允許其它消費者線程加鎖或生產者線程添加新的任務pthread_mutex_unlock(&pool->mutex);// 回調應用的處理函數job->func(job->arg);// 釋放動態分配的內存free(job);// 重新去加鎖pthread_mutex_lock(&pool->mutex);} else {pool->idle_threads++;// 在調用 pthread_cond_wait 等待線程條件變量被通知且自動解鎖status = pthread_cond_wait(&pool->cond, &pool->mutex);pool->idle_threads--;if (status == 0)continue;// 等待線程條件變量異常,則該線程需要退出pool->curr_threads--;pthread_mutex_unlock(&pool->mutex);break;}}return NULL; }// 生產者線程調用此函數添加新的處理任務 void add_thread_job(struct thread_pool *pool, void (*func)(void*), void *arg) {// 動態分配任務對象struct thread_job *job = (struct thread_job*) calloc(1, sizeof(*job));job->func = func;job->arg = arg;pthread_mutex_lock(&pool->mutex);// 將新任務添加進線程池的任務鏈表中if (pool->first == NULL)pool->first = job;elsepool->last->next = job;pool->last = job;job->next = NULL;if (pool->idle_threads > 0) {// 如果有空閑消費者線程,則通知空閑線程進行處理,同時需要解鎖pthread_mutex_unlock(&pool->mutex);pthread_cond_signal(&pool->cond);} else if (pool->curr_threads < pool->max_threads) {// 如果未超過最大線程數限制,則創建一個新的消費者線程pthread_t id;pthread_attr_t attr;pthread_attr_init(&attr);// 將線程屬性設為分離模式,這樣當線程退出時其資源自動由系統回收pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);// 創建一個消費者線程if (pthread_create(&id, &attr, consumer_thread, pool) == 0)pool->curr_threads++;pthread_mutex_unlock(&pool->mutex);pthread_attr_destroy(&attr);} }// 創建線程池對象 struct thread_pool *create_thread_pool(int max_threads) {struct thread_pool *pool = (struct thread_pool*) calloc(1, sizeof(*pool));pool->max_threads = max_threads;pthread_mutex_init(&pool->mutex);pthread_cond_init(&pool->cond);...return pool; }/// // 使用上面線程池的示例如下:// 由消費者線程回調的處理過程 static void thread_callback(void* arg) {... }void test(void) {struct thread_pool *pool = create_thread_pool(100);int i;// 循環添加 1000000 次線程處理任務for (i = 0; i < 1000000; i++)add_thread_job(pool, thread_callback, NULL); }?
?
? ? ? 乍一看去,似乎也沒有什么問題,象很多經典的開源代碼中也是這樣設計的,但有一個重要問題被忽視了:線程池設計中的驚群現象。大家可以看到,整個線程池只有一個線程條件變量和線程互斥鎖,生產者線程和消費者線程(即線程池中的子線程)正是通過這兩個變量進行同步的。生產者線程每添加一個新任務,都會調用 pthread_cond_signal 一次,由操作系統喚醒一個在線程條件變量等待的消費者線程,但如果查看 pthread_cond_signal API 的系統幫助,你會發現其中有一句話:調用此函數后,系統會喚醒在相同條件變量上等待的一個或多個線程。而正是這句模棱兩可的話沒有引起很多線程池設計者的注意,這也是整個線程池中消費者線程收到信號通知后產生驚群現象的根源所在,并且是消費者線程數量越多,驚群現象越嚴重----意味著 CPU 占用越高,線程池的調度性能越低。
? ? ? 要想避免如上線程池設計中的驚群問題,在仍然共用一個線程互斥鎖的條件下,給每一個消費者線程創建一個線程條件變量,生產者線程在添加任務時,找到空閑的消費者線程,將任務置入該消費者的任務隊列中同時只通知 (pthread_cond_signal) 該消費者的線程條件變量,消費者線程與生產者線程雖然共用相同的線程互斥鎖(因為有全局資源及調用 pthread_cond_wait 所需),但線程條件變量的通知過程卻是定向通知的,未被通知的消費者線程不會被喚醒,這樣驚群現象也就不會產生了。
? ? ? 當然,還有一些設計上的細節需要注意,比如:當沒有空閑消費者線程時,需要將任務添加進線程池的全局任務隊列中,消費者線程處理完自己的任務后需要查看一下線程池中的全局任務隊列中是否還有未處理的任務。
? ? ? 更多的線程池的設計細節請參考 acl (https://sourceforge.net/projects/acl/) 庫中 lib_acl/src/thread/acl_pthread_pool.c 中的代碼。
?
?參考:
線程編程常見API簡介(上)
線程編程常見API簡介(中)
線程編程常見API簡介(下)
使用 acl_cpp 庫編寫多線程程序
利用ACL庫開發高并發半駐留式線程池程序
多線程開發時線程局部變量的使用
再談線程局部變量
?
acl 庫下載:https://sourceforge.net/projects/acl/
github:https://github.com/zhengshuxin/acl
svn:svn checkout svn://svn.code.sf.net/p/acl/code/trunk acl-code
qq 群:242722074轉載于:https://my.oschina.net/u/568966/blog/309536
總結
以上是生活随笔為你收集整理的线程池设计中的惊群问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CentOS6.3 下启动Oracle
- 下一篇: 一种导致android开发时无法生成R.