1. 程式人生 > >linux基礎——經典執行緒同步問題解析及程式設計實現

linux基礎——經典執行緒同步問題解析及程式設計實現

前兩天寫了個簡單的執行緒池,結果在處理執行緒的同步互斥上花了不少時間,覺得有必要把以前學習的知識再過一遍,這次主要複習的是幾個非常經典的同步互斥問題。

一、生產者消費者問題 問題描述: 只有緩衝區沒滿時,生產者才能把訊息放入到緩衝區,否則必須等待;只有緩衝區不空時,消費者才能從中取出訊息,否則必須等待。由於緩衝區是臨界資源,它只允許一個生產者放入訊息,或者一個消費者從中取出訊息。 問題分析 1、關係分析:生產者和消費者對緩衝區互斥訪問是互斥關係,同時生產者和消費者又是一個相互協作的關係,只有生產者生產之後,消費者才能消費,它們也是同步關係。 2、整理思路:因為只有生產者和消費者兩個執行緒,且這兩個執行緒存在著互斥關係和同步關係。那麼需要解決的是互斥和同步的PV操作的位置。 3、訊號量設定:互斥鎖mutex用於控制互斥訪問緩衝池;訊號量full用於記錄當前緩衝池中“滿”緩衝區數,初值為 0;訊號量empty用於記錄當前緩衝池中“空”緩衝區數,初值為n。
#define N 10
sem_t empty;//空閒緩衝區訊號量,初始化為N
sem_t full;//滿緩衝區訊號量,初始化為0
pthread_mutex_t mutex;//對緩衝區產品互斥訪問
int product_num = 0;//緩衝區產品數目

void *producter_f(void *arg)
{
        while (1)
        {
                usleep(1000000);
                printf("produce an item\n");//生產資料
                sem_wait(&empty);//獲取空緩衝區單元
                pthread_mutex_lock(&mutex);//進入臨界區
                product_num++;//將資料放入緩衝區
                printf("producter: the num of product is %d\n", product_num);
                pthread_mutex_unlock(&mutex);//離開臨界區
                sem_post(&full);//滿緩衝區加1
        }
}

void *consumer_f(void *arg)
{
        while (1)
        {
                sem_wait(&full);//獲取空緩衝區單元
                pthread_mutex_lock(&mutex);//進入臨界區
                product_num--;
                printf("consumer: the num of product is %d\n", product_num);
                pthread_mutex_unlock(&mutex);//離開臨界區
                sem_post(&empty);//空緩衝區加1
                usleep(5000000);
                printf("consume an item\n");//消費資料
        }
}
執行結果:
該類問題要注意對緩衝區大小為n的處理,當緩衝區中有空時便可對empty變數執行P 操作,一旦取走一個產品便要執行V操作以釋放空閒區。對empty和full變數的P操作必須放在對mutex的P操作之前。如果生產者執行緒先執行P(mutex),然後執行P(empty),消費者執行P(mutex),然後執行P(fall),這樣可不可以?答案是否定的。設想生產者執行緒已經將緩衝區放滿,消費者執行緒並沒有取產品,即empty = 0,當下次仍然是生產者執行緒執行時,它先執行P(mutex)封鎖訊號量,再執行P(empty)時將被阻塞,希望消費者取出產品後將其喚醒。輪到消費者執行緒執行時,它先執行P(mutex),然而由於生產者執行緒已經封鎖mutex訊號量,消費者執行緒也會被阻塞,這樣一來生產者、消費者執行緒都將阻塞,都指望對方喚醒自己,陷入了無休止的等待。同理,如果消費者執行緒已經將緩衝區取空,即 full = 0,下次如果還是消費者先執行,也會出現類似的死鎖。不過生產者釋放訊號量時,mutex、full先釋放哪一個無所謂,消費者先釋放mutex還是empty都可以。 二、讀者-寫者問題
問題描述: 有讀者和寫者兩組併發執行緒,共享一個檔案,當兩個或以上的讀執行緒同時訪問共享資料時不會產生副作用,但若某個寫執行緒和其他執行緒(讀執行緒或寫執行緒)同時訪問共享資料時則可能導致資料不一致的錯誤。因此要求:①允許多個讀者可以同時對檔案執行讀操作;②只允許一個寫者往檔案中寫資訊;③任一寫者在完成寫操作之前不允許其他讀者或寫者工作;④寫者執行寫操作前,應讓已有的讀者和寫者全部退出。 問題分析: 1) 關係分析。由題目分析讀者和寫者是互斥的,寫者和寫者也是互斥的,而讀者和讀者不存在互斥問題。 2) 整理思路。兩個執行緒,即讀者和寫者。寫者是比較簡單的,它和任何執行緒互斥,用互斥訊號量的P操作、V操作即可解決。讀者的問題比較複雜,它必須實現與寫者互斥的同時還要實現與其他讀者的同步,因此,僅僅簡單的一對P操作、V操作是無法解決的。那麼,在這裡用到了一個計數器,用它來判斷當前是否有讀者讀檔案。當有讀者的時候寫者是無法寫檔案的,此時讀者會一直佔用檔案,當沒有讀者的時候寫者才可以寫檔案。同時這裡不同讀者對計數器的訪問也應該是互斥的。 3) 訊號量設定。首先設定訊號量count為計數器,用來記錄當前讀者數量,初值為0; 設定mutex為互斥鎖,用於保護更新count變數時的互斥;設定互斥訊號量rw用於保證讀者和寫者的互斥訪問。
sem_t rw;//保護讀者和寫者互斥地訪問檔案,初始化為1
pthread_mutex_t mutex;//保護更新reader_num時的互斥
int reader_num = 0;//用於記錄讀者數量

