多線程和線程安全

程序結構

程序一旦抽象成一個流程,就能夠用流程圖來表示,只是複雜度不一樣罷了,
常見的程序執行結構有:順序結構和循環結構和選擇結構。linux

peekmessage和dispatchmessage

window是一個事件循環的程序,WinMain方法就是一個死循環,不斷讀取和處理window中的消息。
首先系統有本身的系統消息隊列主要存放來自按鍵和鼠標的消息,每一個應用程序中的線程都有本身的
消息隊列,在多線程的程序中消息隊列的數量與線程數量相等。ios

當一個信號發出後,主線程收到這個消息,而後將它放入事件隊列,也就是說主線程會先peekmessage,
而後再dispatchmessage,相似於一個郵件員將郵件收集起來,而後在分發出去。c++

首先一個GUI程序都應該是一個多線程的程序,否則當前線程阻塞,而後全部其餘的任務都被阻塞。windows

每一個線程在調度機制的影響下,都只有一個時間片的運行時間,因爲是如今的進程是搶佔式進程,每一個線程
都有機會執行。安全

點擊窗口的一個按鈕,按鈕就會發送一個相似信號的東西,主進程捕獲了它,將它放入消息隊列中,主進程每隔
一個時間片就會去peekmessage(查看消息隊列中的事件),而後dispatchmessage(運行這個選中的事件(可
能對應的是一個時間處理函數))。多線程

試想若是在該窗口增長一個"cancel"按鈕,用於取消另外一個"run"按鈕的發出的動做, 那應該怎麼辦? 如今假設系統每隔
5秒會peekmessage,run按鈕執行MyFunc函數會消耗20秒的時間。那麼在這20秒中程序什麼事都幹不了,只能等待執行完成。
解決辦法是在MyFunc這一級別增長用戶模擬的查詢操做,讓MyFunc每隔五秒發送一個peekmessage和dispatchmessage,
這樣就能在合適的時間發現cancel這個事件,從而不須要等待20秒就能迅速取消這個MyFunc處理函數。併發

什麼是進程?

1.所謂進程其實就是程序在運行時的實體,是一個程序的一次運行過程,一個具備獨立運行能力的程序,其實就是一個進程。它是操做系統進行任務調度的基本單元
2.進程由代碼段,數據段,堆棧段三部分組成,每一個進程都有本身的獨立的內存空間,不一樣進程間的數據不能共享,各進程能夠進程通訊。
3.一個進程生成時,系統會爲它分配一個惟一的進程標識符(PCB),操做系統靠它來感知進程的存在。
4.一個進程能夠包含多個線程,也就是所謂的多線程socket

什麼是線程?

1.線程,有時也稱輕量級進程(LWP),它是程序執行流的最小單元。它由堆棧,寄存器集,線程ID,當前指令指針(PC)組成。
2.線程也能夠理解爲顆粒度更小的進程,對任務劃分更爲精細,線程能夠像進程同樣,進行併發,提升了程序的響應度,一個
進程包含了多個線程。
3.線程之間能夠共享進程中一部分的內存空間(包括代碼段,數據段,堆),還有一個進程中的資源(如打開的文件,socket,還有信號)
4.進程是能夠做爲一個程序獨立運行的,可是線程並不能夠,它必須依附於進程來工做,一個進程最少有一個線程。函數

線程相較於進程的優勢

那有了進程,爲何咱們還須要線程?
1.存在就是有價值的,爲何這麼說呢,理由之一:仍是要從進程的缺點提及,進程與線程相比是一個很重的東西,建立一個進程消耗的資源是建立一個線程消耗的資源的幾倍,
不只如此,線程切換上下文比進程切換上下文的效率更高,速度更快,一個進程消耗的資源通常是線程的幾十倍(視工做環境)。
2.理由之二:每一個進程都有一個獨立的地址空間,要進行數據傳遞只能經過通訊的方式,這樣不只費時,並且效率還不高,可是線程就不同了,進程是包含線程的,因此線程可
以共享進程大部分的數據,這樣操做數據時更加快速,快捷,但這也引起了線程安全的問題,稍後會討論。性能

