當前位置:
首頁 > 知識 > 這位 GitHub 冠軍項目背後的「老男人」,堪稱 10 倍程序員本尊

這位 GitHub 冠軍項目背後的「老男人」,堪稱 10 倍程序員本尊

這位 GitHub 冠軍項目背後的「老男人」,堪稱 10 倍程序員本尊

這位 GitHub 冠軍項目背後的「老男人」,堪稱 10 倍程序員本尊

作者 | 馬超,CSDN博客專家,金融科技從業者來源 | CSDN博客

7月12日一款叫做TDengine的時序資料庫項目在GitHub上開源了,這個項目一經發布就穩穩佔據了GitHub排行榜的C位,目前TdEngine已經累積了5000多個star,並且連續一周排在上升榜首位。而且你要知道TdEngine的開發語言並不是火熱的Python或JAVA,而是C語言。C語言無巧可取,雖見功夫,但是代碼比較難讀,能引發如此的關注絕對堪稱奇蹟,在我印象中即使是Mysql也沒有達到如此的熱度。

相信很多人也和筆者一樣,是通過《比hadoop快至少10 倍的物聯網大數據平台,我把它開源》的刷屏文才了解到陶老師與TdEngine的,當看到這位50歲的IT老兵老兵,依舊奮鬥在編程一線,為TDengine開發貢獻3萬行代碼時候,我就立刻四處向朋友打聽,並最終要了陶老師的微信,做為一名80後程序員,我近不急待的想和陶老師直接溝通,想從他身上找到保持編程水平的秘決。

這位 GitHub 冠軍項目背後的「老男人」,堪稱 10 倍程序員本尊

這位 GitHub 冠軍項目背後的「老男人」,堪稱 10 倍程序員本尊

大神面對面:這才是10倍程序員該有的樣子

2008年的時候筆者還是CSDN論壇WINDOWS MOBILE版的版主,從事手機導航軟體的開發工作,而在彼時陶老師也創辦了和信公司,並親自開發了WindowsMobile版和信客戶端,相同的開發平台經歷也讓我們迅速的拉進了彼此的距離。

在使用TdEngine的過程中我發現了兩個小問題,一是資料庫用戶密碼明文存放,二是數據文件許可權設置不合理。讓我十分震驚的是,這兩個問題是我下午在和陶老師聊天時提出的,當晚發布版本就把問題全部解決了。後來溝通得知這些BUG都是陶老師自己動手修改的。我意識到TdEngine的效率應該來自於創始人對於代碼的執著與熱愛,而不是對員工996式的工作要求

陶老師是真的愛編程,尤其對於代碼運行效率有著近乎狂熱的追求,我查閱了陶老師近年來的作品,其和信客戶端只有18K大小,胎心演算法的實現只用了600行代碼,而TDengine這樣一個資料庫項目竟然只需要1.5M安裝包就能搞定,在手機APP都動轍上百M的今天,TDengine體量甚至顯得有些異類。如果沒有深厚的功底和堅定的信念是絕對無法達到如此高度的。我想陶老師應該就是傳說中10倍程序員的典範吧。

10倍程序員對於他周圍親友的影響也是非常巨大的,當我打開TdEngine的官網(https://www.taosdata.com/cn/),其簡潔明快的風格,一目了然的配圖,實在讓我無法把這一切和一位年近半百的老派IT士人聯繫到一起,當然後來我和陶老師聊到這件事的時候才知道,整個網站從設計、前端、後台、瀏覽器適配、數據分析到搜索引擎優化,都是由陶老師的兒子,一位剛剛高中畢業的00後操刀主持的,而且整個網站從無到有只用了三周時間,除了感嘆一句後生可畏,由此也可以看出來和10倍程序員並肩作戰的也都是10倍程序員,所以it團隊的負責人在感嘆自己沒有18程序員相助時也要反思一下,自己是不是一位10程序員。

這位 GitHub 冠軍項目背後的「老男人」,堪稱 10 倍程序員本尊

TdEngine為什麼會火?

傳統資料庫廠商的問題在於傲慢、自大,他們認為數據是零件,資料庫則是各類零件的加中心,很多工序都是為數據的修改準備的,無論修改是否發生加工車間為了保證一致性,都會對流水線上的數據加上各種各樣的鎖。這些操作浪費了很多時間,而且幾乎沒有任何輕量級的框架,可供用戶選擇省略掉這些冗餘操作。而且傳統廠商為了解決資料庫的性能問題不是從底層架構邏輯下手,而是不休止的在應用與資料庫之間加入各種像REDIS,NGIX等等代理或者緩存層,這種方式其實是加大了各層級間的性能開銷。傳統廠商認為自己非常了解數據,但卻忘了用戶比廠商更加了解自己的數據,天下可謂苦秦久已

而TdEngine是認為數據是信息流,它要做的非常簡單,只是數據的錄像機而已,信息調閱只要找到對應的錄像帶即可,這樣的設計思路從底層邏輯上決定了td會是一款性能極高的產品。它更加貼合物聯網時代的數據模型,而且代碼只有10萬行的量級,非常適合從從頭開始學習。

所以TdEngine精確的找到了資料庫市場的細分戰場。他可以在相同的硬體條件下達到其它產品10倍的速度,完美解決了很多物聯網,量化交易等場景的痛點。

這位 GitHub 冠軍項目背後的「老男人」,堪稱 10 倍程序員本尊

TdEngine代碼導讀

當筆者打TdEngine的代碼時不由眼前一亮,其代碼風格及規範性絕對堪稱一流,於是我打開了久違的souce insight,,再一次開始了閱讀C語言代碼的美妙旅程,在這裡強烈推薦各位讀者也來讀一下,絕對堪稱享受。

這裡將給我啟示最大的一段代碼其鏈接在


https://github.com/taosdata/TDengine/blob/master/src/util/src/tsched.c

向大家分享一下。鑒於本文肯定會分享給陶老師,所以估計會有作者親答的環節:-),以下代碼是一個典型的consumer-producer消息傳遞功能的實現,也就是有多個生產者(producer)生成並不斷向隊列中傳遞消息,也有多個消費者(consumer)不斷從隊列中取消息,而在java等高級語言中類似的功能已經被封裝好了,這其實也讓程序員無法了解線程間的同步和互斥機制。在正式進入到代碼之前我想請大家思考這樣的一個,互斥體( mutex)和信號量(semaphore)的使用是如何做到多線程安全的。

