第6章-武技-常見組件上三路
時(shí)間過得真快, 武技修煉完畢, 修真之旅就過去一半了.
武技本是金丹期選手在妖魔大戰(zhàn)中招招實(shí)戰(zhàn)的經(jīng)驗(yàn). 原始而奏效, 飛沙走石, 熱浪滾滾.
說的也許很短, 但未名傳說等你隨意去亂寫 ~
此刻只帶你穿梭那種場景, 感受一瞬間的震耳欲動(dòng), 山河破碎, 天地不仁 ~
帶好你的劍,
那年學(xué)的華山劍法 ~
此刻上路吧 ---:o
6.1 傳說中的線程池
線程池是很古老的舊話題. 討論的很多, 深入的不多. 也就在那些基礎(chǔ)庫中才能見到這種
有點(diǎn)精妙的技巧. 這里也會(huì)隨大流深入簡述一種高效且控制性強(qiáng)的一種線程池實(shí)現(xiàn). 先引入
一個(gè)概念, 驚群. 簡單舉個(gè)例子. 春天來了, 公園出現(xiàn)了很多麻雀. 而你恰巧有一個(gè)玉米粒.
扔出去, 立馬無數(shù)麻雀過來爭搶. 而最終只有一只麻雀得到了. 而那些沒有搶到的麻雀很累
....... 編程中驚群, 也是個(gè)很古老的編程話題了. 服務(wù)器框架開發(fā)中很有機(jī)會(huì)遇到. 有興
趣的可以自行搜索, 多數(shù)介紹的解決方案質(zhì)量非常高. 而我們今天只討論線程池中驚群現(xiàn)象.
采用的POSIX跨平臺的線程庫 pthread.
extern int __cdecl pthread_cond_signal (pthread_cond_t * cond);
上面 pthread 接口就是線程池中出現(xiàn)驚群來源. 它會(huì)激活 pthread_cond_wait 等待態(tài)
一個(gè)或多個(gè)線程. 同樣這里解決驚群的方式很普通也很實(shí)在, 定向激活, 每個(gè)實(shí)際運(yùn)行線程
對象都有自己的條件變量. 那么每次只要激活需要激活的線程變量就可以了. 驚群的現(xiàn)象自
然就避免了.
6.1.1 線程池接口設(shè)計(jì)
#ifndef _H_SIMPLEC_SCTHREADS
#define _H_SIMPLEC_SCTHREADS
#include "schead.h"
//
// 這是個(gè)簡易的線程池的庫.
//
typedef struct threads * threads_t;
//
// threads_create - 創(chuàng)建一個(gè)線程池處理對象
// return : 返回創(chuàng)建好的線程池對象, NULL表示失敗
//
extern threads_t threads_create(void);
//
// threads_delete - 異步銷毀一個(gè)線程池對象
// pool : 線程池對象
// return : void
//
extern void threads_delete(threads_t pool);
//
// threads_insert - 線程池中添加要處理的任務(wù)
// pool : 線程池對象
// run : 運(yùn)行的執(zhí)行題
// arg : run的參數(shù)
// return : void
//
extern void threads_insert_(threads_t pool, node_f run, void * arg);
#define threads_insert(pool, run, arg) threads_insert_(pool, (node_f)run, (void *)(intptr_t)arg)
#endif // !_H_SIMPLEC_SCTHREADS
定義了不完全類型 threads_t, 創(chuàng)建銷毀添加等行為. 其中使用了個(gè)函數(shù)宏的技巧. 用于
去掉警告. 也許 n 年后, 這類 void (*)(long *) convert void (*)(void *) 不再彈
出 warning. 那 threads_insert 就可以壽終正寢了. 其中對于 (intptr_t)arg 的意圖
讓其可以塞入 int 變量, 因?yàn)樵?x64 上 int 是4字節(jié)強(qiáng)轉(zhuǎn)8字節(jié)的指針變量會(huì)被彈出警告.
到這線程池接口設(shè)計(jì)部分已經(jīng)完工. 代碼比文字好理解, 文字可以瞎寫, 但是代碼的真相缺
早已經(jīng)注定. 設(shè)計(jì)的不好, 那將什么都不是 ......
6.1.2 線程池部分實(shí)現(xiàn)
線程池解決的問題是避免創(chuàng)建過多線程對象, 加重操作系統(tǒng)的負(fù)擔(dān). 從而換了一種方式, 將
多個(gè)線程業(yè)務(wù)轉(zhuǎn)成多個(gè)任務(wù)被固定的線程來回處理. 這個(gè)思路是不是很巧妙. 先說說線程池
容易的實(shí)現(xiàn)部分, 來看任務(wù)結(jié)構(gòu)的設(shè)計(jì):
// 任務(wù)鏈表 結(jié)構(gòu) 和 構(gòu)造
struct job {
struct job * next; // 指向下一個(gè)任務(wù)結(jié)點(diǎn)
node_f run; // 任務(wù)結(jié)點(diǎn)執(zhí)行的函數(shù)體
void * arg;
};
static inline struct job * _job_new(node_f run, void * arg) {
struct job * job = malloc(sizeof(struct job));
if (NULL == job)
CERR_EXIT("malloc sizeof(struct job) is error!");
job->next = NULL;
job->run = run;
job->arg = arg;
return job;
}
內(nèi)存處理方式很直接, 申請失敗直接退出. 更加粗暴的方式是調(diào)用 abort(). 強(qiáng)制退出.
但實(shí)際開發(fā)中還真沒遇到過內(nèi)存不足(拋開測試). 假如一個(gè)系統(tǒng)真的內(nèi)存不足了, 可能一
切都是未知. 繼續(xù)分析線程池結(jié)構(gòu), 線程處理對象和線程池對象結(jié)構(gòu)如下:
// 線程結(jié)構(gòu)體, 每個(gè)線程一個(gè)信號量, 定點(diǎn)觸發(fā)
struct thread {
struct thread * next; // 下一個(gè)線程對象
bool wait; // true 表示當(dāng)前線程被掛起
pthread_t tid; // 當(dāng)前線程id
pthread_cond_t cond; // 線程條件變量
};
// 定義線程池(線程集)定義
struct threads {
size_t size; // 線程池大小, 最大線程結(jié)構(gòu)體數(shù)量
size_t curr; // 當(dāng)前線程池中總的線程數(shù)
size_t idle; // 當(dāng)前線程池中空閑的線程數(shù)
volatile bool cancel; // true表示當(dāng)前線程池正在 delete
pthread_mutex_t mutx; // 線程互斥量
struct thread * thrs; // 線程結(jié)構(gòu)體對象集
struct job * head; // 線程任務(wù)鏈表的鏈頭, 隊(duì)列結(jié)構(gòu)
struct job * tail; // 線程任務(wù)隊(duì)列的表尾, 后插入后執(zhí)行
};
上面可以找出每個(gè) struct thread 線程運(yùn)行對象中, 都有個(gè) pthread_cont_t 條件變量
. 這就是定向激活的關(guān)鍵. struct threads::cancel 用于標(biāo)識當(dāng)前線程池是否在銷毀階段
. 來避免使用 pthread_cancel + pthread_cleanup_push 和 pthread_cleanup_pop ,
這類有風(fēng)險(xiǎn)的設(shè)計(jì). 上面兩個(gè)結(jié)構(gòu)衍生了幾個(gè)輔助行為操作如下 :
// 根據(jù) cond 內(nèi)存地址熟悉, 刪除 pool->thrs 中指定數(shù)據(jù)
static void _threads_del(struct threads * pool, pthread_cond_t * cond) {
struct thread * head = pool->thrs, * front = NULL;
while (head) {
// 找見了, 否則開始記錄前置位置
if (cond == &head->cond)
break;
front = head;
head = head->next;
}
if (head) {
if (front)
front->next = head->next;
else
pool->thrs = head->next;
// head 結(jié)點(diǎn)就是我們要找的
pthread_cond_destroy(&head->cond);
free(head);
}
}
// 找到線程 tid 對應(yīng)的條件變量地址
static struct thread * _threads_get(struct threads * pool, pthread_t tid) {
struct thread * head = pool->thrs;
while (head) {
if (pthread_equal(tid, head->tid))
return head;
head = head->next;
}
return NULL;
}
// 找到空閑的線程, 并返回起信號量
static pthread_cond_t * _threads_getcont(struct threads * pool) {
struct thread * head = pool->thrs;
while (head) {
if (head->wait)
return &head->cond;
head = head->next;
}
return NULL;
}
對于 struct threads 結(jié)構(gòu)中 struct job * head, * tail; 是個(gè)待處理的任務(wù)隊(duì)列.
struct thread * thrs; 鏈接所有線程對象的鏈表. 線程池對象中共用 struct threads
中 mutex 一個(gè)互斥量. 希望還記得前面數(shù)據(jù)結(jié)構(gòu)部分扯的, 鏈表是 C結(jié)構(gòu)中基礎(chǔ)的內(nèi)丹,
所有代碼都是或多或少圍繞它這個(gè)結(jié)構(gòu). 要在勤磨練中熟悉提高, 對于剛學(xué)習(xí)的人. 上面代
碼其實(shí)和業(yè)務(wù)代碼沒啥區(qū)別, 創(chuàng)建銷毀添加查找等. 前戲營造的估計(jì)夠了, 現(xiàn)在開搞其它接
口簡單實(shí)現(xiàn)代碼 :
// 開啟的線程數(shù)
#define _UINT_THREADS (4u)
threads_t
threads_create(void) {
struct threads * pool = calloc(1, sizeof(struct threads));
if (NULL == pool) {
RETURN(NULL, "calloc sizeof(struct threads) is error!");
}
pool->size = _UINT_THREADS;
pthread_mutex_init(&pool->mutx, NULL);
return pool;
}
上面創(chuàng)建接口的實(shí)現(xiàn)代碼中, calloc 相比 malloc 多調(diào)用了 bzero 的相關(guān)置零清空操作.
還有一個(gè)釋放資源函數(shù). 設(shè)計(jì)意圖允許創(chuàng)建多個(gè)線程池(但是沒有必要). 請看下面優(yōu)雅的銷
毀操作:
void
threads_delete(threads_t pool) {
struct job * head;
struct thread * thrs;
if (!pool || pool->cancel)
return;
// 標(biāo)識當(dāng)前線程池正在銷毀過程中
pool->cancel = true;
pthread_mutex_lock(&pool->mutx);
// 先來釋放任務(wù)列表
head = pool->head;
while (head) {
struct job * next = head->next;
free(head);
head = next;
}
pool->head = pool->tail = NULL;
pthread_mutex_unlock(&pool->mutx);
// 再來銷毀每個(gè)線程
thrs = pool->thrs;
while (thrs) {
struct thread * next = thrs->next;
// 激活每個(gè)線程讓其主動(dòng)退出
pthread_cond_signal(&thrs->cond);
pthread_join(thrs->tid, NULL);
thrs = next;
}
// 銷毀自己
pthread_mutex_destroy(&pool->mutx);
free(pool);
}
用到很多 pthread api. 不熟悉的多搜索, 多做筆記. 不懂多了, 說明提升功力的機(jī)會(huì)來了.
對于上面釋放函數(shù)先監(jiān)測銷毀標(biāo)識, 后競爭唯一互斥量, 競爭到后就開始釋放過程. 先清除任
務(wù)隊(duì)列并置空, 隨后解鎖. 再去準(zhǔn)備銷毀每個(gè)線程, 激活它并等待它退出. 最終清除鎖銷毀自
己. 優(yōu)雅結(jié)束了線程池的生成周期.
如果不知道是不是真愛, 那就追求優(yōu)雅 ~
如果是真愛, 那么什么都不想要 ~
6.1.3 線程池核心部分
核心部分就兩個(gè)函數(shù), 一個(gè)是線程輪詢處理任務(wù)的函數(shù). 另一個(gè)是構(gòu)建線程池函數(shù). 線程輪詢
函數(shù)如下:
// 線程運(yùn)行的時(shí)候執(zhí)行函數(shù), 消費(fèi)者線程
static void _consumer(struct threads * pool) {
int status;
struct thread * thrd;
pthread_cond_t * cond;
pthread_t tid = pthread_self();
pthread_mutex_t * mutx = &pool->mutx;
pthread_mutex_lock(mutx);
thrd = _threads_get(pool, tid);
assert(thrd);
cond = &thrd->cond;
// 使勁循環(huán)的主體, 開始消費(fèi) or 沉睡
while (!pool->cancel) {
if (pool->head) {
struct job * job = pool->head;
pool->head = job->next;
// 隊(duì)列尾置空監(jiān)測
if (pool->tail == job)
pool->tail = NULL;
// 解鎖, 允許其它消費(fèi)者線程加鎖或生產(chǎn)者添加新任務(wù)
pthread_mutex_unlock(mutx);
// 回調(diào)函數(shù), 后面再去刪除任務(wù)
job->run(job->arg);
free(job);
// 新的一輪開始, 需要重新加鎖
pthread_mutex_lock(mutx);
continue;
}
// job 已經(jīng)為empty , 開啟線程等待
thrd->wait = true;
++pool->idle;
// 開啟等待, 直到線程被激活
status = pthread_cond_wait(cond, mutx);
if (status < 0) {
pthread_detach(tid);
CERR("pthread_cond_wait error status = %zu.", status);
break;
}
thrd->wait = false;
--pool->idle;
}
// 到這里程序出現(xiàn)異常, 線程退出中, 先減少當(dāng)前線程
--pool->curr;
// 去掉這個(gè)線程鏈表pool->thrs中對應(yīng)的數(shù)據(jù)
_threads_del(pool, cond);
// 一個(gè)線程一把鎖, 退出中
pthread_mutex_unlock(mutx);
}
對于消費(fèi)者線程 _consumer 函數(shù)運(yùn)行起來, 內(nèi)部出現(xiàn)異常進(jìn)入自銷毀操作 pthread_detach
. 外部讓其退出, 走 pthread_join 關(guān)聯(lián)接收.
突然想起以前扯的一句話, 關(guān)于提升技術(shù)好辦法
1. 多看書
2. 多寫代碼, 多搜搜, 多問問
3. 多看別人的好代碼, 多臨摹源碼
4. 多創(chuàng)造, 多改進(jìn), 多實(shí)戰(zhàn)
等該明白的都明白了, 一切都是那樣容易, 那樣的美的時(shí)候. 就可以回家種田了. 哈哈 ~
到這里線程池是結(jié)束了, 不妨為其寫個(gè)測試代碼吧 ~
#include "scthreads.h"
// 測試開啟線程量集
#define _INT_THS (8192)
//全局計(jì)時(shí)器,存在鎖問題
static int _old;
//簡單的線程打印函數(shù)
static inline void _ppt(const char * str) {
printf("%d => %s\n", ++_old, str);
}
//另一個(gè)線程測試函數(shù)
static inline void _doc(void * arg) {
printf("p = %d, 技術(shù)不決定項(xiàng)目的成敗!我老大哭了\n", ++_old);
}
int main(void) {
//創(chuàng)建線程池
threads_t pool = threads_create();
//添加任務(wù)到線程池中
for (int i = 0; i<_INT_THS; ++i) {
threads_insert(pool, _ppt, "你為你負(fù)責(zé)的項(xiàng)目拼命過嗎.流過淚嗎");
threads_insert(pool, _doc, NULL);
}
//等待5s 再結(jié)束吧
sh_msleep(5000);
//清除當(dāng)前線程池資源, 實(shí)戰(zhàn)上線程池是常駐內(nèi)存,不要清除.
threads_delete(pool);
return EXIT_SUCCESS;
}
咱們費(fèi)了老大勁寫了個(gè)線程池, 9.9 業(yè)務(wù)基本都不會(huì)用到. 密集型業(yè)務(wù)目前修真界都流
行少量線程加輪詢消息隊(duì)列的方式處理, 下一個(gè)出場的主角登場了 ~
4.2 消息輪序器
系統(tǒng)開發(fā)中, 消息輪詢器基本上就是整個(gè)服務(wù)器調(diào)度處理的核心! 所有待處理的業(yè)務(wù)統(tǒng)
一封裝 push 進(jìn)去, 單獨(dú)線程異步 loop 去處理, 周而復(fù)始. 等同于守護(hù)門派安定的
無上大陣. 下面就帶大家寫個(gè)超酷炫的封魔大鎮(zhèn), 收獲門派一世繁華 ~
接口設(shè)計(jì)部分 scrunloop.h
#ifndef _H_SIMPLEC_SCRUNLOOP
#define _H_SIMPLEC_SCRUNLOOP
#include "struct.h"
typedef struct srl * srl_t;
//
// srl_create - 創(chuàng)建輪詢服務(wù)對象
// run : 輪序處理每條消息體, 彈出消息體的時(shí)候執(zhí)行
// die : srl_push msg 的銷毀函數(shù)
// return : void
//
srl_t srl_create_(node_f run, node_f die);
#define srl_create(run, die) srl_create_((node_f)(run), (node_f)(die))
//
// srl_delete - 銷毀輪詢對象,回收資源
// s : 輪詢對象
// return : void
//
extern void srl_delete(srl_t srl);
//
// srl_push - 將消息壓入到輪詢器中
// s : 輪詢對象
// msg : 待加入的消息地址
// return : void
//
extern void srl_push(srl_t s, void * msg);
#endif // !_H_SIMPLEC_SCRUNLOOP
通過宏函數(shù) srl_create 啟動(dòng)一個(gè)消息輪序器, 需要注冊兩個(gè)函數(shù) run 和 die, 前者用于處
理每個(gè) push 進(jìn)來的消息, die 用戶 push 進(jìn)來消息的善后工作(銷毀清除). 這個(gè)類實(shí)現(xiàn)的非
常精簡. 直貼代碼, 比較有價(jià)值. 多感受其中的妙用, 真是, 小也能滿足你
scrunloop.c
#include "mq.h"
#include "schead.h"
#include "scrunloop.h"
#include <semaphore.h>
// 具體輪詢器
struct srl {
pthread_t id; // 具體奔跑的線程
sem_t blocks; // 線程阻塞
mq_t mq; // 消息隊(duì)列
node_f run; // 每個(gè)消息都會(huì)調(diào)用 run(pop())
node_f die; // 每個(gè)消息體的善后工作
volatile bool loop; // true表示還在繼續(xù)
volatile bool wait; // true表示當(dāng)前輪序器正在等待
};
static void * _srl_loop(struct srl * s) {
while (s->loop) {
void * pop = mq_pop(s->mq);
if (NULL == pop) {
s->wait = true;
sem_wait(&s->blocks);
} else {
// 開始處理消息
s->run(pop);
s->die(pop);
}
}
return s;
}
srl_t
srl_create_(node_f run, node_f die) {
struct srl * s = malloc(sizeof(struct srl));
assert(s && run);
s->mq = mq_create();
s->loop = true;
s->run = run;
s->die = die;
s->wait = true;
// 初始化 POSIX 信號量, 進(jìn)程內(nèi)線程共享, 初始值 0
sem_init(&s->blocks, 0, 0);
// 創(chuàng)建線程,并啟動(dòng)
if (pthread_create(&s->id, NULL, (start_f)_srl_loop, s)) {
mq_delete(s->mq, die);
free(s);
RETURN(NULL, "pthread_create create error !!!");
}
return s;
}
void
srl_delete(srl_t s) {
if (s) {
s->loop = false;
sem_post(&s->blocks);
// 等待線程結(jié)束, 然后退出
pthread_join(s->id, NULL);
sem_destroy(&s->blocks);
mq_delete(s->mq, s->die);
free(s);
}
}
inline void
srl_push(srl_t s, void * msg) {
assert(s && msg);
mq_push(s->mq, msg);
if (s->wait) {
s->wait = false;
sem_post(&s->blocks);
}
}
對于 struct srl::die 可以優(yōu)化, 支持為 NULL 操作, 或者添加函數(shù)行為. 但是總的而言,
語法糖層面支持的越好, 用起來是爽了, 多次爽之后那種退燒的感覺會(huì)更有意思. C 很難把所
有業(yè)務(wù)統(tǒng)一起來支持, 那樣的代碼臃腫和不敢維護(hù). 體會(huì)并學(xué)會(huì)那種 C 設(shè)計(jì)的思路, 大不了
為另一種業(yè)務(wù)寫個(gè)更加貼切的支持, 如果追求性能的話.
再分析會(huì), 假如是多線程環(huán)境 srl_push 觸發(fā)了并發(fā)操作, 相應(yīng)的 sem_post 會(huì)執(zhí)行多次
P 操作. 但 _srl_loop 是單線程輪詢處理的, 只會(huì)觸發(fā)對映次的 sem_wait V 操作. 所以
push 不加鎖不影響業(yè)務(wù)正確性. 而且 sem_wait 是通用層面阻塞性能最好的選擇. 這些都是
高效的保證. 武技修煉中, srl 庫是繼 clog 庫之后, 最小意外的實(shí)現(xiàn) ~
修煉到現(xiàn)在, 逐漸進(jìn)出妖魔戰(zhàn)場, 另一個(gè)異持E担恐怖 - 域外天魔正在逼近. 它會(huì)拷問你的心,
你為什么修煉編程 ? 進(jìn)入彌天幻境, 多數(shù)人在幻境的路中間 ~ 不曾解脫 ~
走不過去那條大道, 元嬰終身無望 ~
4.3 http util by libcurl
開發(fā)中對于 http 請求處理, 也是一個(gè)重要環(huán)節(jié), 鏈接運(yùn)營管理業(yè)務(wù), 不可缺失. 同樣都有固
定套路. 這里用的是 libcurl c http 處理庫.
// linux 安裝總結(jié)
sudo apt-get install libcurl4-openssl-dev
// winds 使用方法
/*
方法1. 查教程, 去 libcurl 官網(wǎng)下載最新版本編譯
方法2. 如果你怕麻煩 github 找我的項(xiàng)目 simplc, 參照 extern 目錄下面
方法3. 隨便, 只要你搭好 hello world 就行
*/
按照上面思路, 搭建好環(huán)境開搞. 服務(wù)器中對于 http 請求, 主要圍繞在 get 和 post.
接收別人的 http 請求和接收 tcp請求一樣, 通過協(xié)議得到完整報(bào)文, 拋給業(yè)務(wù)層. 所
以封裝的 http 工具庫, 接口功能很單一. 就是對外發(fā)送 get post 請求:
httputil.h
#ifndef _H_SIMPLEC_HTTPUTIL
#define _H_SIMPLEC_HTTPUTIL
#include "tstr.h"
#include <curl/curl.h>
//
// http_start - http 庫啟動(dòng)
// return : void
//
extern void http_start(void);
//
// http_get - http get 請求 or https get請求
// url : 請求的url
// str : 返回最終申請的結(jié)果, NULL表示只請求不接收數(shù)據(jù)
// return : true表示請求成功
//
extern bool http_get(const char * url, tstr_t str);
extern bool http_sget(const char * url, tstr_t str);
//
// http_post - http post 請求 or https post 請求
// url : 請求的路徑
// params : 參數(shù)集 key=value&.... 串
// str : 返回結(jié)果, 需要自己釋放, NULL表示只請求不接受數(shù)據(jù)返回
// return : false表示請求失敗, 會(huì)打印錯(cuò)誤信息
//
extern bool http_post(const char * url, const char * params, tstr_t str);
extern bool http_spost(const char * url, const char * params, tstr_t str);
#endif // !_H_SIMPLEC_HTTPUTIL
對于 http_start 啟動(dòng), 稍微說點(diǎn). 不知道有沒有人好奇, winds 上面使用 socket 的
時(shí)候需要先加載 WSAStartup 操作. 其實(shí)是我們用慣了 linux 上面的, 沒有先加載操作.
linux c 程序編譯的時(shí)候, 默認(rèn)加了 socket 庫的初始化加載工作. 所以咱們就不需要繼
續(xù)加載了(編譯器主動(dòng)做了). 這就清晰了 xxx_start 相當(dāng)于啟動(dòng)操作, 先啟動(dòng)車子, 才
能操作車子不是嗎. 那我們繼續(xù)看吧
#include "httputil.h"
// 超時(shí)時(shí)間10s
#define _INT_TIMEOUT (4)
// libcurl 庫退出操作
static inline void _http_end(void) {
curl_global_cleanup();
}
inline void
http_start(void) {
//
// CURLcode curl_global_init(long flags);
// @ 初始化libcurl, 全局只需調(diào)一次
// @ flags : CURL_GLOBAL_DEFAULT // 等同于 CURL_GLOBAL_ALL
// CURL_GLOBAL_ALL // 初始化所有的可能的調(diào)用
// CURL_GLOBAL_SSL // 初始化支持安全套接字層
// CURL_GLOBAL_WIN32 // 初始化WIN32套接字庫
// CURL_GLOBAL_NOTHING // 沒有額外的初始化
//
CURLcode code = curl_global_init(CURL_GLOBAL_DEFAULT);
if (code != CURLE_OK) {
RETURN(NIL, "curl_global_init error = %s.", curl_easy_strerror(code));
}
atexit(_http_end);
}
這類 start 操作基本都是單例, 在 main 函數(shù)啟動(dòng)的時(shí)候組織順序執(zhí)行一次就可以. 其中
atexit 可有可無, 因?yàn)?http_start 操作 curl_global_init 需要跟隨程序整個(gè)生命周期.
對于這種同程序同生共死的操作, 流程操作系統(tǒng)回收吧. 下面 http_get 函數(shù)也只是熟悉
libcurl 庫的簡單用法:
// 請求共有頭部
static CURL * _http_head(const char * url, tstr_t str) {
CURL * crl = curl_easy_init();
if (NULL == crl) {
RETURN(NULL, "curl_easy_init error !!!");
}
// 設(shè)置下載屬性和常用參數(shù)
curl_easy_setopt(crl, CURLOPT_URL, url); // 訪問的URL
curl_easy_setopt(crl, CURLOPT_TIMEOUT, _INT_TIMEOUT); // 設(shè)置超時(shí)時(shí)間 單位s
curl_easy_setopt(crl, CURLOPT_HEADER, true); // 下載數(shù)據(jù)包括HTTP頭部
curl_easy_setopt(crl, CURLOPT_NOSIGNAL, true); // 屏蔽其它信號
curl_easy_setopt(crl, CURLOPT_WRITEFUNCTION, (curl_write_callback)_http_write); // 輸入函數(shù)類型
curl_easy_setopt(crl, CURLOPT_WRITEDATA, str); // 輸入?yún)?shù)
return crl;
}
// 請求共有尾部
static inline bool _http_tail(CURL * crl, tstr_t str) {
CURLcode res = curl_easy_perform(crl);
tstr_cstr(str);
curl_easy_cleanup(crl);
if (res != CURLE_OK) {
RETURN(false, "curl_easy_perform error = %s!", curl_easy_strerror(res));
}
return true;
}
inline bool
http_get(const char * url, tstr_t str) {
CURL * crl = _http_head(url, str);
return crl ? _http_tail(crl, str) : false;
}
對于 http_sget 走 https 協(xié)議, 相比 http_get 請求也只是多了個(gè)屬性設(shè)置
// 添加 https 請求設(shè)置
static inline void _http_sethttps(CURL * crl) {
curl_easy_setopt(crl, CURLOPT_SSL_VERIFYPEER, false);
curl_easy_setopt(crl, CURLOPT_SSL_VERIFYHOST, false);
}
inline bool
http_sget(const char * url, tstr_t str) {
CURL * crl = _http_head(url, str);
if (crl) {
_http_sethttps(crl);
return _http_tail(crl, str);
}
return false;
}
get 方面請求完了, 自然 post 也會(huì)很快就完了. 不是它太簡單, 而是這種看的多了,
查查手冊, 看看源碼, 用的會(huì)賊溜 ~
// 添加 post 請求設(shè)置
static inline void _http_setpost(CURL * crl, const char * params) {
curl_easy_setopt(crl, CURLOPT_POST, true);
curl_easy_setopt(crl, CURLOPT_POSTFIELDS, params);
}
inline bool
http_post(const char * url, const char * params, tstr_t str) {
CURL * crl = _http_head(url, str);
if (crl) {
_http_setpost(crl, params);
return _http_tail(crl, str);
}
return false;
}
bool
http_spost(const char * url, const char * params, tstr_t str) {
CURL * crl = _http_head(url, str);
if (crl) {
_http_sethttps(crl);
_http_setpost(crl, params);
return _http_tail(crl, str);
}
return false;
}
前面幾章內(nèi)功練到好, 這里武技只是三花聚頂, 越打越帶勁. 出掌震飛尸, 拔劍滅妖魔.
當(dāng)然了 http 解決方案不少, 很多知名網(wǎng)絡(luò)庫都會(huì)附贈(zèng)個(gè) http 處理小單元. libcurl 是
我見過比較良心的了. 成熟可靠, 資料很全. 武技講的這么多, 希望修煉的你能懂. 一切
都是套路, 只有赤城之人才能走到道的遠(yuǎn)方 ~
4.4 閱讀理解時(shí)間, 不妨來個(gè)定時(shí)器
對于定式器實(shí)現(xiàn)而言無外乎三大套路. 一種是有序鏈表用于解決, 大量重復(fù)輪詢的定時(shí)結(jié)點(diǎn)設(shè)
計(jì)的. 另一種是采用時(shí)間堆構(gòu)建的定時(shí)器, 例如小頂堆, 時(shí)間差最小的在堆定執(zhí)行速度最快.
還有一種時(shí)間片結(jié)構(gòu), 時(shí)間按照一定顆度轉(zhuǎn)呀轉(zhuǎn), 轉(zhuǎn)到那就去執(zhí)行那條顆度上的鏈表. 總的而
言定時(shí)器的套路不少, 具體看應(yīng)用的場景. 我們這里帶來的閱讀理解是基于有序鏈表, 剛好溫
故下 list 用. 可以說起緣 list, 終于 list.
sctimer.h
#ifndef _H_SIMPLEC_SCTIMER
#define _H_SIMPLEC_SCTIMER
#include "schead.h"
//
// st_add - 添加定時(shí)器事件,雖然設(shè)置的屬性有點(diǎn)多但是都是必要的
// intval : 執(zhí)行的時(shí)間間隔, <=0 表示立即執(zhí)行, 單位是毫秒
// timer : 定時(shí)器執(zhí)行函數(shù)
// arg : 定時(shí)器參數(shù)指針
// return : 返回這個(gè)定時(shí)器的唯一id
//
extern int st_add_(int intval, node_f timer, void * arg);
#define st_add(intval, timer, arg) st_add_(intval, (node_f)timer, (void *)(intptr_t)arg)
//
// st_del - 刪除指定事件
// id : st_add 返回的定時(shí)器id
// return : void
//
extern void st_del(int id);
#endif // !_H_SIMPLEC_SCTIMER
結(jié)構(gòu)清晰易懂, 業(yè)務(wù)只有添加和刪除. 順便用了一個(gè)去除警告的函數(shù)宏技巧 st_add . 實(shí)現(xiàn)部分如下,
重點(diǎn)感受數(shù)據(jù)結(jié)構(gòu)內(nèi)功的 list 結(jié)構(gòu)的用法.
// 使用到的定時(shí)器結(jié)點(diǎn)
struct stnode {
$LIST_HEAD;
int id; // 當(dāng)前定時(shí)器的id
struct timespec tv; // 運(yùn)行的具體時(shí)間
node_f timer; // 執(zhí)行的函數(shù)事件
void * arg; // 執(zhí)行函數(shù)參數(shù)
};
// 當(dāng)前鏈表對象管理器
struct stlist {
int lock; // 加鎖用的
int nowid; // 當(dāng)前使用的最大timer id
bool status; // false表示停止態(tài), true表示主線程loop運(yùn)行態(tài)
struct stnode * head; // 定時(shí)器鏈表的頭結(jié)點(diǎn)
};
// 定時(shí)器對象的單例, 最簡就是最復(fù)雜
static struct stlist _st;
// 先創(chuàng)建鏈表對象處理函數(shù)
static struct stnode * _stnode_new(int s, node_f timer, void * arg) {
struct stnode * node = malloc(sizeof(struct stnode));
if (NULL == node)
RETURN(NULL, "malloc struct stnode is error!");
// 初始化, 首先初始化當(dāng)前id
node->id = ATOM_INC(_st.nowid);
timespec_get(&node->tv, TIME_UTC);
node->tv.tv_sec += s / 1000;
node->tv.tv_nsec += (s % 1000) * 1000000;
node->timer = timer;
node->arg = arg;
return node;
}
// 得到等待的微秒時(shí)間, <=0的時(shí)候頭時(shí)間就可以執(zhí)行了
static inline int _stlist_sus(struct stlist * st) {
struct timespec t[1], * v = &st->head->tv;
timespec_get(t, TIME_UTC);
return (int)((v->tv_sec - t->tv_sec) * 1000000
+ (v->tv_nsec - t->tv_nsec) / 1000);
}
// 重新調(diào)整, 只能在 _stlist_loop 后面調(diào)用, 線程安全,只加了一把鎖
static void _stlist_run(struct stlist * st) {
struct stnode * sn;
ATOM_LOCK(st->lock); // 加鎖防止調(diào)整關(guān)系覆蓋,可用還是比較重要的
sn = st->head;
st->head = (struct stnode *)list_next(sn);
ATOM_UNLOCK(st->lock);
sn->timer(sn->arg);
free(sn);
}
// 運(yùn)行的主loop,基于timer管理器
static void * _stlist_loop(struct stlist * st) {
// 正常輪詢,檢測時(shí)間
while (st->head) {
int nowt = _stlist_sus(st);
if (nowt > 0) {
usleep(nowt);
continue;
}
_stlist_run(st);
}
// 已經(jīng)運(yùn)行結(jié)束
st->status = false;
return NULL;
}
// st < sr 返回 < 0, == 返回 0, > 返回 > 0
static inline int _stnode_cmptime(const struct stnode * sl, const struct stnode * sr) {
if (sl->tv.tv_sec != sr->tv.tv_sec)
return (int)(sl->tv.tv_sec - sr->tv.tv_sec);
return (int)(sl->tv.tv_nsec - sr->tv.tv_nsec);
}
int
st_add_(int intval, node_f timer, void * arg) {
struct stnode * now;
// 各種前戲操作
if (intval <= 0) {
timer(arg);
return SufBase;
}
now = _stnode_new(intval, timer, arg);
if (NULL == now) {
RETURN(ErrAlloc, "_new_stnode is error intval = %d.", intval);
}
ATOM_LOCK(_st.lock); //核心添加模塊 要等, 添加到鏈表, 看線程能否取消等
list_add(&_st.head, _stnode_cmptime, now);
// 這個(gè)時(shí)候重新開啟線程
if(!_st.status) {
if (async_run(_stlist_loop, &_st)) {
list_destroy(&_st.head, free);
RETURN(ErrFd, "pthread_create is error!");
}
_st.status = true;
}
ATOM_UNLOCK(_st.lock);
return now->id;
}
// 通過id開始查找
static inline int _stnode_cmpid(int id, const struct stnode * sr) {
return id - sr->id;
}
void
st_del(int id) {
struct stnode * node;
if (!_st.head) return;
ATOM_LOCK(_st.lock);
node = list_findpop(&_st.head, _stnode_cmpid, id);
ATOM_UNLOCK(_st.lock);
free(node);
}
其中用到的 usleep 移植到 winds 上面實(shí)現(xiàn)為
// 為 Visual Studio 導(dǎo)入一些 GCC 上優(yōu)質(zhì)思路
#ifdef _MSC_VER
//
// usleep - 微秒級別等待函數(shù)
// usec : 等待的微秒
// return : The usleep() function returns 0 on success. On error, -1 is returned.
//
int
usleep(unsigned usec) {
int rt = -1;
// Convert to 100 nanosecond interval, negative value indicates relative time
LARGE_INTEGER ft = { .QuadPart = -10ll * usec };
HANDLE timer = CreateWaitableTimer(NULL, TRUE, NULL);
if (timer) {
// 負(fù)數(shù)以100ns為單位等待, 正數(shù)以標(biāo)準(zhǔn)FILETIME格式時(shí)間
SetWaitableTimer(timer, &ft, 0, NULL, NULL, FALSE);
WaitForSingleObject(timer, INFINITE);
if (GetLastError() == ERROR_SUCCESS)
rt = 0;
CloseHandle(timer);
}
return rt;
}
#endif
以上 sctime 模塊中操作, 無外乎利用 list 構(gòu)建了一個(gè)升序鏈表, 通過額外異步分離
線程 loop 監(jiān)測下去并執(zhí)行. 定時(shí)器一個(gè)通病, 不要放入阻塞函數(shù), 容易失真.
sctimer 使用方面也很簡單, 例如一個(gè)技能, 吟唱 1s, 持續(xù)傷害 2s. 構(gòu)造如下:
struct skills {
int id;
bool exist; // 實(shí)戰(zhàn)走狀態(tài)機(jī), true 表示施法狀態(tài)中
};
// 007 號技能 火球術(shù), 沒有釋放
struct skills fireball = { 007, false };
static void _end(struct skills * kill) {
...
if (kill.id == 007) {
puts("火球術(shù)持續(xù)輸出結(jié)束...");
kill.exist = false;
}
...
}
static void _continued(struct skills * kill) {
...
if (kill.id == 007) {
puts("火球術(shù)吟唱成功, 開始持續(xù)輸出...");
kill.exist = true;
st_add(2000, _end, kill);
}
...
}
static void _start(struct skills * kill) {
...
if (kill.id == 007) {
puts("火球術(shù)開始吟唱...");
kill.exist = false;
st_add(1000, _continued, kill);
}
...
}
調(diào)用 _start 就可以了, 火球術(shù)吟唱, 持續(xù)輸出. 中間打斷什么鬼, 那就自己擴(kuò)展. 后期
根據(jù)標(biāo)識統(tǒng)一繪制顯示. 以上是簡單到吐的思路說不定也很有效. 有點(diǎn)像優(yōu)化過的 select
特定的時(shí)候出其不意 ~
過的真快, 修煉之路已經(jīng)走過小一半了. 從華山劍法練起, 到現(xiàn)在的一步兩步三步. 以后
自己可以上路了, 單純的客戶端業(yè)務(wù)的小妖魔, 分分鐘可以干掉吧. 本章在實(shí)戰(zhàn)中會(huì)用的最
多就是日月輪 sclooprun 模塊. 對于定時(shí)器, 多數(shù)內(nèi)嵌到主線程輪詢模塊(update) 中.
此刻應(yīng)該多出去歷練求索, 在血與歌中感受生的洗禮. 聆聽心中的道. Thx
元日
王安石 - 宋代
爆竹聲中一歲除,春風(fēng)送暖入屠蘇屋匕。
千門萬戶曈曈日,總把新桃換舊符。