void *writer_f(void *arg)
{
        sem_wait(&rw);//互斥訪問共享檔案
        usleep(2000000);
        printf("writing ...\n");
        sem_post(&rw); //釋放共享檔案
}

void *reader_f(void *arg)
{
        pthread_mutex_lock(&mutex);//互斥訪問reader_num變數

        if (reader_num == 0)
        {
            sem_wait(&rw);//阻止寫執行緒寫
        }
        reader_num++;//讀者計數器加1
        printf("reader num add the num is %d \n", reader_num);
        pthread_mutex_unlock(&mutex);//釋放互斥變數


        usleep(2000000);
        printf("reading ...\n");

        pthread_mutex_lock(&mutex);//互斥訪問reader_num變數

        reader_num--;//讀者計數器減1
        printf("reader num dec the num is %d \n", reader_num);

        if (reader_num == 0)
        {
                sem_post(&rw);//允許寫執行緒寫
        }

        pthread_mutex_unlock(&mutex);//釋放互斥變數
}
執行結果:
在上面的演算法中,讀執行緒是優先的,也就是說,當存在讀執行緒時,寫操作將被延遲,並且只要有一個讀執行緒活躍,隨後而來的讀執行緒都將被允許訪問檔案。這樣的方式下,會導致寫執行緒可能長時間等待,且存在寫執行緒“餓死”的情況。 如果希望寫執行緒優先,即當有讀執行緒正在讀共享檔案時,有寫執行緒請求訪問,這時應禁止後續讀執行緒的請求,等待到已在共享檔案的讀執行緒執行完畢則立即讓寫執行緒執行,只有在無寫執行緒執行的情況下才允許讀執行緒再次執行。為此,增加一個訊號量並且在上面的程式中 writer_f()和reader_f()函式中各增加一對PV操作,就可以得到寫執行緒優先的解決程式。
sem_t rw;//保護讀者和寫者互斥地訪問檔案,初始化為1
sem_t w;//用於實現寫優先,初始化為1
pthread_mutex_t mutex;//保護更新reader_num時的互斥
int reader_num = 0;//用於記錄讀者數量

void *writer_f(void *arg)
{
        sem_wait(&w);
        sem_wait(&rw);//互斥訪問共享檔案
        usleep(2000000);
        printf("writing ...\n");
        sem_post(&rw); //釋放共享檔案
        sem_post(&w);
}

void *reader_f(void *arg)
{
        sem_wait(&w);
        pthread_mutex_lock(&mutex);//互斥訪問reader_num變數

        if (reader_num == 0)
        {
            sem_wait(&rw);//阻止寫執行緒寫
        }
        reader_num++;//讀者計數器加1
        printf("reader num add the num is %d \n", reader_num);
        pthread_mutex_unlock(&mutex);//釋放互斥變數
        sem_post(&w);

        usleep(2000000);
        printf("reading ...\n");

        pthread_mutex_lock(&mutex);//互斥訪問reader_num變數

        reader_num--;//讀者計數器減1
        printf("reader num dec the num is %d \n", reader_num);

        if (reader_num == 0)
        {
                sem_post(&rw);//允許寫執行緒寫
        }

        pthread_mutex_unlock(&mutex);//釋放互斥變數
}

