Linux 線程和線程同步

来源:https://www.cnblogs.com/stux/archive/2023/11/07/17812681.html
-Advertisement-
Play Games

包括線程概念簡介;線程創建函數pthread_create以及退出、回收等;線程同步互斥鎖pthread_mutex_t,讀寫鎖pthread_rwlock_t,條件變數pthread_cond_t以及信號量semaphore ...


1. 線程的概念

 【操作系統】2.進程和線程 - imXuan - 博客園 (cnblogs.com)

  • 線程:light weight process(LWP)輕量級的進程,在 Linux 中本質上仍然是一個進程
  • 進程:有獨立的地址空間,獨立PCB,可以當作只有一個線程的進程。進程是電腦資源分配的最小單位
  • 線程:有獨立的PCB,共用物理地址空間,是最小的執行單位。cpu時間片劃分以PCB為依據,是調度的基本單位
  • LWP號:cpu劃分時間片的依據。指令 " ps -Lf pid " 查看

1.1 線程共用的資源

  • 1) 文件描述符表
  • 2) 每種信號的處理方式
  • 3) 當前工作目錄
  • 4) 用戶ID和組ID
  • 記憶體地址空間 (.text/.data/.bss/heap/共用庫) -> 堆區全局共用變數是共用的(進程是讀時共用寫時複製,實際上就是非共用)

1.2 線程非共用資源

  • 1) 線程id
  • 2) 處理器現場和棧指針(內核棧)
  • 3) 獨立的棧空間(用戶空間棧)
  • 4) errno變數
  • 5) 信號屏蔽字(共用信號,但是可以使用信號屏蔽字)
  • 6) 調度優先順序

1.3 線程優缺點

  • 優點:
    • 提高程式併發性
    • 開銷小
    • 數據通信、共用數據方便
  • 缺點:
    • 庫函數,不穩定
    • 調試、編寫困難、gdb不支持
    • 對信號支持不好
    • 優點相對突出,缺點均不是硬傷。Linux下由於實現方法導致進程、線程差別不是很大。

2. 線程常用操作

  • 創建線程:pthread_create
  • 線程獲取:pthread_self
  • 線程退出:
    • 線程內部:return void* (0);
    • 線程內部:pthread_exit(void *(0));
    • 線程外部:pthread_canel(返回值是 -1,需要一個取消點)
  • 線程回收
    • 手動回收:pthread_join
    • 自動回收:pthread_detach

2.1 創建線程 pthread_create

  功能:創建一個線程。

#include <pthread.h>int pthread_create(pthread_t *thread,
            const pthread_attr_t *attr,
            void *(*start_routine)(void *),
            void *arg );
/*  參數:
        thread:傳出參數。線程標識符地址(一個無符號數)。
        attr:線程屬性結構體地址,通常設置為 NULL。
        start_routine:線程函數的入口地址。
        arg:傳給線程函數的參數。
    返回值:
        成功:0
        失敗:非 0
*/

   線程中處理出錯

#include <string.h>
char *strerror(int errnum);

fprintf(stderr, "xxx error: %s\n", strerror(錯誤號));

2.2 獲取線程ID pthread_self

  功能:獲取線程號(與ps -Lf 查看的 id 不同)

#include <pthread.h>
​
pthread_t pthread_self(void);
/*   參數:無
     返回值 調用線程的線程 ID 。
*/

2.3 線程退出 pthread_exit

  功能:退出調用線程。一個進程中的多個線程是共用該進程的數據段,因此,通常線程退出後所占用的資源並不會釋放。

  • return:返回到調用者
  • exit:退出進程
  • pthread_exit:退出線程
#include <pthread.h>void pthread_exit(void *retval);
/*    參數:retval:存儲線程退出狀態的指針。
      返回值:無 */ 

2.4 線程回收 pthread_join

  功能:等待線程結束(此函數會阻塞),並回收線程資源,類似進程的 wait() 函數。如果線程已經結束,那麼該函數會立即返回。

#include <pthread.h>int pthread_join(pthread_t thread, void **retval);
/*  參數:
        thread:被回收的線程號。
        retval:用來存儲線程退出狀態的指針的地址
               (pthread_exit 退出返回值是 void*, 這裡是一個指針, 指向 void*指針, 所以是void**)
    返回值:
        成功:0
        失敗:非 0         */

2.5 線程分離 pthread_detach

  功能:使調用線程與當前進程分離,分離後不代表此線程不依賴與當前進程,線程分離的目的是將線程資源的回收工作交由系統自動來完成,也就是說當被分離的線程結束之後,系統會自動回收它的PCB資源。所以,此函數不會阻塞。

