官术网_书友最值得收藏!

  • 編程之美
  • 《編程之美》小組
  • 2151字
  • 2019-01-10 16:04:00

1.10 雙線程高效下載

我們經常需要編寫程序,從網絡上下載數據,然后存儲到硬盤上。一個簡單的做法,就是下載一塊數據,寫入硬盤,然后再下載,再寫入硬盤……不斷重復這個過程,直到所有的內容下載完畢為止。能否對此進行優化?

我們不妨對問題做一些抽象和簡化。

1.假設所有數據塊的大小都是固定的。你可以使用一個全局緩存區:

Block g_buffer[BUFFER_COUNT]

2.假設兩個基本函數已經實現(你可以假定兩個函數都能正常工作,不會拋出異常):

    //downloads a block from Internet sequentially in each call
    // return true, if the entire file is downloaded, otherwise false.
    bool GetBlockFromNet(Block * out_block);

    //writes a block to hard disk
    bool WriteBlockToDisk(Block * in_block);

上述的想法可以用代碼清單1-15中的偽代碼實現。

代碼清單1-15

    while(true)
    {
        bool isDownloadCompleted;
        isDownloadCompleted=GetBlockFromNet(g_buffer);
        WriteBlockToDisk(g_buffer);
        if(isDownloadCompleted)
            break;
    }

可以看到,在上述方法中,我們要下載完一塊數據之后才能寫入硬盤。下載數據和寫入硬盤的過程是串行的。為了提高效率,我們希望能夠設計兩個線程,使得下載和寫硬盤能并行進行。

線程A:從網絡中讀取一個數據塊,存儲到內存的緩存中。

線程B:從緩存中讀取內容,存儲到文件中。

試實現如下子程序:

1.初始化部分

2.線程A

3.線程B

你可以使用下面的多線程API(如代碼清單1-16)。

代碼清單1-16

    class Thread
    {
    public:
        // initialize a thread and set the work function
        Thread(void (*work_func)());
        // once the object is destructed, the thread will be aborted
        ~Thread();
        // start the thread
        void Start();
        // stop the thread
        void Abort();
    };

    class Semaphore
    {
    public:
        // initialize semaphore counts
        Semaphore(int count, int max_count);
        ~Semaphore();
        // consume a signal (count--), block current thread if count==0
        void Unsignal();
        // raise a signal (count++)
        void Signal();
    };

    class Mutex
    {
    public:
        // block thread until other threads release the mutex
        WaitMutex();
        // release mutex to let other thread wait for it
        ReleaseMutex();
    };

如果網絡延遲為L1,磁盤I/O延遲為L2,將此多線程與單線程進行比較,分析這個設計的性能問題,并考慮是否還有其他改進的設計方法?

分析與解法

這道題目出現在2007年微軟校園招聘的筆試中。出題者為了讓不同知識背景的同學都能發揮水平,特地提供了詳細的說明。這樣,沒有接觸過實際的Windows多線程編程的同學也能寫出代碼?,F在越來越多的電腦采用雙核甚至是多核的體系結構,并行計算會成為常用的程序工作模式。這道題只是一個簡單的例子。

在實際工作中,程序員經常要依靠已有的模塊和API完成任務,這些模塊也許只有簡單的接口說明,沒有源代碼。在這種情況下能夠高效地完成任務,也是優秀程序員的特質之一。

我們需要使用兩個線程來完成從網絡上下載數據并存儲到硬盤上的過程。下載線程和存儲線程共享一個全局緩存區,我們需要協調兩個線程的工作。下面若干因素是我們要重點考慮的。

1.什么時候才算完成任務?

兩個線程必須協同工作,將網絡上的數據下載完畢并且完全存儲到硬盤上,只有在這個時候,兩個線程才能正常終止。

2.為了提高效率,希望兩個線程能盡可能地同時工作。

如果使用Mutex,下載和存儲線程將不能同時工作。因此,Semaphore是更好的選擇關于在不同平臺上進行多線程的通訊的詳細技術細節,請參考相應的SDK和API說明。

3.下載和存儲線程工作的必要條件如下。

如果共享緩存區已滿,沒有緩沖空間來存儲下載的內容,則應該暫停下載。如果所有的內容都已經下載完畢,也沒必要繼續下載。

如果緩存區為空,則沒必要運行存儲線程。進一步,如果下載工作已經完成,存儲線程也可以結束了。

4.共享緩存區的數據結構。

