天天看點

作業系統核心原理-4.線程原理(上):線程基礎與線程同步

作業系統核心原理-4.線程原理(上):線程基礎與線程同步

我們都知道,程序是運轉中的程式,是為了在CPU上實作多道程式設計而發明的一個概念。但是程序在一個時間隻能幹一件事情,如果想要同時幹兩件或者多件事情,例如同時看兩場電影,我們自然會想到傳說中的分身術,就像孫悟空那樣可以變出多個真身。雖然我們再現實中無法分身,但程序卻可以辦到,辦法就是線程。線程就是我們為了讓一個程序能夠同時幹多件事情而發明的“分身術”。

  我們都知道,程序是運轉中的程式,是為了在CPU上實作多道程式設計而發明的一個概念。但是程序在一個時間隻能幹一件事情,如果想要同時幹兩件或者多件事情,例如同時看兩場電影,我們自然會想到傳說中的分身術,就像孫悟空那樣可以變出多個真身。雖然我們在現實中無法分身,但程序卻可以辦到,辦法就是線程。線程就是我們為了讓一個程序能夠同時幹多件事情而發明的“分身術”。

一、線程基礎

1.1 線程概念

  線程是程序的“分身”,是程序裡的一個執行上下文或執行序列。of course,一個程序可以同時擁有多個執行序列。這就像舞台,舞台上可以有多個演員同時出場,而這些演員和舞台就構成了一出戲。類比程序和線程,每個演員是一個線程,舞台是位址空間,這樣同一個位址空間中的所有線程就構成了程序。

作業系統核心原理-4.線程原理(上):線程基礎與線程同步

  線上程模式下,一個程序至少有一個線程,也可以有多個線程,如下圖所示:

作業系統核心原理-4.線程原理(上):線程基礎與線程同步

  将程序分解為線程可以有效地利用多處理器和多核計算機。例如,當我們使用Microsoft Word時,實際上是打開了多個線程。這些線程一個負責顯示,一個負責接收輸入,一個定時進行存盤......這些線程一起運轉,讓我們感覺到輸入和顯示同時發生,而不用鍵入一些字元等待一會兒才顯示到螢幕上。在不經意間,Word還能定期自動儲存。

1.2 線程管理

  線程管理與程序管理類似,需要一定的基礎:維持線程的各種資訊,這些資訊包含了線程的各種關鍵資料。于是,就有了線程控制塊。

  由于線程間共享一個程序空間,是以,許多資源是共享的(這部分資源不需要存放線上程控制塊中)。但又因為線程是不同的執行序列,總會有些不能共享的資源。一般情況下,統一程序内的線程間共享和獨享資源的劃分如下表所示:

作業系統核心原理-4.線程原理(上):線程基礎與線程同步

1.3 線程模型

  現代作業系統結合了使用者态和核心态的線程模型,其中使用者态的執行系統負責程序内部在非阻塞時的切換,而核心态的作業系統則負責阻塞線程的切換,即同時實作核心态和使用者态線程管理。其中,核心态線程數量極少,而使用者态線程數量較多。每個核心态線程可以服務一個或多個使用者态線程。換句話說,使用者态線程會被多路複用到核心态線程上。

1.4 多線程的關系

  推出線程模型的目的就是實作程序級并發,因為在一個程序中通常會出現多個線程。多個線程共享一個舞台,時而互動,時而獨舞。但是,共享一個舞台會帶來不必要的麻煩,這些麻煩歸結到下面兩個根本問題:

  (1)線程之間如何通信?

  (2)線程之間如何同步?

  上述兩個問題在程序層面同樣存在,在前面的程序原理部分已經進行了介紹,從一個更高的層次上看,不同的程序也共享着一個巨大的空間,這個空間就是整個計算機。

二、線程同步

2.1 同步的原因和目的

  (1)原因

  線程之間的關系是合作關系,既然是合作,那麼久得有某種約定的規則,否則合作就會出問題。例如下圖中,一個程序的兩個線程因為操作不同步而造成線程1運作錯誤:

作業系統核心原理-4.線程原理(上):線程基礎與線程同步

  出現上述問題原因在于兩點:一是線程之間共享的全局變量;二是線程之間的相對執行順序是不确定的。針對第一點,如果所有資源都不共享,那就違背了程序和線程設計的初衷:資源共享、提高資源使用率。針對第二點,需要讓線程之間的相對執行順序在需要的時候可以确定。

  (2)目的

  線程同步的目的就在于不管線程之間的執行如何穿插,其運作結果都是正确的。換句話說,就是要保證多線程執行下結果的确定性。與此同時,也要保持對線程執行的限制越少越少。

