讓你的程式更優雅的sleep
sleep的作用無需多說,幾乎每種語言都提供了類似的函式,呼叫起來也很簡單。sleep的作用無非是讓程式等待若干時間,而為了達到這樣的目的,其實有很多種方式,最簡單的往往也是最粗暴的,我們就以下面這段程式碼來舉例說明(注:本文提及的程式編譯執行環境為Linux)
/* filename: test.cpp */ #include <stdio.h> #include <unistd.h> #include <pthread.h> #include <signal.h> class TestServer { public: TestServer() : run_(true) {}; ~TestServer(){}; void Start() { pthread_create(&thread_, NULL, ThreadProc, (void*)this); } void Stop() { run_ = false; } void Wait() { pthread_join(thread_, NULL); } void Proc() { int count = 0; while (run_) { printf("sleep count:%d\n", ++count); sleep(5); } } private: bool run_; pthread_t thread_; static void* ThreadProc(void* arg) { TestServer* me = static_cast<TestServer*>(arg); me->Proc(); return NULL; } }; TestServer g_server; void StopService() { g_server.Stop(); } void StartService() { g_server.Start(); g_server.Wait(); } void SignalHandler(int sig) { switch(sig) { case SIGINT: StopService(); default: break; } } int main(int argc, char* argv[]) { signal(SIGINT, SignalHandler); StartService(); return 0; }
這段程式碼描述了一個簡單的服務程式,為了簡化我們省略了服務的處理邏輯,也就是Proc函式的內容,這裡我們只是週期性的列印某條語句,為了達到週期性的目的,我們用sleep來實現,每隔5秒鐘列印一次。在main函式中我們對SIGINT訊號進行了捕捉,當程式在終端啟動之後,如果你輸入ctr+c,這會向程式傳送中斷訊號,一般來說程式會退出,而這裡我們捕捉到了這個訊號,會按我們自己的邏輯來處理,也就是呼叫server的Stop函式。執行編譯命令
$ g++ test.cpp -o test -lpthread
然後在終端輸入./test
執行程式,這時程式每隔5秒會在螢幕上列印一條語句,按下ctl+c,你會發現程式並沒有立即退出,而是等待了一會兒才退出,究其原因,當按下ctl+c發出中斷訊號時,程式捕捉到並執行自己的邏輯,也就是呼叫了server的Stop函式,執行標記位run_被置為false,Proc函式檢測到run_為false則退出迴圈,程式結束,但有可能(應該說大多數情況都是如此)此時Proc正好執行到sleep那一步,而sleep是將程式掛起,由於我們捕捉到了中斷訊號,因此它不會退出,而是繼續掛起直到時間滿足為止。這個sleep顯然顯得不夠優雅,下面介紹兩種能快速退出的方式。
自定義sleep
在我們呼叫系統提供的sleep時我們是無法在函式內部做其它事情的,基於此我們就萌生出一種想法,如果在sleep中能夠檢測到退出變數,那豈不是就能快速退出了,沒錯,事情就是這樣子的,通過自定義sleep,我們將時間片分割成更小的片段,每隔一個片段檢測一次,這樣就能將程式的退出延遲時間縮小為這個更小的片段,自定義的sleep如下
void sleep(int seconds, const bool* run) { int count = seconds * 10; while (*run && count > 0) { --count; usleep(100000); } }
需要注意的是,這個sleep的第二個引數必須是指標型別的,因為我們需要檢測到它的實時值,而不只是使用它傳入進來的值,相應的函式呼叫也得稍作修改,完整的程式碼如下
/* filename: test2.cpp */
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
class TestServer
{
public:
TestServer() : run_(true) {};
~TestServer(){};
void Start()
{
pthread_create(&thread_, NULL, ThreadProc, (void*)this);
}
void Stop()
{
run_ = false;
}
void Wait()
{
pthread_join(thread_, NULL);
}
void Proc()
{
int count = 0;
while (run_)
{
printf("sleep count:%d\n", ++count);
sleep(5, &run_);
}
}
private:
bool run_;
pthread_t thread_;
void sleep(int seconds, const bool* run)
{
int count = seconds * 10;
while (*run && count > 0)
{
--count;
usleep(100000);
}
}
static void* ThreadProc(void* arg)
{
TestServer* me = static_cast<TestServer*>(arg);
me->Proc();
return NULL;
}
};
TestServer g_server;
void StopService()
{
g_server.Stop();
}
void StartService()
{
g_server.Start();
g_server.Wait();
}
void SignalHandler(int sig)
{
switch(sig)
{
case SIGINT:
StopService();
default:
break;
}
}
int main(int argc, char* argv[])
{
signal(SIGINT, SignalHandler);
StartService();
return 0;
}
編譯g++ test2.cpp -o test
,執行./test
,當程式啟動之後按ctl+c
,看程式是不是很快就退出了。
其實這種退出並不是立馬退出,而是將sleep的等待時間分成了更小的時間片,上例是0.1秒,也就是說在按下ctr+c之後,程式其實還會延時0到0.1秒才會退出,只不過這個時間很短,看上去就像立馬退出一樣。
用條件變數實現sleep
大致的思想就是,在迴圈時等待一個條件變數,並設定超時時間,如果在這個時間之內有其它執行緒觸發了條件變數,等待會立即退出,否則會一直等到設定的時間,這樣就可以通過對條件變數的控制來實現sleep,並且可以在需要的時候立馬退出。
條件變數往往會和互斥鎖搭配使用,互斥鎖的邏輯很簡單,如果一個執行緒獲取了互斥鎖,其它執行緒就無法獲取,也就是說如果兩個執行緒同時執行到了pthread_mutex_lock
語句,只有一個執行緒會執行完成,而另一個執行緒會阻塞,直到有執行緒呼叫pthread_mutex_unlock
才會繼續往下執行。所以我們往往在多執行緒訪問同一記憶體區域時會用到互斥鎖,以防止多個執行緒同時修改某一塊記憶體區域。本例用到的函式有如下幾個,互斥鎖相關函式有
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
以上函式功能分別是初始化、加鎖、解鎖、銷燬。條件變數相關函式有
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_destroy(pthread_cond_t *cond);
以上函式功能分別是初始化、超時等待條件變數、觸發條件變數、銷燬。這裡需要解釋一下pthread_cond_timedwait和pthread_cond_signal函式
pthread_cond_timedwait
這個函式呼叫之後會阻塞,也就是類似sleep的作用,但是它會在兩種情況下被喚醒:1、條件變數cond被觸發時;2、系統時間到達abstime時,注意這裡是絕對時間,不是相對時間。它比sleep的高明之處就在第一點。另外它還有一個引數是mutex,當執行這個函式時,它的效果等同於在函式入口處先對mutex加鎖,在出口處再對mutex解鎖,當有多執行緒呼叫這個函式時,可以按這種方式去理解
pthread_cond_signal
它只有一個引數cond,作用很簡單,就是觸發等待cond的執行緒,注意,它一次只會觸發一個,如果要觸發所有等待cond的縣城,需要用到pthread_cond_broadcast函式,引數和用法都是一樣的
有了以上背景知識,就可以更加優雅的實現sleep,主要關注Proc函式和Stop函式,完整的程式碼如下
/* filename: test3.cpp */
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <sys/time.h>
class TestServer
{
public:
TestServer() : run_(true)
{
pthread_mutex_init(&mutex_, NULL);
pthread_cond_init(&cond_, NULL);
};
~TestServer()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
};
void Start()
{
pthread_create(&thread_, NULL, ThreadProc, (void*)this);
}
void Stop()
{
run_ = false;
pthread_mutex_lock(&mutex_);
pthread_cond_signal(&cond_);
pthread_mutex_unlock(&mutex_);
}
void Wait()
{
pthread_join(thread_, NULL);
}
void Proc()
{
pthread_mutex_lock(&mutex_);
struct timeval now;
int count = 0;
while (run_)
{
printf("sleep count:%d\n", ++count);
gettimeofday(&now, NULL);
struct timespec outtime;
outtime.tv_sec = now.tv_sec + 5;
outtime.tv_nsec = now.tv_usec * 1000;
pthread_cond_timedwait(&cond_, &mutex_, &outtime);
}
pthread_mutex_unlock(&mutex_);
}
private:
bool run_;
pthread_t thread_;
pthread_mutex_t mutex_;
pthread_cond_t cond_;
static void* ThreadProc(void* arg)
{
TestServer* me = static_cast<TestServer*>(arg);
me->Proc();
return NULL;
}
};
TestServer g_server;
void StopService()
{
g_server.Stop();
}
void StartService()
{
g_server.Start();
g_server.Wait();
}
void SignalHandler(int sig)
{
switch(sig)
{
case SIGINT:
StopService();
default:
break;
}
}
int main(int argc, char* argv[])
{
signal(SIGINT, SignalHandler);
StartService();
return 0;
}
和test2.cpp一樣,編譯之後執行,程式每隔5秒在螢幕列印一行輸出,輸入ctr+c,程式會立馬退出
本文為作者原創,轉載請註明出處,多謝!