利用最小堆結構來設計一種定時方案
伺服器程式通常管理著眾多的定時事件,因此有效地組織這些定時事件,使之能在預期的時間點被觸發且不影響伺服器的主要邏輯,對於伺服器的效能有著至關重要的影響。
本篇主要討論的是時間堆的設計。在利用最小堆結構來設計定時方案前,我們先來了解一下什麼是最小堆。
什麼是最小堆
1.概念
最小堆是指每個節點的值都小於或等於其子節點的值的完全二叉樹。如下圖,就是一個具有6個元素最小堆:
2.最小堆的插入操作
樹的基本操作是插入節點和刪除節點,最小堆的插入操作步驟如下:
以插入X元素為例
1)在樹的下一個空閒位置建立一個空穴,如果X可以放在空穴中而不破壞堆序,則插入完成,否則就執行2)上慮操作;
2)上慮操作即交換空穴和它的父節點上的元素。不斷執行該過程,直到X可以被放入空穴,則插入操作完成。
例如向上圖的最小堆中插入值為14的元素,步驟如下:
3.最小堆的刪除操作
最小堆的刪除操作指的是刪除其根節點上的元素,並且不破壞堆序性質。最小堆的刪除操作步驟如下:
1)首先在根節點出建立一個空穴;
2)因為刪除了根節點,現在堆少了一個元素,因此我們可以把堆的最後一個元素X移動到該堆的某個地方。如果X可以被放入空穴,則刪除操作完成,否則就執行3)下慮操作;
3)下慮操作即交換空穴和它的兩個兒子節點中的較小者。不斷執行該過程,直到X可以被放入空穴,則刪除操作完成。
例如對上圖的最小堆執行刪除操作,步驟如下:
4.最小堆的陣列表示
由於最小堆是一種完全二叉樹,所以我們可以用陣列來組織其中的元素。比如上圖的最小堆可用可用陣列表示如下:
對於陣列中的任意一個位置 i 上的元素,其左兒子節點在位置 2i + 1 上,其右兒子節點在位置 2i + 2 上,其父節點則在位置 [ ( i - 1) / 2 ] ( i > 0 ) 上。與用連結串列來表示堆相比,用陣列表示堆不僅節省空間,而且更容易實現堆的插入、刪除等操作。
5.最小堆的初始化操作
假設我們已經有一個包含N個元素的陣列,現在要把它初始化為一個最小堆。那麼最簡單的方法是:初始化一個空堆,然後將陣列中的每個元素插入該堆中。不過這樣做的效率偏低。實際上,我們只需要對陣列中的第 0 ~ [ (N - 1) / 2 ] 個元素執行下慮操作,即可確保該陣列構成一個最小堆。這是因為對包含N個元素的完全二叉樹而言,它具有 [ (N - 1) / 2 ] 個非葉子節點,這些非葉子節點正是該完全二叉樹的第 0 ~ [ (N - 1) / 2 ] 個節點。我們只要確保這些非葉子節點構成的子樹都具有堆序性質,整個樹就具有堆序性質。
方案設計
設計定時器的一種思路如下:
將所有定時器中超時時間最小的一個定時器的超時值作為心搏間隔。這樣,一旦心搏函式 tick 被呼叫,超時時間最小的定時器必然到期,我們就可以在 tick 函式中處理該定時器。然後,再次從剩餘的定時器中找出超時時間最小的一個,並將這段最小時間設定為下一次心搏間隔,如此反覆,就實現了較為精確的定時。
最小堆很適合處理這種定時方案,我們稱用最小堆實現的定時器為時間堆。
一種時間堆的實現如下:
#include <iostream>
#include <netinet/in.h>
#include <time.h>
using std::exception;
#define BUFFER_SIZE 64
// 前向宣告定時器類
class heap_timer;
// 使用者資料,繫結socket和定時器
struct client_data
{
sockaddr_in address;
int sockfd;
char buf[ BUFFER_SIZE ];
heap_timer* timer;
};
// 定時器類
class heap_timer
{
public:
heap_timer( int delay )
{
expire = time( NULL ) + delay;
}
public:
time_t expire; // 定時器生效的絕對時間
void (*cb_func)( client_data* ); // 定時器的回撥函式
client_data* user_data; // 使用者資料
};
// 時間堆類
class time_heap
{
public:
// 建構函式之一,初始化一個大小為cap的空堆
time_heap( int cap ) throw ( std::exception )
: capacity( cap ), cur_size( 0 )
{
array = new heap_timer* [capacity];
if ( ! array )
{
throw std::exception();
}
for( int i = 0; i < capacity; ++i )
{
array[i] = NULL;
}
}
// 建構函式之二,用已有陣列來初始化堆
time_heap( heap_timer** init_array, int size, int capacity ) throw ( std::exception )
: cur_size( size ), capacity( capacity )
{
if ( capacity < size )
{
throw std::exception();
}
array = new heap_timer* [capacity];
if ( ! array )
{
throw std::exception();
}
for( int i = 0; i < capacity; ++i )
{
array[i] = NULL;
}
if ( size != 0 )
{
// 初始化堆陣列
for ( int i = 0; i < size; ++i )
{
array[ i ] = init_array[ i ];
}
// 對陣列中的第 [ (N - 1) / 2 ] ~ 0 個元素執行下慮操作
for ( int i = (cur_size-1)/2; i >=0; --i )
{
percolate_down( i );
}
}
}
// 解構函式,銷燬時間堆
~time_heap()
{
for ( int i = 0; i < cur_size; ++i )
{
delete array[i];
}
delete [] array;
}
public:
// 新增目標定時器,時間複雜度為O(logN)
void add_timer( heap_timer* timer ) throw ( std::exception )
{
if( !timer )
{
return;
}
// 當前堆陣列容量不夠則將其擴大一倍
if( cur_size >= capacity )
{
resize();
}
int hole = cur_size++; // hole是新建空穴的位置
int parent = 0;
// 對從空穴到根節點的路徑上的所有節點執行上慮操作
for( ; hole > 0; hole=parent )
{
parent = (hole-1)/2;
if ( array[parent]->expire <= timer->expire )
{
break;
}
array[hole] = array[parent];
}
array[hole] = timer;
}
// 刪除目標定時器,時間複雜度為O(1)
void del_timer( heap_timer* timer )
{
if( !timer )
{
return;
}
// 延遲銷燬,僅僅將目標定時器的回撥函式設定為空
// 這將節省真正刪除該定時器造成的開銷,但這樣做容易使堆陣列膨脹
timer->cb_func = NULL;
}
// 獲得堆頂的定時器
heap_timer* top() const
{
if ( empty() )
{
return NULL;
}
return array[0];
}
// 刪除堆頂的定時器
void pop_timer()
{
if( empty() )
{
return;
}
if( array[0] )
{
delete array[0];
// 將堆頂元素替換為堆陣列中最後一個元素,並對它進行下慮操作
array[0] = array[--cur_size];
percolate_down( 0 );
}
}
// 心搏函式,執行一個定時器的時間複雜度為O(1)
void tick()
{
heap_timer* tmp = array[0];
time_t cur = time( NULL );
// 迴圈處理堆中到時的定時器
while( !empty() )
{
if( !tmp )
{
break;
}
// 定時器未到期則退出迴圈
if( tmp->expire > cur )
{
break;
}
// 定時器到期則執行堆頂定時器中的任務
if( array[0]->cb_func )
{
array[0]->cb_func( array[0]->user_data );
}
// 彈出堆頂元素,同時設定新的堆頂元素
pop_timer();
tmp = array[0];
}
}
bool empty() const { return cur_size == 0; }
private:
// 下慮操作函式
void percolate_down( int hole )
{
heap_timer* temp = array[hole];
int child = 0;
// 當下慮到葉子節點時,迴圈終止
for ( ; ((hole*2+1) <= (cur_size-1)); hole=child )
{
child = hole*2+1; // 空穴的左孩子節點
// 選擇兩個兒子節點中的較小者
if ( (child < (cur_size-1)) && (array[child+1]->expire < array[child]->expire ) )
{
++child;
}
// 如果空穴比較小的兒子節點大則交換,否則該元素可以放入空穴
if ( array[child]->expire < temp->expire )
{
array[hole] = array[child];
}
else
{
break;
}
}
array[hole] = temp;
}
// 將堆陣列容量擴大一倍
// 建立新陣列並將容量擴大一倍,初始化新陣列,然後將原內容拷貝過來,並釋放原空間
void resize() throw ( std::exception )
{
heap_timer** temp = new heap_timer* [2*capacity];
for( int i = 0; i < 2*capacity; ++i )
{
temp[i] = NULL;
}
if ( ! temp )
{
throw std::exception();
}
capacity = 2*capacity;
for ( int i = 0; i < cur_size; ++i )
{
temp[i] = array[i];
}
delete [] array;
array = temp;
}
private:
heap_timer** array; // 堆陣列
int capacity; // 堆陣列的容量
int cur_size; // 堆陣列當前包含的元素個數
};
參考資料:
《Linux高效能伺服器程式設計》 遊雙
相關文章
- 網路程式設計定時器三:使用最小堆程式設計定時器
- 結合元素和屬性的定義分析Schema的幾種設計方案
- SAP Spartacus module 層級結構設計的一種實踐
- linux系統程式設計之訊號(八):三種時間結構及定時器setitimer()詳解Linux程式設計定時器
- 網路程式設計定時器一:使用升序連結串列程式設計定時器
- 一種遷移到體系結構化環境的方案
- 定時中斷基本結構
- 一種介面設計方法,最終一致性方法
- C++ 11 時間程式設計(4)利用std::chrono::steady_clock寫一個定時器C++程式設計定時器
- 程式碼效能——盤點資料結構設計方案資料結構
- iOS 設定代理(Proxy)方案總結iOS
- 使用設計模式改善程式結構(一)設計模式
- 聊一聊設計模式(三)-- 結構型設計模式設計模式
- 利用memcached配置session一致性另外一種方案Session
- 設計randompool結構random
- 【Flutter】 介紹一種通用的頁面路由設計方案Flutter路由
- 如何利用資料來支撐設計?
- MySql架構設計:如何合理利用第三方 Cache 解決方案?MySql架構
- PHP-RBAC單角色設計-最簡單的設計方案PHP
- MySql樹形結構(多級選單)查詢設計方案MySql
- 利用WMRouter 重新架構設計業務模式架構模式
- 如何設計一個好玩的競技遊戲——結構時間軸與戰鬥時間軸遊戲
- JS 非同步程式設計六種方案JS非同步程式設計
- 血壓計方案定製設計軟硬體解決方案
- EasyBridge:一種簡單的js-bridge方案設計JS
- 如何利用 AVFoundation 設計一個通用穩定的音視訊框架?框架
- 如何設定一個定時任務?
- 設計模式-結構型設計模式
- 程式結構&&程式設計程式設計
- 結構型設計模式設計模式
- Redis 設計與實現 (一)--資料結構Redis資料結構
- lua定時器與定時任務的介面設計定時器
- 利用winrar定時備份計算機資料夾計算機
- 第一次重構的架構設計總結架構
- 成為更好的程式設計師必須學習的 4 種程式設計結構程式設計師
- 幾種設計良好結構以提高.NET應用效能的方法
- 計劃策略設定的兩種方式
- VS 2008 解決方案的目錄結構設定和管理 [整理]