glib庫異步隊列和線程池代碼分析

本文章主要講了兩部分內容:一是分析了異步隊列的原理和實現,二是分析線程池的原理和實現。
在多線程程序的運行中,如果經常地創建和銷燬執行過程相似而所用數據不同的線程,系統的效率,系統資源的利用率將會受到極大的影響。對於這一問題可用類似glib庫中的線程池的解決辦法。
  
我 們可以這樣想像線程池的處理,當有新的數據要交給線程處理時,主程序/主線程 就從線程池中找到一個未被使用的線程處理這新來的數據,如果線程池中沒有找到可用的空閒線程,就新創建一個線程來處理這個數據,並在處理完後不銷燬它而是 把這個線程放到線程池中,以備後用。線程池的這個原理和內存管理中slab機制有異曲同工之妙!我想無論是線程池的這種處理方式還是slab機制,其本質 思想還是一致的。
近來做的項目,在框架中用到了多線程的異步隊列,實現形式和glib中的異步隊列極其相似,而glib線程池中的代碼也用到了異步隊列(名字用同步隊列更合適),因此就先分析一下異步隊列。
異 步隊列的概念是這樣的:所有的數據組織成隊列,供多線程併發訪問,而這些併發控制全部在異步隊列裏面實現,對外面只提供讀寫接口;當隊列中的數據爲空時, 如果是讀線程訪問異步隊列,那麼這一讀線程就等待,直到有數據爲止;寫線程向隊列放數據時,如果有線程在等待數據就喚醒等待線程。
異步隊列主要代碼剖析:
異步隊列的數據結構如下:
struct _GAsyncQueue
{
GMutex *mutex; //互斥變量
GCond *cond; //等待條件
GQueue *queue; //數據隊列
guint waiting_threads; //等待的讀線程個數
gint32 ref_count;
};
g_async_queue_push_unlocked (GAsyncQueue* queue, gpointer data)
{
………//這些點代表一些省略的代碼
//把數據放入隊列
g_queue_push_head (queue->queue, data);
//現在隊列已經有數據了,判斷是否有讀線程在等待數據,
//如果有就發送信號喚醒讀線程
if (queue->waiting_threads > 0)
    g_cond_signal (queue->cond);
}
g_async_queue_push (GAsyncQueue* queue, gpointer data)
{
……..
g_mutex_lock (queue->mutex); //在訪問臨界區前先獲得互斥變量
g_async_queue_push_unlocked (queue, data); //執行寫數據操作
g_mutex_unlock (queue->mutex); //釋放互斥變量,以使其它線程可以進入臨界區
}
從以上的接口可看出,”…._ unlocked” 這樣的接口就是異步隊列這個對象已獲得互斥變量的接口,glib中線程處理相關接口都有類似的命名規則,在接下來的代碼分析中,如沒有特別的需要就只看”…._ unlocked” 這樣的接口。
// 讀線程從異步隊列中獲取數據的接口
// try參數和時間參數在多線程同步/內核多進程的實現中是很常見的東西了,在這裏就不再作特殊的解釋了。
g_async_queue_pop_intern_unlocked (GAsyncQueue *queue,
                               gboolean     try,
                               GTimeVal    *end_time)
{
gpointer retval;
//判斷是否有數據在隊列中,如果沒有就要執行if語句相應的睡眠等待,直到被寫進程喚醒
if (!g_queue_peek_tail_link (queue->queue))
    {
      if (try)//如果try爲真,則永遠不睡眠
       return NULL;
     
    // 接下來是要讓線程進行睡眠等待了,在等待之前先確保等待條件已創建
      if (!queue->cond)
       queue->cond = g_cond_new ();
      if (!end_time) // 等待無時間限制
        {
          queue->waiting_threads++; // 等待線程數加一
      // 這裏爲什麼用循環?因爲這是多線程的環境,有可能有多個讀線程在等待
      // 當前線程被喚醒時,有可能數據隊列中的數據又被別的線程讀走了,所以
      // 當前線程就得繼續睡眠等待
      // 注意:睡眠等待時會暫時放棄互斥鎖,被喚醒時會重新獲取互斥鎖
       while (!g_queue_peek_tail_link (queue->queue))
            g_cond_wait (queue->cond, queue->mutex);
          queue->waiting_threads--; // 等待線程數減一
        }
      else
        {
          queue->waiting_threads++;
          while (!g_queue_peek_tail_link (queue->queue))
           if (!g_cond_timed_wait (queue->cond, queue->mutex, end_time))
              break;
          queue->waiting_threads--;
          if (!g_queue_peek_tail_link (queue->queue))
           return NULL;
        }
    }
retval = g_queue_pop_tail (queue->queue);
g_assert (retval);
return retval;
}
/* 返回數據隊列的長度,也即數據隊列中的數據個數.
* 如果是負值表明是等待數據的線程個數,正數表示數據隊列的數據個數
* g_async_queue_length == 0 表示是有 'n' 個數據和' n' 個等待線程在數據隊列
* 這種特殊情況可能是在對數據隊列加鎖或調度時發生
*/
g_async_queue_length_unlocked (GAsyncQueue* queue)
{
g_return_val_if_fail (queue, 0);
g_return_val_if_fail (g_atomic_int_get (&queue->ref_count) > 0, 0);
return queue->queue->length - queue->waiting_threads;
}
  
