1. 程式人生 > >讓你的程式更優雅的sleep

讓你的程式更優雅的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,程式會立馬退出

本文為作者原創,轉載請註明出處,多謝!