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 8、WPF、Prism.DryIoc、MVVM設計模式、Blazor以及MySQL資料庫構建的企業級工作流系統的WPF客戶端框架-AIStudio.Wpf.AClient 6.0。 項目介紹 框架採用了 Prism 框架來實現 MVVM 模式,不僅簡化了 MVVM 的典型 ...
  • 先看一下效果吧: 我們直接通過改造一下原版的TreeView來實現上面這個效果 我們先創建一個普通的TreeView 代碼很簡單: <TreeView> <TreeViewItem Header="人事部"/> <TreeViewItem Header="技術部"> <TreeViewItem He ...
  • 1. 生成式 AI 簡介 https://imp.i384100.net/LXYmq3 2. Python 語言 https://imp.i384100.net/5gmXXo 3. 統計和 R https://youtu.be/ANMuuq502rE?si=hw9GT6JVzMhRvBbF 4. 數 ...
  • 本文為大家介紹下.NET解壓/壓縮zip文件。雖然解壓縮不是啥核心技術,但壓縮性能以及進度處理還是需要關註下,針對使用較多的zip開源組件驗證,給大家提供個技術選型參考 之前在《.NET WebSocket高併發通信阻塞問題 - 唐宋元明清2188 - 博客園 (cnblogs.com)》講過,團隊 ...
  • 之前寫過兩篇關於Roslyn源生成器生成源代碼的用例,今天使用Roslyn的代碼修複器CodeFixProvider實現一個cs文件頭部註釋的功能, 代碼修複器會同時涉及到CodeFixProvider和DiagnosticAnalyzer, 實現FileHeaderAnalyzer 首先我們知道修 ...
  • 在軟體行業,經常會聽到一句話“文不如表,表不如圖”說明瞭圖形在軟體應用中的重要性。同樣在WPF開發中,為了程式美觀或者業務需要,經常會用到各種個樣的圖形。今天以一些簡單的小例子,簡述WPF開發中幾何圖形(Geometry)相關內容,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 在 C# 中使用 RabbitMQ 通過簡訊發送重置後的密碼到用戶的手機號上,你可以按照以下步驟進行 1.安裝 RabbitMQ 客戶端庫 首先,確保你已經安裝了 RabbitMQ 客戶端庫。你可以通過 NuGet 包管理器來安裝: dotnet add package RabbitMQ.Clien ...
  • 1.下載 Protocol Buffers 編譯器(protoc) 前往 Protocol Buffers GitHub Releases 頁面。在 "Assets" 下找到適合您系統的壓縮文件,通常為 protoc-{version}-win32.zip 或 protoc-{version}-wi ...
  • 簡介 在現代微服務架構中,服務發現(Service Discovery)是一項關鍵功能。它允許微服務動態地找到彼此,而無需依賴硬編碼的地址。以前如果你搜 .NET Service Discovery,大概率會搜到一大堆 Eureka,Consul 等的文章。現在微軟為我們帶來了一個官方的包:Micr ...
  • ZY樹洞 前言 ZY樹洞是一個基於.NET Core開發的簡單的評論系統,主要用於大家分享自己心中的感悟、經驗、心得、想法等。 好了,不賣關子了,這個項目其實是上班無聊的時候寫的,為什麼要寫這個項目呢?因為我單純的想吐槽一下工作中的不滿而已。 項目介紹 項目很簡單,主要功能就是提供一個簡單的評論系統 ...