有 了前面的異步隊列基礎就可以分析線程池是怎麼實現的了。在glib庫中的線程池的實現和使用有兩種方式:1. 單個線程池對象不共享方式;2. 多個線程池對象共享線程方式,也即把各個具體的線程池對象創建的把任務做完了的線程統一放在全局線程池中進行統一管理,各個具體的線程池對象要使用線程 時,可以先向全局線程池中取線程,如果全局線程池沒有線程了具體的線程池對象就可自行創建線程。
       線程池的數據結構有兩部分,一部分是在頭文件中,另一部分在C文件中。這是C語言中常用的信息隱藏方法之一,把要暴露給用戶的數據放在頭文件中,而要隱藏的數據則放在C文件中。下面是線程池頭文件中的數據結構:
typedef struct _GThreadPool     GThreadPool;
struct _GThreadPool
{
   // 具體處理數據的函數
   // 它的第一個參數爲g_thread_pool_push進去的數據,也即要執行的任務
GFunc func;
gpointer user_data; // func的第二個參數
// 通過這個成員控制線程池對象創建的線程是否在全局線程池中共享,
// TRUE爲不共享,FALSE爲共享
gboolean exclusive;
};
C文件中線程池的數據結構:
typedef struct _GRealThreadPool GRealThreadPool;
struct _GRealThreadPool
{
GThreadPool pool; // 頭文件已定義
GAsyncQueue* queue; // 異步數據隊列
GCond* cond;
gint max_threads; // 線程池對象持有的線程數上限
gint num_threads;// 池程池對象當前持有的線程數
gboolean running;
gboolean immediate;
gboolean waiting;
GCompareDataFunc sort_func;
gpointer sort_user_data;
};
我們可以先來分析單個線程對象不共享的主要實現。在分析它的實現之前,可以先看看一個流程圖
從上圖可見當主線程有數據交給線程池處理時,只要調用異步隊列相關的push接口,線程池中的任何一個線程都可以爲這服務。根據以上的流程圖看看單個線程對象不共享方式的主要實現代碼,它的調用從創建線程池對象開始:
g_thread_pool_new--->g_thread_pool_start_thread---> g_thread_create(g_thread_pool_thread_proxy,pool,FALSE,&local_error)--->>g_thread_pool_wait_for_new_task(pool----> g_async_queue_pop_unlocked (pool->queue);
// max_threads爲 -1 時表示線程池中的線程數無限制並且線程由動態生成
// max_threads爲正整數時,線程池就會預先創建max_threads個線程
g_thread_pool_new (GFunc             func,
                 gpointer         user_data,
                 gint             max_threads,
                 gboolean         exclusive,
                 GError         **error)
{
GRealThreadPool *retval;
    ……………. //這些點代表一些省略的代碼
retval = g_new (GRealThreadPool, 1);
retval->pool.func = func;
retval->pool.user_data = user_data;
retval->pool.exclusive = exclusive;
retval->queue = g_async_queue_new (); // 創建異步隊列
retval->cond = NULL;
retval->max_threads = max_threads;
retval->num_threads = 0;
retval->running = TRUE;
    …………….
if (retval->pool.exclusive)
{
      g_async_queue_lock (retval->queue);
      while (retval->num_threads < retval->max_threads)
          {
             GError *local_error = NULL;
             g_thread_pool_start_thread (retval, &local_error);//起動新的線程
             …………….
       }
      g_async_queue_unlock (retval->queue);
}
return (GThreadPool*) retval;
}
g_thread_pool_start_thread (GRealThreadPool *pool,
                         GError          **error)
{
gboolean success = FALSE;
if (pool->num_threads >= pool->max_threads && pool->max_threads != -1)
    /* Enough threads are already running */
    return;
…………….
if (!success)
{
      GError *local_error = NULL;
      /* No thread was found, we have to start a new one */
      // 真正創建一個新的線程
      g_thread_create (g_thread_pool_thread_proxy, pool, FALSE, &local_error);
      ……………….
}
pool->num_threads++;
}
g_thread_pool_thread_proxy (gpointer data)
{
GRealThreadPool *pool;
pool = data;
……………..
g_async_queue_lock (pool->queue);
while (TRUE)
{
      gpointer task;
      // 線程等待任務,也即等待數據,線程在等待就是處在線程池中的空閒線程
      task = g_thread_pool_wait_for_new_task (pool);
      // 如果線程被喚醒收到並數據就用此線程執行任務,否則繼續循環等待
      // 注意:當任務做完時,繼續循環又會調用上面的g_thread_pool_wait_for_new_task
      // 而進入等待狀態,
if (task)
       {
             if (pool->running || !pool->immediate)
              {
                /* A task was received and the thread pool is active, so
              * execute the function.
              */
                g_async_queue_unlock (pool->queue);
                pool->pool.func (task, pool->pool.user_data);
                g_async_queue_lock (pool->queue);
           }
       }
      else
       {
            ………………
      }
}
return NULL;
}
g_thread_pool_wait_for_new_task (GRealThreadPool *pool)
{
gpointer task = NULL;
if (pool->running || (!pool->immediate &&
                     g_async_queue_length_unlocked (pool->queue) > 0))
{
      /* This thread pool is still active. */
      if (pool->num_threads > pool->max_threads && pool->max_threads != -1)
       {
           …………..
       }
     else if (pool->pool.exclusive)
       {
           /* Exclusive threads stay attached to the pool. */
        // 調用異步隊列的pop接口進入等待狀態,到此一個線程的創建過程就完成了
           task = g_async_queue_pop_unlocked (pool->queue);
       }
      else
       {
           ………….
       }
}
else
{
     …………
}
return task;
}

現在可以結合流程圖分析線程池中創建一個線程的一個情景:從函數g_thread_pool_new的while循環調用了 g_thread_pool_start_thread函數,在函數中直接調用g_thread_create創建線程,被創建的線程調用函數 g_thread_pool_wait_for_new_task循環等待任務的到來,函數 g_thread_pool_wait_for_new_task調用g_async_queue_pop_unlocked (pool->queue)真正進入等待。如此可知,最終新創建的線程是調用異步隊列的pop接口進入等待狀態的,這樣一個線程的創建就大功告成 了。而函數g_thread_pool_new的while循環結束時就創建了max_threads個等待線程,也即這個新建的線程池對象有了 max_threads個線程以備使用。

       創建線程池、線程池中的線程是爲了使用它,在線程池中取線程,叫線程幹活的過程就很簡單多了,這個調用過程:g_thread_pool_push--à g_thread_pool_queue_push_unlocked--à g_async_queue_push_unlocked。可見最終調用的是異步數據隊列的push接口,把要處理的數據插入隊列後它就會喚醒等待異步隊列數據的等待線程。

g_thread_pool_push (GThreadPool *pool,
                  gpointer      data,
                  GError      **error)
{
……………
//
if (g_async_queue_length_unlocked (real->queue) >= 0)
    /* No thread is waiting in the queue */
    g_thread_pool_start_thread (real, error);
g_thread_pool_queue_push_unlocked (real, data);
g_async_queue_unlock (real->queue);
}
g_thread_pool_queue_push_unlocked (GRealThreadPool *pool,
                               gpointer         data)
{
   ………….
    g_async_queue_push_unlocked (pool->queue, data);
}

    總結:單個線程池對象不共享方式在管理多線程時是以線程池對象中的異步隊列爲中心,新創建的線程或做完任務的線程並不釋放,讓它調用異步隊列的pop接口進入等待狀態,而在使用喚醒線程池中的線程就是調用異步隊列的push接口。

    以上對於理解線程池的實現已經足夠,多個線程池對象共享線程方式和具體線程池的銷燬的技巧,在這裏就不討論了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章