#include <pthread.h>int pthread_detach(pthread_t thread);
/*  參數:thread:線程號。
    返回值:
        成功:0
        失敗:非0            */

2.6 線程取消 pthread_cancel

  功能:殺死(取消)線程

  • 被 pthread_cancel() 殺死的線程,使用 pthread_join() 再進行回收,會得到返回值 -1
  • 使用 pthread_cancel() 殺死線程必須有一個保存點才能生效,否則無法殺死線程。應該在被 cancel 函數調用的線程函數里自己添加一個取消點 pthread_testcancel(); 實際上就是進入系統內核,給他一個殺死線程的機會
#include <pthread.h>int pthread_cancel(pthread_t thread);
/*  參數:thread : 目標線程ID。
    返回值:
        成功:0
        失敗:出錯編號             */

3. 線程同步

3.1 互斥鎖 pthread_mutex_t

  互斥鎖是一種簡單的加鎖的方法來控制對共用資源的訪問,互斥鎖只有兩種狀態,即加鎖( lock )和解鎖( unlock )

#include <pthread.h>

// 創建互斥鎖
pthread_mutex_t mutex; 

// 靜態初始化 互斥鎖
mutex = PTHREAD_MUTEX_INITIALIZER;

// 動態初始化 互斥鎖, attr:設置互斥量的屬性, NULL表示預設
int pthread_mutex_init(pthread_mutex_t *restrict mutex,  const pthread_mutexattr_t *restrict attr);

// 銷毀指定的一個互斥鎖。互斥鎖在使用完畢後,必須要對互斥鎖進行銷毀,以釋放資源
int pthread_mutex_destroy(pthread_mutex_t *mutex);

// 對互斥鎖上鎖,若互斥鎖已經上鎖,則調用者阻塞,直到互斥鎖解鎖後再上鎖
int pthread_mutex_lock(pthread_mutex_t *mutex);

// 嘗試對互斥鎖上鎖,若已經上鎖跳過執行後面的代碼
int pthread_mutex_trylock(pthread_mutex_t *mutex);

// 對指定的互斥鎖解鎖
int pthread_mutex_unlock(pthread_mutex_t *mutex);

3.2 讀寫鎖 pthread_rwlock_t

  當有一個線程已經持有互斥鎖時,互斥鎖將所有試圖進入臨界區的線程都阻塞住。但是當前持有互斥鎖的線程只是要讀訪問共用資源,而同時有其它幾個線程也想讀取這個共用資源,但是由於互斥鎖的排它性,所有其它線程都無法獲取鎖,也就無法讀訪問共用資源了,但是實際上多個線程同時讀訪問共用資源並不會導致問題。

  在對數據的讀寫操作中,更多的是讀操作,寫操作較少,例如對資料庫數據的讀寫應用。為了滿足當前能夠允許多個讀出,但只允許一個寫入的需求,線程提供了讀寫鎖來實現。

讀寫鎖的特點:

  • 如果有其它線程讀數據,則允許其它線程執行讀操作,但不允許寫操作
  • 如果有其它線程寫數據,則其它線程都不允許讀、寫操作

讀寫鎖分為讀鎖和寫鎖,規則如下:

  • 如果某線程申請了讀鎖,其它線程可以再申請讀鎖,但不能申請寫鎖
  • 如果某線程申請了寫鎖,其它線程不能申請讀鎖,也不能申請寫鎖

舉例子:線程1 給讀寫鎖加了讀鎖,此時 線程2 請求讀鎖、線程3 請求寫鎖;則 線程2 的讀鎖會被阻塞,等 線程1 讀鎖釋放後,線程3 進行寫,之後 線程2 再讀

#include <pthread.h>// 初始化一個讀寫鎖(restrict 修飾指針變數, 被變數修飾的記憶體操作只能由本指針操作)
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

// 銷毀一個讀寫鎖
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

// 阻塞上讀鎖
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 非堵塞上讀鎖
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

// 阻塞上寫鎖
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 非阻塞上寫鎖
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

// 全解鎖
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

3.3 條件變數 pthread_cond_t

  與互斥鎖不同,條件變數是用來等待而不是用來上鎖的,條件變數本身不是鎖,條件變數用來自動阻塞一個線程,直到某特殊情況發生為止。通常條件變數和互斥鎖同時使用。生產者消費者模型中比較常用

  條件變數的兩個動作:

  • 條件不滿, 阻塞線程
  • 當條件滿足, 通知阻塞的線程開始工作

  條件變數的類型: pthread_cond_t

#include <pthread.h>// 初始化一個條件變數
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

// 銷毀一個條件變數
int pthread_cond_destroy(pthread_cond_t *cond);

