oracle怎樣定時觸發_c++定時器管理
技術標籤:oracle怎樣定時觸發
在上篇中提了定時器的兩種實現的思路,同時也提到了一個問題,就是當一個程序需要使用大量定時器時,需要利用時間輪、最小堆或紅黑樹等結構來管理定時器。為什麼要這樣呢?我們先回顧下上篇的觀點,上篇中提到定時器比較常見的有兩種,一種是cron like,另外一種是週期性執行的定時器,但是對於cron like的定時器而言,如果要效能好,一般要每個定時器一個執行緒,或者是要借用另外一個週期執行的定時器來做觸發源,用一個數據結構,如連結串列,把所有cron like的定時器串起來,然後在每個週期,如1秒一次,去掃描這個連結串列,去判斷是否有定時器能夠被觸發。這種做法在定時器比較少時固然問題不大,但是如果定時器多起來的話,整個過程的效能就不那麼的好了,所以,我們需要有一個比較連結串列更好的資料結構來管理我們的定時器。對於定時器管理這個場景中,用連結串列的查詢慢的原因主要是因為定時器在連結串列中是無序儲存的,所以只能夠遍歷整個連結串列,那要改進的話,就必須解決一個問題,就是怎樣把這些定時器變得有序。這個問題最簡單的解法,就是預先計算出這些定時下次被觸發的時間點,然後根據這個時間點由小到大排序就行了,但是如果還是沿用連結串列,每個有節點要增加或者移除,效能都不會太好,所以,我們必須引入更好的資料結構來管理這些定時器。下面說下筆者認為比較常見的思路吧。
1、紅黑樹
對於改進連結串列的查詢效能,最直接也是最常見的思路,就是使用二分搜尋樹。但是由於定時器執行的時間點是一直在變化的,也就是說,如果這棵二分查詢樹的根節點不變的情況下,二分查詢樹會變得很不平衡,因此,使用能夠自動調節高度的紅黑樹會比使用普通的二分查詢樹會來的更加合理。但是由於紅黑樹的實現比較長,而且大部分演算法書裡面都會有提到,所以這裡就不去囉嗦了,大家自行參考這裡吧。
2、最小堆
紅黑樹在解決問題的角度看,卻是一個不錯的選擇,不管是插入還是查詢還是刪除都是lgN的時間複雜度,無奈要實現一棵正確的紅黑樹還是有一定難度的,當然,現在開源的實現還是有不少的,我們可以考慮拿來主義。不過,如果想用二分查詢樹,但是由不想維護像紅黑樹這樣的複雜資料結構,能怎樣辦呢?可能有同學想到了,沒錯,用堆。我們的定時器是從時間小到大的順序執行的,所以,我們可以使用對裡面的最小堆。如果不懂最小堆是什麼的同學,麻煩看看教科書,或者簡單點看看這裡。下面提供一個用vector實現堆的思路
#include<iostream> #include<assert.h> #include<vector> using namespace std; template<typename T> struct Less { bool operator()(const T& left, const T& right) const{ return left < right; } }; template<typename T> struct Greater { bool operator()(const T& left, const T& right) const{ return left > right; } }; template<typename T, typename Compare=Less<T> > class Heap { public: Heap(size_t n){ data.reserve(n); } void adjustDown(int root){ Compare com; int parent = root; size_t child = parent * 2 + 1; while (child < data.size()){ if (child + 1 < data.size() && com(data[child + 1], data[child])){ ++child; } if (com(data[child], data[parent])){ swap(data[child], data[parent]); parent = child; child = parent * 2 + 1; } else{ break; } } } void adjustUp(int child){ Compare com; int parent = (child - 1) / 2; while (parent >= 0){ if (com(data[child], data[parent])){ swap(data[parent], data[child]); child = parent; parent = (child - 1) / 2; } else{ break; } } } void push(const T&x){ data.push_back(x); adjustUp(data.size() - 1); } void pop(){ assert(!data.empty()); swap(data[0], data[data.size() - 1]); data.pop_back(); adjustDown(0); } T top(){ assert(!data.empty()); return data[0]; } size_t size(){ return data.size(); } bool empty(){ return data.empty(); } private: vector<T> data; };
那既然都用到了c++,stl就沒有提供嗎?答案是有的,那就是priority_queue,但是priority_queue預設是一個最大堆,而我們要的是最小堆,咋辦呢?一個比較土的辦法是把時間都變成負數,這是可以解決問題的,至於優雅程度吧,那就只能這樣了,至於更優雅的辦法吧,我們先看看stl中priority_queue的定義先
template<
class T,
class Container = std::vector<T>,
class Compare = std::less<typename Container::value_type>
> class priority_queue;
從這裡可以看到,priority_queue的排序是由Compare來決定的,所以只要我們修改Compare為我們自己的排序函式,就能實現按照我們的排序方式來排序堆裡面的元素了。下面舉個簡單的例子,
struct node{
int idx;
int key;
node(int a=0, int b=0):idx(a), key(b){}
};
struct cmp{
bool operator()(node a, node b){
return a.key > b.key;
}
};
priority_queue<node, vector<node>, cmp> min_heap;
3、時間輪
在討論時間輪之前,我們先來想一個和定時器無關的問題,那就是如果我給一個任意大的整數集合R和一個整數A,怎樣判斷這個A是否在集合R中。方法有很多種,暴力查詢,排序等等,都能完成,就是效能上好與壞的差別了,當然了,如果在空間無限大的情況下,最好的解法肯定是做一個無限大的HASH表,這樣只要經常O(1)的時間就能完成這個判斷了。沒錯,在空間用不完的前提下,這確實是最優解,但是理想是豐滿的,現實是骨感的,空間怎麼可能沒有限制的呢?那優化一下,我們做一個大小為N的閉地址HASH,這樣在假定hash函式能夠保證每個地址的元素個數接近的情況下,對比的次數只有原來直接對比的1/N,如果覺得這還不夠,可以為每個地址再做一個二次HASH,這樣查詢的耗時就能夠進一步減少。好了,回到正題,為什麼會先討論這個東西呢?大家看看,在定時器的實現中,我們知道一個時間值,然後我們要判斷這個時間對應是否有定時任務需要觸發,這和上面說的問題是不是等價的?如果是,那是不是也能用類似的手段來優化定時器的寫入和查詢速度呢?答案是肯定的,如果我們有無限記憶體的話,我們把未來的每一秒都分配一個槽,拿到時間值之後直接看那個槽有沒有任務就可以了,但是理想歸理想,現實還是得尊重的。我們沒有無限的記憶體,所以我們只能考慮次優解,就是如上所說的構建一個大小為N的閉地址HASH了。假設我們簡單得用取模作為HASH函式,那麼,我們就可以得到一個類似下面的資料結構
這樣,我們處理的集合就比原來小多了。當然,我們還可以對每個HASH的節點的連結串列進行排序或者二次HASH的操作,只是在一般應用層來說,一般使用最小堆就能滿足了,所以這裡就不再做展開了。這裡有一篇關於LINUX核心的時間輪的實現介紹,有興趣d的同學可以自己看看,同時附上兩個比較好的時間輪實現,分別是雲風的skynet和facebook的folly,這裡就不做玩具級程式碼的展示了。