三、哲學家進餐問題 問題描述: 一張圓桌上坐著5名哲學家,每兩個哲學家之間的桌上擺一根筷子,桌子的中間是一碗米飯,如圖2-10所示。哲學家們傾注畢生精力用於思考和進餐,哲學家在思考時,並不影響他人。只有當哲學家飢餓的時候,才試圖拿起左、 右兩根筷子(一根一根地拿起)。如果筷子已在他人手上,則需等待。飢餓的哲學家只有同時拿到了兩根筷子才可以開始進餐,當進餐完畢後,放下筷子繼續思考。 問題分析: 1) 關係分析。5名哲學家與左右鄰居對其中間筷子的訪問是互斥關係。 2) 整理思路。顯然這裡有五個執行緒。本題的關鍵是如何讓一個哲學家拿到左右兩個筷子而不造成死鎖或者飢餓現象。那麼解決方法有兩個,一個是讓他們同時拿兩個筷子;二是對每個哲學家的動作制定規則,避免飢餓或者死鎖現象的發生。 3) 訊號量設定。定義互斥訊號量陣列chopstick[5] = {l, 1, 1, 1, 1}用於對5個筷子的互斥訪問。 對哲學家按順序從0~4編號,哲學家i左邊的筷子的編號為i,哲學家右邊的筷子的編號為(i+l)%5。 該演算法存在以下問題:當五個哲學家都想要進餐,分別拿起他們左邊筷子的時候(都恰好執行完wait(chopstick[i]);)筷子已經被拿光了,等到他們再想拿右邊的筷子的時候(執行 wait(chopstick[(i+l)%5]);)就全被阻塞了,這就出現了死鎖。
為了防止死鎖的發生,可以對哲學家執行緒施加一些限制條件,比如至多允許四個哲學家同時進餐;僅當一個哲學家左右兩邊的筷子都可用時才允許他抓起筷子;對哲學家順序編號,要求奇數號哲學家先抓左邊的筷子,然後再轉他右邊的筷子,而偶數號哲學家剛好相反。正解制定規則如下:假設釆用第二種方法,當一個哲學家左右兩邊的筷子都可用時,才允許他抓起筷子。
sem_t chopstick[N];//定義訊號量陣列,初始化為1
pthread_mutex_t mutex;//保護取筷子時的互斥
int reader_num = 0;//用於記錄讀者數量

void *philosopher_i(void *arg)
{
        int index = *(int *)arg;

        pthread_mutex_lock(&mutex);//在取筷子前獲得互斥量
        sem_wait(&chopstick[index]);//取左邊筷子
        sem_wait(&chopstick[(index+1) % 5]);//取右邊筷子
        printf("get two chopstick the index is %d\n", index);
        pthread_mutex_unlock(&mutex);//釋放取筷子的互斥量
        usleep(2000000);
        printf("eating... the index is %d\n", index);//進餐

        sem_post(&chopstick[index]);//放回左邊筷子
        sem_post(&chopstick[(index+1) % 5]);//放回右邊筷子
        usleep(2000000);        
        printf("thinking... the index is %d\n", index);//思考
}
執行結果:
四、吸菸者問題 問題描述: 假設一個系統有三個抽菸者執行緒和一個供應者執行緒。每個抽菸者不停地捲菸 並抽掉它,但是要捲起並抽掉一支菸,抽菸者需要有三種材料:菸草、紙和膠水。三個抽菸 者中,第一個擁有菸草、第二個擁有紙,第三個擁有膠水。供應者執行緒無限地提供三種材料, 供應者每次將兩種材料放到桌子上,擁有剩下那種材料的抽菸者卷一根菸並抽掉它,並給供 應者一個訊號告訴完成了,供應者就會放另外兩種材料在桌上,這種過程一直重複(讓三個 抽菸者輪流地抽菸)。 問題分析: 1) 關係分析。供應者與三個抽菸者分別是同步關係。由於供應者無法同時滿足兩個或 以上的抽菸者,三個抽菸者對抽菸這個動作互斥(或由三個抽菸者輪流抽菸得知 2) 整理思路。顯然這裡有四個執行緒。供應者作為生產者向三個抽菸者提供材料。 3) 訊號量設定。訊號量offer1、offer2、offer3分別表示菸草和紙組合的資源、菸草和 膠水組合的資源、紙和膠水組合的資源。訊號量finish用於互斥進行抽菸動作。
sem_t offer1;//定義訊號量對應菸草和紙組合的資源,初始化為1
sem_t offer2;//定義訊號量對應菸草和膠水組合的資源,初始化為1
sem_t offer3;//定義訊號量對應紙和膠水組合的資源,初始化為1
sem_t finish;//定義訊號量表示吸菸者是否完成,初始化為1