// 等待條件滿足
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
/*
 * 功能:
 *   1.阻塞等待一個條件變數
 *   2.解鎖已經加鎖成功的互斥量    (1.2為原子操作)
 *   .....等待.....
 *   3.當條件滿足,函數返回時,重新加鎖互斥量
 */

// 等待條件滿足, 超時退出
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec*restrict abstime);

// 喚醒阻塞在條件變數上的線程
int pthread_cond_signal(pthread_cond_t *cond);

// 喚醒所有阻塞在條件變數上的線程
int pthread_cond_broadcast(pthread_cond_t *cond);

  timespec結構體(abs_time 表示絕對時間,從1970年1月1日 00:00:00計算)

struct timespec {
    time_t tv_sec;      /* seconds */ //
    long   tv_nsec; /* nanosecondes*/ // 納秒
}
​
time_t cur = time(NULL);        //獲取當前時間。
struct timespec t;              //定義timespec 結構體變數t
t.tv_sec = cur + 1;             // 定時1秒
pthread_cond_timedwait(&cond, &t);

  一個示例代碼

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

void err_thread(int ret, char* str)
{
    if(ret!=0){
        fprintf(stderr, "%s:%s\n", str, strerror(ret));
        pthread_exit(NULL);
    }
}

// 創建公共區
struct msg{
    int num;
    struct msg *next;
};
struct msg *head = NULL;
// 創建互斥量,初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 創建條件變數,初始化
pthread_cond_t has_data = PTHREAD_COND_INITIALIZER;

void* producer(void* arg)
{
    int i = 0;
    while(1){
        struct msg *p = malloc(sizeof(struct msg));
        // 生產數據
        p->num = ++i;
        p->next = NULL;
        printf("product: %d\n", p->num);
        
        // 將數據保存到公共區
        pthread_mutex_lock(&mutex);
        p->next = head;
        head = p;
        pthread_mutex_unlock(&mutex);

        // 通知消費者
        pthread_cond_signal(&has_data);

        sleep(rand() % 3);
    }
    return NULL;
}

void* consumer(void* arg)
{
    while(1){
        struct msg *mp;
        // 加鎖互斥量
        pthread_mutex_lock(&mutex);
        // 判斷條件是否滿足
        while (head==NULL)  // 註意 while 迴圈才可以解決多消費者搶鎖的問題
        {   // 阻塞等待, 解鎖
            pthread_cond_wait(&has_data, &mutex);
        }   // 返回值, 重新加鎖

        mp = head;
        head = mp->next;

        // 操作公共區結束, 立即解鎖
        pthread_mutex_unlock(&mutex);
        printf("consumer:%d\n", mp->num);
        free(mp);

        sleep(rand() % 3);
    }
    
    return NULL;
}

int main()
{
    int ret;
    pthread_t pid1, pid2, cid1, cid2, cid3;

    srand(time(NULL));
    
    ret = pthread_create(&pid1, NULL, producer, NULL);
    if(ret!=0)
        err_thread(ret, "pthread_create producer:");
    ret = pthread_create(&pid2, NULL, producer, NULL);
    if(ret!=0)
        err_thread(ret, "pthread_create producer:");
    
    ret = pthread_create(&cid1, NULL, consumer, NULL);
    if(ret!=0)
        err_thread(ret, "pthread_create consumer:");
    ret = pthread_create(&cid2, NULL, consumer, NULL);
    if(ret!=0)
        err_thread(ret, "pthread_create consumer:");
    ret = pthread_create(&cid3, NULL, consumer, NULL);
    if(ret!=0)
        err_thread(ret, "pthread_create consumer:");
    
    pthread_join(pid1, NULL);
    pthread_join(pid2, NULL);
    pthread_join(cid1, NULL);
    pthread_join(cid2, NULL);
    pthread_join(cid3, NULL);

    return 0;
}

3.4 信號量 semaphore

  信號量廣泛用於進程或線程間的同步和互斥,信號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。編程時可根據操作信號量值的結果判斷是否對公共資源具有訪問的許可權,當信號量值大於 0 時,則可以訪問,否則將阻塞

#include <semaphore.h>// 創建一個信號量並初始化它的值。一個無名信號量在被使用前必須先初始化。
// pshared 0:線程同步; 1: 進程同步
// value: 信號量的初值
// 成功返回0, 失敗返回-1, 設置errno
int sem_init(sem_t *sem, int pshared, unsigned int value);

// 刪除 sem 標識的信號量。
int sem_destroy(sem_t *sem);

// 將信號量的值減 1。操作前,先檢查信號量(sem)的值是否為 0,若信號量為 0,此函數會阻塞,直到信號量大於 0 時才進行減 1 操作。
int sem_wait(sem_t *sem);