先來看結構體設計,具體我已經注釋好了:

typedef struct {

char label[16];#消息內容

sem_t emptySem;#此信號量代表隊列的可寫狀態

sem_t fullSem;#此信號量代表隊列的可讀狀態

pthread_mutex_t queueMutex;#此互斥體為保證消息不會被誤修改,保證線程程安全

int fullSlot;#隊尾位置

int emptySlot;#隊頭位置

int queueSize;#隊列長度

int numOfThreads;#同時操作的線程數量

pthread_t * qthread;#線程指針

SSchedMsg * queue;#隊列指針

} SSchedQueue;

再來看初始化函數,這裡需要特別說明的是,兩個信號量的創建,其中emptySem是隊列的可寫狀態,初始化時其值為queueSize,即初始時隊列可寫,可接受消息長度為隊列長度,fullSem是隊列的可讀狀態,初始化時其值為0,即初始時隊列不可讀。具體代碼及我的注釋如下:

void *taosInitScheduler(int queueSize, int numOfThreads, char *label) {

pthread_attr_t attr;

SSchedQueue * pSched = (SSchedQueue *)malloc(sizeof(SSchedQueue));

memset(pSched, 0, sizeof(SSchedQueue));

pSched->queueSize = queueSize;

pSched->numOfThreads = numOfThreads;

strcpy(pSched->label, label);

if (pthread_mutex_init(&pSched->queueMutex, ) < 0) {

pError("init %s:queueMutex failed, reason:%s", pSched->label, strerror(errno));

goto _error;

}

#emptySem是隊列的可寫狀態,初始化時其值為queueSize,即初始時隊列可寫,可接受消息長度為隊列長度。

if (sem_init(&pSched->emptySem, 0, (unsigned int)pSched->queueSize) != 0) {

pError("init %s:empty semaphore failed, reason:%s", pSched->label, strerror(errno));

goto _error;

}

#fullSem是隊列的可讀狀態,初始化時其值為0,即初始時隊列不可讀

if (sem_init(&pSched->fullSem, 0, 0) != 0) {

pError("init %s:full semaphore failed, reason:%s", pSched->label, strerror(errno));

goto _error;

}

if ((pSched->queue = (SSchedMsg *)malloc((size_t)pSched->queueSize * sizeof(SSchedMsg))) == ) {

pError("%s: no enough memory for queue, reason:%s", pSched->label, strerror(errno));

goto _error;

}

memset(pSched->queue, 0, (size_t)pSched->queueSize * sizeof(SSchedMsg));

pSched->fullSlot = 0;#實始化時隊列為空,故隊頭和隊尾的位置都是0

pSched->emptySlot = 0;#實始化時隊列為空,故隊頭和隊尾的位置都是0

pSched->qthread = malloc(sizeof(pthread_t) * (size_t)pSched->numOfThreads);

pthread_attr_init(&attr);

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

for (int i = 0; i < pSched->numOfThreads; ++i) {

if (pthread_create(pSched->qthread + i, &attr, taosProcessSchedQueue, (void *)pSched) != 0) {

pError("%s: failed to create rpc thread, reason:%s", pSched->label, strerror(errno));

goto _error;

}

}

pTrace("%s scheduler is initialized, numOfThreads:%d", pSched->label, pSched->numOfThreads);

return (void *)pSched;

_error:

taosCleanUpScheduler(pSched);

return ;

}

再來看讀消息的taosProcessSchedQueue函數,這個主要邏輯是

1.使用無限循環,只要隊列可讀即sem_wait(&pSched->fullSem)不再阻塞就繼續向下處理