void *provider_f(void *arg)
{
        while (1)
        {
                int rand_num = rand() % 3;
                if (rand_num == 0)
                {
                        sem_post(&offer1);//提供菸草和紙
                }
                else if (rand_num == 1)
                {
                        sem_post(&offer2);//提供菸草和膠水
                }
                else
                {
                        sem_post(&offer3);//提供紙和膠水
                }

                usleep(1000000);
                printf("put two material on table\n");

                sem_wait(&finish);
        }
}

void *smoker1_f(void *arg)
{
        while (1)
        {
                sem_wait(&offer1);
                usleep(1000000);
                printf("remove the paper and glue\n");//拿走紙和膠水
                sem_post(&finish);
        }   
}

void *smoker2_f(void *arg)
{
        while (1)
        {
                sem_wait(&offer2);
                usleep(1000000);
                printf("remove the tobacco and glue\n");//拿走菸草和膠水
                sem_post(&finish);
        }   
}

void *smoker3_f(void *arg)
{
        while (1)
        {
                sem_wait(&offer3);
                usleep(1000000);
                printf("remove the paper and tobacco\n");//拿走紙和菸草
                sem_post(&finish);
        }   
}
執行結果:

相關推薦

linux基礎——經典執行同步問題解析程式設計實現

前兩天寫了個簡單的執行緒池,結果在處理執行緒的同步互斥上花了不少時間,覺得有必要把以前學習的知識再過一遍,這次主要複習的是幾個非常經典的同步互斥問題。 一、生產者消費者問題 問題描述: 只有緩衝

C++ 經典執行同步 事件Event示例解析(十)

下面給個多執行緒案例使用event #define _CRT_SECURE_NO_WARNINGS #include <windows.h> #include <process.h> #include <stdio.h> struc

秒殺多執行第八篇 經典執行同步 訊號量Semaphore

前面介紹了關鍵段CS、事件Event、互斥量Mutex在經典執行緒同步問題中的使用。本篇介紹用訊號量Semaphore來解決這個問題。 首先也來看看如何使用訊號量,訊號量Semaphore常用有三個函式,使用很方便。下面是這幾個函式的原型和使用說明。 第一個 Create

秒殺多執行第七篇 經典執行同步 互斥量Mutex

閱讀本篇之前推薦閱讀以下姊妹篇: 《秒殺多執行緒第四篇一個經典的多執行緒同步問題》 《秒殺多執行緒第五篇經典執行緒同步關鍵段CS》 《秒殺多執行緒第六篇經典執行緒同步事件Event》   前面介紹了關鍵段CS、事件Event在經典執行緒同步問題中的使用。本篇介紹用互斥量Mu

Linux 學習筆記—執行同步之互斥量與條件變數

執行緒同步(同步的意思是協同步調) 執行緒同步機制包括互斥,讀寫鎖以及條件變數等 3.2.1 互斥量(互斥鎖) **互斥量本質是一把鎖,在訪問公共資源前對互斥量設定(加鎖),確保同一時間只有一個執行緒訪問資料,在訪問完成後再釋放(解鎖)互斥量。**在互斥量加鎖之

Linux 學習筆記—執行同步之讀寫鎖、自旋鎖、屏障

3.2.1 讀寫鎖 讀寫鎖和互斥體類似,不過讀寫鎖有更高的並行性,互斥體要麼是鎖住狀態,要麼是不加鎖狀態,而且一次只有一個執行緒可以對其加鎖。而讀寫鎖可以有3個狀態,讀模式下鎖住狀態,寫模式下鎖住狀態,不加鎖狀態。一次只有一個執行緒可以佔有寫模式的讀寫鎖,但是多

秒殺多執行第九篇 經典執行同步總結 關鍵段 事件 互斥量 訊號量

                前面《秒殺多執行緒第四篇一個經典的多執行緒同步問題》提出了一個經典的多執行緒同步互斥問題,這個問題包括了主執行緒與子執行緒的同步,子執行緒間的互斥,是一道非常經典的多執行緒同步互斥問題範例,後面分別用了四篇來詳細介紹常用的執行緒同步互斥機制——關鍵段、事件、互斥量、訊號量。下面