下載線程和存儲線程工作的過程是“先進先出”,先下載的內容要先存儲,這樣才能保證內容的正確順序?!跋冗M先出”的典型數據結構是隊列。由于我們采用了固定的緩沖空間來保存下載的內容,循環隊列會是一個很好的選擇。

綜合考慮上面的因素,調用題目提供的API可以得到下面這個可供參考的偽代碼(如代碼清單1-17所示)。

代碼清單1-17

    #define BUFFER_COUNT 100
    Block g_buffer[BUFFER_COUNT];
    Thread g_threadA(ProcA);
    Thread g_threadB(ProcB);
    Semaphore g_seFull(0, BUFFER_COUNT);
    Semaphore g_seEmpty(BUFFER_COUNT, BUFFER_COUNT);
    bool g_downloadComplete;
    int in_index=0;
    int out_index=0;
    void main()
    {
        g_downloadComplete=false;
        threadA.Start();
        threadB.Start();
        // wait here till threads finished
    }
    void ProcA()
    {
        while(true)
        {
            g_seEmpty.Unsignal();
            g_downloadComplete=GetBlockFromNet(g_buffer+in_index);
            in_index=(in_index+1) % BUFFER_COUNT;
            g_seFull.Signal();
            if(g_downloadComplete)
                break;
        }
    }
    void ProcB()
    {
        while(true)
        {
            g_seFull.Unsignal();
            WriteBlockToDisk(g_buffer+out_index);
            out_index=(out_index+1) % BUFFER_COUNT;
            g_seEmpty.Signal();
            if(g_downloadComplete && out_index==in_index)
                break;
        }
    }

上面的偽代碼中,ProcA和ProcB的操作能夠一一對應起來,下載線程和存儲線程可以同時工作,看起來很美。如果網絡延遲為L1,磁盤I/O延遲為L2,那么這個多線程程序執行的時間約為Max(L1, L2)。尤其是需要下載的內容很多的時候,基本可以保證兩個線程是流水線工作。而在單線程的情況下,需要的時間是L1+L2。

如果網絡延遲遠大于I/O存儲延遲,則多個下載線程的設計將可以進一步改善性能。但也將帶來一些更復雜的問題。多個下載線程和存儲線程之間如何協同工作呢?這個問題留給讀者思考。

微軟亞洲研究院的研發主管鄒欣曾經參加過多個版本Microsoft Outlook的開發,他提供了這個題目,并且講了下面的故事。

在Outlook和Exchange服務器連接的情況下,Outlook需要下載一系列的“離線地址簿文件”(OAB, Offline Address Book)以支持Outlook在離線的情況下還能正常地搜索到公司所有E-mail用戶和用戶組的信息。這個文件相當大,對于Microsoft這樣的公司來說,大概有300MB~500MB,原來的算法是單線程的(正如題目所示),運行要花很長時間,用戶抱怨他們不得不盯著這個對話框的進度條緩慢移動,非常不爽。于是我寫了一個雙線程的方案,在經過反復測試,并且得到測試人員的認可后,我把修改正式提交到源代碼庫。幾天后,同事們高興地反映,新的版本的確快多了!但是又有另兩位同事抱怨,這個功能太“狠”了,在筆記本電腦上,Outlook像瘋了一樣地寫硬盤,他們都做不了其他事情!后來幾經討論和試驗,我們做了進一步修改,如果用戶正在使用電腦,那么雙線程會自動放慢速度,如果發現用戶在幾秒鐘內沒有鼠標、鍵盤的輸入,那雙線程會逐漸恢復高速運行。從那以后,我就再也沒有聽到類似的抱怨了。也許提高程序效能(performance)的最高境界,就是把事情做了,同時又不讓用戶感覺到程序在費力地做事情。

最近發布的Windows桌面搜索(Desktop Search)也有類似的“人性化”功能,如果你正在使用電腦,它會提示“Index speed is reduced while you're using the computer”,那么Windows中的哪些API能了解用戶是否在使用鼠標或者鍵盤呢?這個問題留給讀者去探索。

主站蜘蛛池模板: 金溪县| 桂林市| 武义县| 纳雍县| 阿图什市| 拜泉县| 肃宁县| 那坡县| 桂阳县| 南昌县| 尤溪县| 德清县| 台南县| 灯塔市| 池州市| 凌海市| 天祝| 大厂| 奈曼旗| 邻水| 盐山县| 阳信县| 清河县| 麟游县| 四平市| 双峰县| 哈密市| 葫芦岛市| 乐东| 济阳县| 丹江口市| 永嘉县| 北海市| 如东县| 响水县| 邳州市| 缙云县| 九台市| 若尔盖县| 庆城县| 天柱县|