2.在操作msg前,加入互斥體防止msg被誤用。

3.讀操作完畢後修改fullSlot的值,注意這為避免fullSlot溢出,需要對於queueSize取余。同時退出互斥體。

4.對emptySem進行post操作,即把emptySem的值加1,如emptySem原值為5,讀取一個消息後,emptySem的值為6,即可寫狀態,且能接受的消息數量為6。

具體代碼及注釋如下:

void *taosProcessSchedQueue(void *param) {

SSchedMsg msg;

SSchedQueue *pSched = (SSchedQueue *)param;

#注意這裡是個無限循環,只要隊列可讀即sem_wait(&pSched->fullSem)不再阻塞就繼續處理

while (1) {

if (sem_wait(&pSched->fullSem) != 0) {

pError("wait %s fullSem failed, errno:%d, reason:%s", pSched->label, errno, strerror(errno));

if (errno == EINTR) {

/* sem_wait is interrupted by interrupt, ignore and continue */

continue;

}

}

#加入互斥體防止msg被誤用。

if (pthread_mutex_lock(&pSched->queueMutex) != 0)

pError("lock %s queueMutex failed, reason:%s", pSched->label, strerror(errno));

msg = pSched->queue[pSched->fullSlot];

memset(pSched->queue + pSched->fullSlot, 0, sizeof(SSchedMsg));

#讀取完畢修改fullSlot的值,注意這為避免fullSlot溢出,需要對於queueSize取余。

pSched->fullSlot = (pSched->fullSlot + 1) % pSched->queueSize;

#讀取完畢修改退出互斥體

if (pthread_mutex_unlock(&pSched->queueMutex) != 0)

pError("unlock %s queueMutex failed, reason:%s
", pSched->label, strerror(errno));

#讀取完畢對emptySem進行post操作,即把emptySem的值加1,如emptySem原值為5,讀取一個消息後,emptySem的值為6,即可寫狀態,且能接受的消息數量為6

if (sem_post(&pSched->emptySem) != 0)

pError("post %s emptySem failed, reason:%s
", pSched->label, strerror(errno));

if (msg.fp)

(*(msg.fp))(&msg);

else if (msg.tfp)

(*(msg.tfp))(msg.ahandle, msg.thandle);

}

}

最後來看寫消息的taosScheduleTask函數,其基本邏輯是

1.寫隊列前先對emptySem進行減1操作,如emptySem原值為1,那麼減1後為0,也就是隊列已滿,必須在讀取消息後,即emptySem進行post操作後,隊列才能進行可寫狀態。

2.加入互斥體防止msg被誤操作,寫入完成後退出互斥體。

3.寫隊列完成後對fullSem進行加1操作,如fullSem原值為0,那麼加1後為1,也就是隊列可讀,咱們上面介紹的讀取taosProcessSchedQueue中sem_wait(&pSched->fullSem)不再阻塞就繼續向下。

int taosScheduleTask(void *qhandle, SSchedMsg *pMsg) {

SSchedQueue *pSched = (SSchedQueue *)qhandle;

if (pSched == ) {

pError("sched is not ready, msg:%p is dropped", pMsg);

return 0;

}

#在寫隊列前先對emptySem進行減1操作,如emptySem原值為1,那麼減1後為0,也就是隊列已滿,必須在讀取消息後,即emptySem進行post操作後,隊列才能進行可寫狀態。

if (sem_wait(&pSched->emptySem) != 0) pError("wait %s emptySem failed, reason:%s", pSched->label, strerror(errno));

#加入互斥體防止msg被誤操作

if (pthread_mutex_lock(&pSched->queueMutex) != 0)

pError("lock %s queueMutex failed, reason:%s", pSched->label, strerror(errno));

pSched->queue[pSched->emptySlot] = *pMsg;

pSched->emptySlot = (pSched->emptySlot + 1) % pSched->queueSize;

if (pthread_mutex_unlock(&pSched->queueMutex) != 0)

pError("unlock %s queueMutex failed, reason:%s", pSched->label, strerror(errno));

#在寫隊列前先對fullSem進行加1操作,如fullSem原值為0,那麼加1後為1,也就是隊列可讀,咱們上面介紹的讀取函數可以進行處理。

if (sem_post(&pSched->fullSem) != 0) pError("post %s fullSem failed, reason:%s", pSched->label, strerror(errno));

return 0;

}

當然以上只是TdEngine優美代碼的一小部分,而且筆者解讀的功力也十分有限,這裡再次強烈建議大家下載全部源碼仔細學習,定能受益匪淺。

原文:

https://blog.csdn.net/BEYONDMA/article/details/96578186

【End】

這位 GitHub 冠軍項目背後的「老男人」,堪稱 10 倍程序員本尊

這位 GitHub 冠軍項目背後的「老男人」,堪稱 10 倍程序員本尊

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 CSDN 的精彩文章:

Github Trending被中文項目「佔領」,國外開發者不開心了
如何利用 MySQL 攻破資料庫性能瓶頸?

TAG:CSDN |