秒殺多執行第五篇 經典執行同步 關鍵段CS

                上一篇《秒殺多執行緒第四篇 一個經典的多執行緒同步問題》提出了一個經典的多執行緒同步互斥問題,本篇將用關鍵段CRITICAL_SECTION來嘗試解決這個問題。本文首先介紹下如何使用關鍵段,然後再深層次的分析下關鍵段的實現機制與原理。關鍵段CRITICAL_SECTION一共就

Java中的執行Thread解析用途

Java中的執行緒 程序和執行緒 在併發性程式中,有兩個基本的執行單元:程序和執行緒。在Java程式語言中,併發程式設計大多數情況下都是和執行緒相關。然而,程序也是很重要的。 一個計算機系統中通常都有很多活動的程序和執行緒。這一點即使是在只有一個執行核心,並且在給

Java併發核心基礎——執行池使用底層實現機制詳解

Java執行緒池概述: 從使用入手: java.util.concurrent.Executosr是執行緒池的靜態工廠,我們通常使用它方便地生產各種型別的執行緒池,主要的方法有三種: 1、newS

Linux最大執行數限制當前執行數查詢

1、總結系統限制有:     /proc/sys/kernel/pid_max #查系統支援的最大執行緒數,一般會很大,相當於理論值     /proc/sys/kernel/thread-max     max_user_process(ulimit -u) #系統限制某

作業系統核心原理-4.執行原理(上):執行基礎執行同步

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

秒殺多執行第五篇---經典執行同步 關鍵段(臨界區)CS

上一篇《秒殺多執行緒第四篇 一個經典的多執行緒同步問題》提出了一個經典的多執行緒同步互斥問題,本篇將用關鍵段CRITICAL_SECTION來嘗試解決這個問題。 本文首先介紹下如何使用關鍵段,然後再深層次的分析下關鍵段的實現機制與原理。 關鍵段CRITICA

Linux基礎執行概述

在linux系統中,當程序進行切換等操作時需要負責的上下文切換等動作,而因每一個程序都擁有自已的資料段,程式碼段和堆疊段,從造成程序的切換造成很大的花銷。為了減少處理機的空轉時間,支援多處理器和減少上下文切換開銷,這樣出現了一個新概念—執行緒。執行緒是一個程序內的基本排程單位,也可以稱為輕量級程序,一個程序內

訊號量解決經典執行同步問題

       訊號量 是E. W.Dijkstra在l965年提出的一種方法,它使用一個整型變數來累計喚醒次數,以供以後使用。在他的建議中引入一個新的變號型別,稱作訊號量(semapore )。一個訊號量的值可以為0,表示沒有積累下來的喚醒操作;或者為正值,表示有一個或多個

【ARM&Linux】常用執行同步的三種方法

【執行緒同步高效率程式設計】 Linux系統中執行緒最大的特點就是共享性,執行緒同步問題較為困難也很重要,最常用的三種是:條件變數、互斥鎖、無名訊號量。(ps: 有名訊號量可用於程序同步,無名訊

linux下多執行同步機制之訊號量、互斥量、讀寫鎖、條件變數

之前有寫過類似的部落格,這東西不用老忘,現在又有更清晰的理解了。 一、訊號量 編譯時候加入-lrt 訊號量最基本的兩個操作就是PV操作:P()操作實現訊號量減少,V()操作實現訊號量的增加 訊號量的值取決於訊號量的型別,訊號量的型別有多種: (1)二進位制訊號量:0與1.

Linux基礎執行基本操作

在linux中,建立執行緒所用的函式是pthread_create.而建立執行緒實際上就是確定呼叫該執行緒函式的入口點。執行緒退出有兩種方法:一種是線上程被建立後,就開始執行相關的執行緒函式,在該函式執行完之後,該執行緒也就退出了;另一種是使用函式pthread_exit主動退出。在這裡應注意到,執行緒退出使

執行同步的三種實現方式

java執行緒的同步問題可以通過三種方式實現: 首先建立四個執行緒: public class Test01 { public static void main(String[] args) {

執行-執行同步有幾種實現方式

執行緒同步有幾種實現方式 1. Synchronized 在方法級別  public synchronized …. 在程式碼塊   synchronized(物件){} 1. 當synchronized作用在方法上的時候,鎖住的就是這個物件的例項 synchronized