除去進程,單論線程的優點

1.提升程序響應能力,就如咱們通常所使用的桌面環境,若是你點擊一個按鈕,接下的動做須要二十秒的時間完成,那麼在這段時間中,咱們什麼都作不了,若是有了多線程,那
咱們徹底能夠從新生成一個線程處理這個線程,,原線程繼續跟用戶進行交互,大大提高了程序的響應程度。
2.提供CPU的利用率,隨着計算機技術的發展,如今的CPU都是多核的,要想徹底利用好這些CPU資源,咱們能夠在每一個核中生成一個線程,只要咱們的線程數小於CPU中的核心數
數,就能夠實現正的併發(內核線程在線程模型中會介紹)

線程的訪問權限

  • 棧:(儘管並非徹底不能被其餘線程訪問,可是通常狀況下,仍是被認爲爲私有數據)
  • 線程局部存儲(Thread Local Storge, TLS):是某些操做系統爲線程單獨提供的私有空間,一般具備頗有限的容量。
  • 寄存器(包括pc寄存器):寄存器是執行流的基本數據,爲線程私有。

線程私有:

  1. 函數參數
  2. TLS

  3. 線程之間共享:
  4. 代碼段
  5. 全局變量
  6. 函數中的靜態變量
  7. 堆上的數據
  8. 一些進程中資源:打開的文件,socket,信號等

線程的調度和優先級

線程在進程中的三種狀態

線程在運行時能夠有三種狀態:等待,就緒,運行。
切換順序:

  1. 等待 -> 就緒 -> 運行 -> 等待
  2. 運行 -> 就緒 -> 運行(對於CPU密集型程序常出現時間片用完還沒完成任務,因而在運行)

調度和優先級的基本概念

1.多線程的出現,致使了一個處理器上常常要運行多個線程,各個線程怎麼運行?誰先誰後?這些問題都交給了線程調度,
有了線程調度,一個線程在規定的時間片(處於運行中的程序擁有的一段能夠執行的時間)內運行,超出了時間片的時間,調度機制將會安排下一個線程運行。
2.操做系統從開始的多道程序到後來的分時操做再到如今的多任務操做系統,線程調度經歷不少的演變,如今線程調度雖然各不相同,可是都有優先級調度和
輪轉法的痕跡。輪轉法(讓每個程序都運行一段時間,時間一到就切換到下一個程序),優先級調度(根據程序的優先級來運行,優先級高的先運行)

優先級的設置

通常在linux和window中用戶能夠手動設置線程的優先級,但系統也能夠本身根據運行狀態設置優先級,好比一個頻繁進行等待的線程比每次都要把時間片
用光的線程更收歡迎,而且也更容易提高線程的優先級。

線程餓死

CPU密集型和IO密集型
簡單來講,咱們通常把那些頻繁進行等待的線程稱爲IO密集型,而把那些不多進行等待的線程成爲CPU密集型線程(具體可查閱資料)

想象一個場景,若是一個高優先級的CPU密集型任務,在每次時間片用盡後進入就緒狀態,而後又進入運行狀態,那麼很低
優先級的程序就會永遠等不到運行,這就是線程餓死

線程提高優先級的方法

爲了不線程餓死現象,操做系統會把那些老是等不到運行的程序,隨着時間的累計,逐漸增長他們的優先級,直到他們可以
被運行爲止。

提供線程優先級的方法:
1.手動設置
2.根據等待的頻繁程度,增長或減小優先級。
3.隨着時間的累計,逐漸增減優先級。

搶佔式線程和不可搶佔式線程

在以前咱們討論的線程調度,每當一個線程在執行時間到達時間片後,都會被系統收回控制權,以後執行其餘線程,這就是搶佔式
線程,當在不可搶佔式線程中時,線程是不可搶佔的,除非線程本身發出一箇中止執行的命令,或者進入等待。在該線程模型下,
線程必須本身主動進入就緒狀態,而不是靠時間片強行進入就緒狀態。若是一個線程沒有等待,也沒有主動進入就緒,那麼它將
一直運行,其餘線程被阻塞。

