Linux線程簡單理解3

線程不安全

//寫多線程一定確保是多核的
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#define Thread_num 2
int g_count=0;
void* ThreadEntry(void* arg)
{
    (void)arg;
    for(int i=0;i<50000;i++)
    {
        ++g_count;
    }
    return NULL;
}
int main()
{
    pthreda_t tid[Thread_num];
    for(int i=0;i<Thread_num;++i)
    {
        pthread_create(&tid[i],NULL,ThreadEntry,NULL);
    }
    for(int i=0;i<Thread_num;i++)
    {
        pthead_join(tid[i],NULL);  //線程回收
    }
    printf("g_count=%d\n",g_count);
    return 0;
}///Thread_num改成1試試

++g_count 操作:
1.把g_count從內存加載到cpu中。2,執行++(寄存器上的++),對寄存器的內容進行自增。3.cpu中的值寫回內存。

兩次操作相互影響(2個線程) 線程不安全(多線程環境下,程序執行結果出現預期之外的值)概率性問題。

  • 多線程訪問的公共資源叫做“臨界資源”
  • 訪問臨界資源的代碼叫做“臨界區”
  • 在臨界區中使用互斥機制,解決線程不安全機制
  • 互斥:任何時刻,互斥保證有且只有一個執行流進入臨界區,訪問臨界區資源,通常對臨界資源起保護作用
  • 原子性:不會被任何調度機制打斷的操作,該操作只有兩態,要麼完成,要麼未完成。

互斥量mutex

  • 大部分情況下,線程使用的數據都是局部變量,變量的地址空間在線程棧空間內,這種情況,變量歸屬單個線程,其他線程無法獲取這種變量。
  • 但有的時候,很多變量都需要在線程間共享,這樣的變量稱爲共享變量,可以通過數據的共享,完成線程之間的交互。
  • 多個線程併發的操作共享變量,會帶來一些問題。

互斥量的接口
初始化互斥量

1.靜態分配:
pthread_mutex_t mutex= PTHREAD_MUTEX_INITIALIZER
2.動態分配:
pthread_mutex_init()

銷燬互斥量

  • 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要銷燬
  • 不要銷燬一個已經加鎖的互斥量
  • 已經銷燬的互斥量,要確保後面不會有線程再嘗試加鎖
pthread_mutex_destroy();

互斥量加鎖和解鎖

pthread_mutex_lock()
pthread_mutex_unlock()
  • 互斥量處於未鎖狀態,該函數會將互斥量鎖定,同時返回成功
  • 發起函數調用時,其他線程已經鎖定互斥量,或者存在其他線程同時申請互斥量,但沒有競爭到互斥量,那麼pthread_lock調用會陷入阻塞(執行流被掛起),等待互斥量解鎖。

互斥鎖pthread_mutex”掛起等地鎖“,一旦線程獲取鎖失敗,就會掛起(進入操作系統提供的一個等待隊列中)這個線程什麼時候才能恢復執行,也不是其他線程釋放鎖,立即就能恢復執行
而是其他線程釋放鎖之後,當前線程還得看操作系統的心情來決定啥時候恢復執行。

互斥鎖可以保證線程安全,最終的程序效率收到影響。加鎖也會有開銷。除此之外,還有一個更嚴重的問題“死鎖”

死鎖

  • 死鎖是指在一組進程中的各個進程均佔有不會釋放的資源,但因互相申請被其他進程所佔用不會釋放的 資源而處於的一種永久等待狀態。

死鎖四個必要條件

  • 互斥條件:一個資源每次只能被一個執行流使用
  • 請求與保持條件:一個執行流因請求資源而阻塞時,對已獲得的資源保持不放
  • 不剝奪條件:一個執行流已獲得的資源,在未使用完之前,不能強行剝奪。(當系統把這類資源分配給某進程後,再不能強行收回,只能在進程用完後自行釋放。)
  • 循環等待條件:若干執行流之間形成一種頭尾相接的循環等待資源的關係。

避免死鎖

  • 破壞死鎖的四個必要條件
  • 加鎖順序一致
  • 避免鎖未釋放的場景
  • 資源一次性分配
    比較實用的解決方案,從代碼設計的角度來解決死鎖問題
    1.短 讓臨界區的代碼儘量短
    2.平 臨界區代碼儘量不去調用其他複雜函數
    3.快 讓臨界區代碼執行速度快,別做太多耗時的操作

避免死鎖算法

  • 死鎖檢測算法
  • 銀行家算法

線程安全
多個線程併發執行同一份代碼,不會出現不同的結果。

重入
同一個函數被不同的執行流調用,當前一個執行流還沒有結束,就有其他執行流再次進入,我們稱之爲重入。一個函數在重入的情況下,運行結果不會出現問題,我們就稱該函數是可重入的,否則爲不可重入。

常見線程不安全的情況

  • 不保護共享變量的函數
  • 函數狀態隨着被調用,狀態發生變化的函數
  • 調用線程不安全函數的函數

常見線程安全的情況

  • 每個線程對全局變量或者靜態變量,只有讀權限,沒有寫權限,這樣的線程一般是安全的。
  • 類或者接口對於線程來說都是原子操作
  • 多個線程之間的切換不會導致該接口的執行結果不會存在二義性

常見不可重入的情況

  • 調用了malloc/free函數,因爲malloc函數是用全局鏈表來管理堆的
  • 調用了標準I/O函數,因爲標準I/O函數大部分是調用不可重入的方式來管理全局數據結構
  • 可重入函數體內使用了靜態的數據結構

常見不可重入的情況

  • 不使用全局變量或者靜態變量
  • 不使用malloc和new來開闢空間
  • 不調用不可重入函數
  • 不返回靜態數據和全局數據,所有數據都由函數調用者提供

可重入與線程安全區別

  • 可重入函數是線程安全函數的一種
  • 線程安全不一定是可重入的,而可重入函數一定是線程安全
  • 如果將對臨界資源的訪問加上鎖,則這個函數是線程安全的,如果這個重入函數若鎖還未釋放繼續調用,就會產生死鎖,因此是不可重入的。
  • 如果一個函數中由全局變量,那麼這個函數既不是可重入函數也不是線程安全函數

線程同步
條件變量
當一個線程互斥地訪問某個變量時,它可能發現在其他線程在改變狀態之前,它什麼也做不了(不能訪問數數據,但是一直調度,產生資源的開銷,線程飢餓)。例如一個線程訪問隊列時,發現隊列爲空,它只能等待,等待其他線程將一個數據節點插入隊列裏。

同步概念
在保證數據安全的前提下,讓線程能夠按照某種特定的訪問順序訪問臨界資源,從而有效避免飢餓問題

條件變量常用函數

//條件變量初始化
int pthread_cond_init()
//條件變量銷燬
int pthread_cond_destroy()
//等待條件滿足
//經常搭配互斥鎖來使用
int pthread_cond_wait()

pthread_cond_wait() 做了3件事情
1.先釋放鎖
2.等待條件就緒

3.重新獲取鎖,準備執行後續的操作
前兩步操作必須是在一起的(原子操作),否則可能會錯過其他線程的通知信息,導致還是在這傻等!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章