// 以非阻塞的方式來對信號量進行減 1 操作。
// 若操作前,信號量的值等於 0,則對信號量的操作失敗,函數立即返回。
int sem_trywait(sem_t *sem);

// 限時嘗試將信號量的值減 1
// abs_timeout:絕對時間
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

// 將信號量的值加 1 併發出信號喚醒等待線程(sem_wait())。
int sem_post(sem_t *sem);

// 獲取 sem 標識的信號量的值,保存在 sval 中。
int sem_getvalue(sem_t *sem, int *sval);

   一個示例代碼

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#define NUM 5

int queue[NUM];
sem_t blank_number, product_number;

// 消費者
void* consumer(void *arg)
{
    int i = 0;
    while(1)
    {
        sem_wait(&product_number);   // 產品數量-- (0則阻塞)
        printf("Consume:%d\n", queue[i]);
        queue[i] = 0;
        sem_post(&blank_number);     // 空格數量++

        i = (i+1) % NUM;
        sleep(rand() % 6);
    }
}
// 生產者
void* producer(void *arg)
{
    int i = 0;
    int number = 0;
    while(1)
    {
        sem_wait(&blank_number);    // 空閑數量-- (0則阻塞)
        queue[i] = ++number;
        printf("Produce:%d\n", number);
        sem_post(&product_number);  // 產品數量++

        i = (i+1) % NUM;
        sleep(rand() % 2);
    }
}
int main()
{
    pthread_t pid, cid;
    sem_init(&blank_number, 0, NUM);     // 初始化空閑區域
    sem_init(&product_number, 0, 0);     // 初始化產品數量

    pthread_create(&pid, NULL, producer, NULL);
    pthread_create(&cid, NULL, consumer, NULL);

    pthread_join(pid, NULL);
    pthread_join(cid, NULL);

    sem_destroy(&blank_number);
    sem_destroy(&product_number);

    return 0;
}

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 高精度的本質是將數字以字元串的形式讀入,然後將每一位分別存放入`int`數組中,通過模擬每一位的運算過程,來實現最終的運算效果。 ...
  • 對於手工計算來說,積分計算是非常困難的,對於一些簡單的函數,我們可以直接通過已知的積分公式來求解,但在更多的情況下,原函數並沒有簡單的表達式,因此確定積分的反函數變得非常困難。 另外,相對於微分運算來說,積分運算則具有更多的多樣性,包括不同的積分方法(如換元積分法、分部積分法等)和積分技巧,需要根據 ...
  • 一、概念 AOP面向切麵編程,一種編程範式 二、作用 在不改動原始設計(原代碼不改動)的基礎上為方法進行功能增強(即增加功能) 三、核心概念 1、代理(Proxy):SpringAOP的核心本質是採用代理模式實現的 2、連接點(JoinPoint):在SpringAOP中,理解為任意方法的執行 3、 ...
  • Go 方法集合與選擇receiver類型 目錄Go 方法集合與選擇receiver類型一、receiver 參數類型對 Go 方法的影響二、選擇 receiver 參數類型原則2.1 選擇 receiver 參數類型的第一個原則2.2 選擇 receiver 參數類型的第二個原則三、方法集合(Met ...
  • 線程池(重點) 線程池:三大方法、七大參數、四種拒絕策略 池化技術 程式的運行,本質:占用系統的資源!優化資源的使用!-> 池化技術(線程池、連接池、對象池......);創建和銷毀十分消耗資源 池化技術:事先準備好一些資源,有人要用就拿,拿完用完還給我。 線程池的好處: 1、降低資源消耗 2、提高 ...
  • 我們在類中通常會有一個屬性為 IsDel來表示軟刪除或也稱邏輯刪除,這個屬性會導致我們在進行查詢操作時,每一次都要 .where(s=>s.IsDel==false) 非常的麻煩。在使用efCore時可以通過配置查詢篩選器來很好的解決這個問題。 public class SysUser { publ ...
  • 搜索查找指令 find 指令 find指令將從指定目錄向下遞歸的遍歷其各個子目錄,將滿足條件的文件或者目錄顯示在終端。 基本語法 find [搜索範圍(指定目錄)] [選項] 選項說明 選項 功能 -name<查詢方式> 按照指定的文件名查找模式查找文件 -user<用戶名> 查找屬於指定用戶名所有 ...
  • rsyslog 介紹 日誌(Log)是記錄和存儲電腦、軟體、應用或其他系統的操作和事件的文件或數據流。它們可以為系統管理員、開發人員和最終用戶提供詳細的背景信息,以幫助他們瞭解和診斷系統的行為。 rsyslog 是一個開源的日誌處理工具,一般用在類Unix系統上,是syslogd 的擴展。它提供了 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...