在非搶佔式進程中,線程主動放棄有兩種狀況:

  • 線程本身放棄
  • 線程進入等待

但在如今非搶佔式線程基本已經看不到了,基本上都是搶佔式進程。

線程模型

內核線程

線程的併發執行是由多處理器和操做系統實現的,但實際狀況更爲複雜一點,windows和linux等操做系統,大多都在內核裏提供了
線程的支持,內核線程有多處理器或者調度來實現併發,然而用戶使用的線程實際上是存在於用戶態的線程,並非內核線程,用戶態
線程的數量並不等同於內核線程的數量,極可能是一個內核線程對應多個用戶線程。

線程模型

1.一對一模型:
一個內核線程對應於一個用戶線程,這樣用戶線程就有了和內核線程一致的線程,這時候線程之間的併發是真正的併發,
一個線程阻塞並不影響其餘線程的運行。可是也有很大的缺點,不少操做系統限制操做系統的內核數量,所以用戶線程的
數量就會收到影響,其次許多系統的內核線程切換上下文時開銷比較大,致使效率低下。
2.一對多模型:
一個內核線程映射了許多個用戶線程,這種狀況下,線程之間的切換效率很是高,可是若是其中有一個線程阻塞的話,那麼
處於該內核線程的其餘線程將得不到運行。除此以外,多處理器系統上,若是處理器的增多,對線程的性能提高並不大,但
是多對一模型獲得的好處是線程切換的高效和幾乎不限制的線程數量。
3.多對多模型:
多對多模型結合了一對一和一對多模型的特色,一個線程的阻塞並不會使其餘線程阻塞。多對多模型線程對用戶線程的數量
也沒有什麼限制,在多處理器上的性能也還行,可是不如一對多模型。

線程安全

假想一個場景,若是你和你女友各有一張相同卡號的銀行卡,餘額爲100萬,而後大家同時在ATM上取錢,你直接100萬,而後你
女友也取了40萬用來買車,也成功取出來了。可是100萬怎麼取出140萬了,這樣銀行豈不是要倒閉了,緣由在於男的在取錢的時
候假如正在訪問餘額這個變量,可是女的也同時在訪問這個變量,這就形成數據錯誤了, 若是男的在訪問這個餘額這個變量的時候,
女的不能訪問,那麼數據就不會形成錯誤了,這就是簡單的線程安全。

幾種線程鎖

多線程處於一個多變的環境中,全局變量,堆數據,靜態局部變量隨時可能被多個程序更改,形成毀滅性的打擊,因此在併發時數據的
安全性和一致性很重要。

同步與鎖
1.爲了不一個變量被多個線程同時使用和修改,咱們須要多個線程對該數據進行數據進行訪問同步。同步就是一個線程在訪問一個數據
的時候,其餘線程不能再對其進行訪問。
2.同步最多見的方法是使用鎖,鎖是一種非強制機制,線程在訪問數據時Acquire,在訪問結束時Release。當鎖還沒release時,其餘
的線程不能訪問該數據,處於阻塞狀態,直到鎖release。

常見的鎖有:
二元信號量,多元信號量(簡稱信號量),互斥量,臨界區,讀寫鎖,條件變量

二元信號量

這是最簡單的一種鎖,只有兩種狀態,即鎖住和未鎖住,它適合只能惟一被一個線程佔用的資源, 他能夠先被一個線程得到,可是能夠被其餘
線程釋放。

信號量

信號量能夠稱爲多元信號量,它容許多個線程併發訪問一個資源,一個初值爲N的信號量,能夠容許N個線程併發訪問。線程訪問資源時,首先
獲取信號量,具體步驟以下:
1.h將信號量的值減1
2.若是信號量相減的值大於0,則繼續運行,不然進入等待狀態。
3.訪問完資源後,進行下面操做
4.將信號量加1,若是大於1,喚醒一個等待中的進程

互斥量