2.2 同步的方式

  (1)一些必要概念

  ① 兩個或多個線程争相執行同一段代碼或通路同一資源的現象稱為競争。

  ② 可能造成競争的共享代碼段或資源就被稱為臨界區。

  ③ 在任何時刻都能有一個線程在臨界區中的現象被稱為互斥。(一次隻有一個人使用共享資源,其他人皆排除在外)

  (2)鎖

作業系統核心原理-4.線程原理(上):線程基礎與線程同步

  ① 關于鎖

  當兩個教師都想使用同一個教室來為學生補課,如何協調呢?進到教室後将門鎖上,另外一個教室就無法進來使用教室了。即教室是用鎖來保證互斥的,那麼在作業系統中,這種可以保證互斥的同步機制就被稱為鎖。

  例如,在.NET中可以直接使用lock語句來實作線程同步:

private object locker = new object();
    public void Work()
    {
          lock (locker)
          {
              // 做一些需要線程同步的工作
          }
     }      

  鎖有兩個基本操作:閉鎖和開鎖。很容易了解,閉鎖就是将鎖鎖上,其他人進不來;開鎖就是你做的事情做完了,将鎖打開,别的人可以進去了。開鎖隻有一個步驟那就是打開鎖,而閉鎖有兩個步驟:一是等待鎖達到打開狀态,二是獲得鎖并鎖上。顯然,閉鎖的兩個操作應該是原子操作,不能分開。

  ② 睡覺與叫醒

  當對方持有鎖時,你就不需要等待鎖變為打開狀态,而是去睡覺,鎖打開後對方再來把你叫醒,這是一種典型的生産者消費者模式。用計算機來模拟生産者消費者并不難:一個程序代表生産者,一個程序代表消費者,一片記憶體緩沖區代表商店。生産者将生産的物品從一端放入緩沖區,消費者則從另外一端擷取物品,如下圖所示:

作業系統核心原理-4.線程原理(上):線程基礎與線程同步

  例如,在.NET中可以通過Monitor.Wait()與Monitor.Pulse()來進行睡覺和叫醒操作:

  首先是消費者線程

public void ConsumerDo()
{
    while (true)
    {
        lock(sync)
        {
            // Step1:做一些消費的事情
            ......
            // Step2:喚醒生産者線程
            Monitor.Pulse(sync);
            // Step3:釋放鎖并阻止消費者線程
            Monitor.Wait(sync);
        }
    }
}      

   其次是生産者線程

public void ProducerDo()
{
    while (true)
    {
        lock(sync)
        {
            // Step1:做一些生産操作
            ......
            // Step2:喚醒消費者線程
            Monitor.Pulse(Dog.lockCommunicate);
            // Step3:釋放鎖并阻止生産者線程
            Monitor.Wait(Dog.lockCommunicate);
        }
    }
}      

   但是,在此種情形下,生産者和消費者都有可能進入睡覺狀态,進而無法互相叫醒對方而繼續往前推進,也就發生了系統死鎖。如何解決?我們可以用某種方法将發出的信号累積起來,而不是丢掉。即消費者獲得CPU執行sleep語句後,生産者在這之前發送的叫醒信号還保留,是以消費者将馬上獲得這個信号而醒過來。而能夠将信号累積起來的作業系統原語就是信号量。

  (2)信号量

  信号量(Semaphore)是一個計數器,其取值為目前累積的信号數量。它支援兩個操作:加法操作up和減法操作down。執行down減法操作時,請求該信号量的一個線程會被挂起;而執行up加法操作時,會叫醒一個在該信号量上面等待的線程。down和up操作在曆史上被稱為P和V操作,是作業系統中最重要的同步原語的兩個基本操作。

  有些房間,可以同時容納n個人,比如廚房。也就是說,如果人數大于n,多出來的人隻能在外面等着。這好比某些記憶體區域,隻能供給固定數目的線程使用。這時的解決方法,就是在門口挂n把鑰匙。

作業系統核心原理-4.線程原理(上):線程基礎與線程同步

  進去的人就取一把鑰匙,出來時再把鑰匙挂回原處。後到的人發現鑰匙架空了,就知道必須在門口排隊等着了。這種做法就叫做"信号量",用來保證多個線程不會互相沖突。

  例如,在.NET中提供了一個Semaphore類來進行信号量操作,下面的示例代碼示範了4條線程想要同時執行ThreadEntry()方法,但同時隻允許2條線程進入:

class Program
    {
        // 第一個參數指定目前有多少個“空位”(允許多少條線程進入)
        // 第二個參數指定一共有多少個“座位”(最多允許多少個線程同時進入)
        static Semaphore sem = new Semaphore(2, 2);

        const int threadSize = 4;

        static void Main(string[] args)
        {
            for (int i = 0; i < threadSize; i++)
            {
                Thread thread = new Thread(ThreadEntry);
                thread.Start(i + 1);
            }

            Console.ReadKey();
        }

        static void ThreadEntry(object id)
        {
            Console.WriteLine("線程{0}申請進入本方法", id);
            // WaitOne:如果還有“空位”,則占位,如果沒有空位,則等待;
            sem.WaitOne();
            Console.WriteLine("線程{0}成功進入本方法", id);
            // 模拟線程執行了一些操作
            Thread.Sleep(100);
            Console.WriteLine("線程{0}執行完畢離開了", id);
            // Release:釋放一個“空位”
            sem.Release();
        }
    }      

  如果将資源比作“座位”,Semaphore接收的兩個參數中:第一個參數指定目前有多少個“空位”(允許多少條線程進入),第二個參數則指定一共有多少個“座位”(最多允許多少個線程同時進入)。WaitOne()方法則表示如果還有“空位”,則占位,如果沒有空位,則等待;Release()方法則表示釋放一個“空位”。

  不難看出,mutex互斥鎖是semaphore信号量的一種特殊情況(n=1時)。也就是說,完全可以用後者替代前者。

  但是,如果生産者或消費者将兩個up/down操作順序颠倒,也同樣會産生死鎖。也就是說,使用信号量原語時,信号量操作的順序至關重要。那麼,有木有辦法改變這種情況,可不可将信号量的這些組織工作交給一個專門的構造來負責,解放廣大程式員?答案是管程。

  (3)管程

  管程(Monitor)即螢幕的意思,它監視的就是程序或線程的同步操作。具體來說,管程就是一組子程式、變量和資料結構的組合。言下之意,把需要同步的代碼用一個管程的構造框起來,即将需要保護的代碼置于begin monitor和end monitor之間,即可獲得同步保護,也就是任何時候隻能有一個線程活躍在管程裡面。

  同步操作的保證是由編譯器來執行的,編譯器在看到begin monitor和end monitor時就知道其中的代碼需要同步保護,在翻譯成低級代碼時就會将需要的作業系統原語加上,使得兩個線程不能同時活躍在同一個管程内。

  例如,在.NET中提供了一個Monitor類,它可以幫我們實作互斥的效果:

private object locker = new object();
    public void Work()
    {
        // 避免直接使用私有成員locker(直接使用有可能會導緻線程不安全)
        object temp = locker;
        Monitor.Enter(temp);
        try
        {
            // 做一些需要線程同步的工作
        }
        finally
        {
            Monitor.Exit(temp);
        }
    }      

  在管程中使用兩種同步機制:鎖用來進行互斥,條件變量用來控制執行順序。從某種意義上來說,管程就是鎖+條件變量。

About:條件變量就是線程可以在上面等待的東西,而另外一個線程則可以通過發送信号将在條件變量上的線程叫醒。是以,條件變量有點像信号量,但又不是信号量,因為不能對其進行up和down操作。

  管程最大的問題就是對編譯器的依賴,因為我們需要将編譯器需要的同步原語加在管程的開始和結尾。此外,管程隻能在單台計算機上發揮作用,如果想在多計算機環境下進行同步,那就需要其他機制了,而這種其他機制就是消息傳遞。

  (4)消息傳遞

  消息傳遞是通過同步雙方經過互相收發消息來實作,它有兩個基本操作:發送send和接收receive。他們均是作業系統的系統調用,而且既可以是阻塞調用,也可以是非阻塞調用。而同步需要的是阻塞調用,即如果一個線程執行receive操作,就必須等待受到消息後才能傳回。也就是說,如果調用receive,則該線程将挂起,在收到消息後,才能轉入就緒。

  消息傳遞最大的問題就是消息丢失和身份識别。由于網絡的不可靠性,消息在網絡間傳輸時丢失的可能性較大。而身份識别是指如何确定收到的消息就是從目标源發出的。其中,消息丢失可以通過使用TCP協定減少丢失,但也不是100%可靠。身份識别問題則可以使用諸如數字簽名和加密技術來彌補。

  (5)栅欄

  栅欄顧名思義就是一個障礙,到達栅欄的線程必須停止下來,知道出去栅欄後才能往前推進。該院與主要用來對一組線程進行協調,因為有時候一組線程協同完成一個問題,是以需要所有線程都到同一個地方彙合之後一起再向前推進。

  例如,在并行計算時就會遇到這種需求,如下圖所示:

作業系統核心原理-4.線程原理(上):線程基礎與線程同步

參考資料

作業系統核心原理-4.線程原理(上):線程基礎與線程同步

鄒恒明,《作業系統之哲學原理》,機械工業出版社

作者:周旭龍

出處:http://edisonchou.cnblogs.com

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連結。

繼續閱讀