互斥量和二元信號量很類似,同時只容許一個線程訪問,只是二元信號量它能夠被一個線程獲取,但能夠被任意進程釋放。互斥量與之不一樣,一個
進程獲取了互斥量,釋放時只能由本線程釋放,不能由其餘線程釋放。

臨界區

臨界區是比互斥量更爲嚴格的一種鎖,咱們把臨界區的鎖的獲取稱爲進入臨界區,鎖的釋放離開臨界區。無論是互斥量仍是信號量,他們都是在整個
系統中可見的,也就是說一個進程建立了一個互斥量和信號量,其餘進程能夠獲取該把鎖。然而臨界區的做用範圍僅限於本進程可見,其餘進程是無
法獲取該鎖的,除此以外,臨界區和互斥量相同。

讀寫鎖

假想一個場景,若是一個進程中,對一個數據要進行大量的讀寫,更具體的來講是大量地讀,少許地寫,若是每次在讀寫以前都上鎖,讀寫完成後都
釋放鎖,那麼加入我讀寫一共進行100次,那就一共有100次得到鎖和釋放鎖的過程,若是使用讀寫鎖,事情變得想對簡單。

首先讀寫鎖有兩種獲取方式,一種是獨佔式,一種是共享式

**當鎖處於自由狀態時,以任何一種狀態得到鎖都能成功,若是鎖處於共享狀態,其餘線程以共享方式得到也能成功(獨佔式不行)。若是一個鎖處於獨
佔式的狀態,那麼以任何一種方法都不能得到鎖**

條件變量

以上介紹的鎖處於系統自動控制的狀態,不能準確地控制各自線程的順序,因此再次基礎上,咱們又加上了條件變量這個機制,在下面有一段關於條件變
量和互斥量相互使用的實例,能夠參閱。

官方點來講條件變量是一種同步手段,對於條件變量來講,線程有兩種狀態,一種是線程能夠等待條件變量(類比接收一個信號),其次是線程能夠喚醒條件
變量(類比發送一個信號)。一個條件變量能夠被多個線程等待,通俗點來講當一個線程喚醒了一個條件變量(發送信號)後,多個線程等到了條件變量(接收
到了信號),那麼多個線程就能夠一塊兒執行。

可重入

一個函數被重入,若是一個函數沒有執行完成,可是因爲外部或者內部調用,又一次進入函數內部,一個函數要被重入,要有兩個條件:
1.多個線程執行此函數
2.函數自身調用自身

一個建議:在鎖中間最好避免出現函數調用的現象,以防出現重入現象。

// 這是一個函數調用自身的例子,當打印出hello world以後就一直卡死,形成死鎖
#include <iostream>
#include <pthread.h>
pthread_mutex_t task_mutex;

void hello(){

    pthread_mutex_lock(&task_mutex);
    std::cout << "hello world" << std::endl;
    hello();
    pthread_mutex_unlock(&task_mutex);
}
int main(int argc, char const* argv[])
{
    hello();
    return 0;
}

代碼演示

主要使用到的頭文件:
pthread.h
semaphore.h
在命令行編譯時要加上 -lpthread 表示連接libpthread.so這個動態庫。

// c++中建立多個線程
#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;
#define NUM_THREADS 5

void* say_hello(void *args){
        cout << "hello world" << endl;
}
int main(){

        for (int i = 0; i < NUM_THREADS; ++i) {

int ret = pthread_create(&tids[i], NULL, say_hello, NULL);

if(ret != 0){
                        cout << "pthread_create error: error_code= " << ret << endl;
                }
        }

        //pthread_join(tids[0], NULL);
        //pthread_join(tids[1], NULL);
        //pthread_join(tids[2], NULL);
        //pthread_join(tids[3], NULL);
        //pthread_join(tids[4], NULL);
        pthread_exit(NULL);
        return 0;
}
運行結果:
hello world
hello world
hello world
hello world
hello world
// 這個版本是pthread_create中帶參數
#include <iostream>
#include <pthread.h> 
#include <cstdlib> 
#include <unistd.h> 
using namespace std;
#define NUM_THREADS 5

void* say_hello(void *args){
        // 必定要進行強制類型轉換
        int* tid  = (int *)args;
        cout << "in say_hello, the args is " << *tid << endl;
}
int main(){

        pthread_t tids[NUM_THREADS];
        int arr[NUM_THREADS];
        for (int i = 0; i < NUM_THREADS; ++i) {
                cout << "in main, the i is" << i << endl;
                arr[i] = i;
                // pthread_create中參數是void*類型,因此要強制類型轉換
                int ret = pthread_create(&tids[i], NULL, say_hello, (void*)&arr[i]);
                if(ret != 0){
                        cout << "pthread_create error: error_code= " << ret << endl;
                        exit(-1);
                }
        }

        pthread_exit(NULL);
        return 0;
}
運行結果:
in main, the i is0
in main, the i is1
in say_hello, the args is 0
in main, the i is2
in say_hello, the args is 1
in main, the i is3
in say_hello, the args is 2
in main, the i is4
in say_hello, the args is 3
in say_hello, the args is 4




// 使用條件信號量的例子, 其中也包含了互斥鎖的使用
#include <iostream>
#include <pthread.h>
#include <stdio.h>
using namespace std;

#define BOUNDARY 5
int tasks = 10;

pthread_mutex_t tasks_mutex; //定義互斥鎖
pthread_cond_t tasks_cond; // 條件信號變量,處理處理兩個線程之間的條件關係,當tasks>5,hello2處理,反之hello1,直到tasks8減到0

void* say_hello2(void *args){

        pthread_t pid = pthread_self(); //獲取當前線程id
        cout << "[" << pid << "] hello in thread" << *((int *)args) << endl;
        bool is_signed = false;
        while(1){
                pthread_mutex_lock(&tasks_mutex);
                if(tasks > BOUNDARY){
                        cout << "[" << pid << "] tasks task: " << tasks << "in thread " << *((int *)args) << endl;
                        --tasks;
                }else if(!is_signed){
                        cout << "[" << pid << "] pthread_cond_signal in thread" << *((int *)args) << endl;
                        pthread_cond_signal(&tasks_cond);
                        is_signed = true;
                }
                pthread_mutex_unlock(&tasks_mutex);
                if(tasks == 0)
                        break;
        }
}
void *say_hello1(void *args){

        pthread_t pid = pthread_self();
        cout << "[" << pid << "] hello in thread " << *((int *)args) << endl;
        while(1){
                pthread_mutex_lock(&tasks_mutex);
                if(tasks > BOUNDARY){
                        cout << "[" << pid << "] pthread_cond_signal in thread " << *((int *)args) << endl;
                        pthread_cond_wait(&tasks_cond, &tasks_mutex);

                } else {
                        cout << "[" << pid << "] task task: " << tasks << "in thread " << *((int *)args) << endl;
                        --tasks;
                }
                pthread_mutex_unlock(&tasks_mutex);
                if (tasks == 0)
                        break;
        }
}
int main(){

        pthread_attr_t attr;
        pthread_attr_init(&attr);
        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
        pthread_cond_init(&tasks_cond, NULL); // 初始化條件信號量
        pthread_mutex_init(&tasks_mutex, NULL); // 初始化互斥量
        pthread_t tid1, tid2; // 保存兩個線程
        int index = 1;
        int ret1 = pthread_create(&tid1, &attr, say_hello1, (void *)&index);
        if (ret1 != 0){
                cout << "pthread_create error:error_code= " << ret1 << endl;
        }

        int index2 = 2;
        int ret2 = pthread_create(&tid2, &attr, say_hello2, (void *)&index);
        if (ret2 != 0){
                cout << "pthread_create error:error_code= " << ret2 << endl;
        }

        pthread_join(tid1, NULL); //連接兩個線程
        pthread_join(tid2, NULL);
        pthread_attr_destroy(&attr); //釋放內存
        pthread_mutex_destroy(&tasks_mutex);
        pthread_cond_destroy(&tasks_cond); //正常退出

}
相關文章
相關標籤/搜索