線程式控制制

来源:https://www.cnblogs.com/songhe364826110/archive/2019/09/13/11432412.html
-Advertisement-
Play Games

[TOC] 1. 線程與進程 線程的概念 線程是進程內相對獨立的一個執行流,是進程內的一個執行單元,是操作系統中一個可調度的實體。 深入理解進程和線程 在現代操作系統中,資源分配的基本單位是進程,而CPU調度執行的基本單位是線程 進程不是調度單元,線程是進程使用CPU資源的基本單位 進程有獨立的地址 ...


目錄

1. 線程與進程

線程的概念

線程是進程內相對獨立的一個執行流,是進程內的一個執行單元,是操作系統中一個可調度的實體。

深入理解進程和線程

  • 在現代操作系統中,資源分配的基本單位是進程,而CPU調度執行的基本單位是線程
  • 進程不是調度單元,線程是進程使用CPU資源的基本單位
  • 進程有獨立的地址空間,進程中可以存在多個線程共用進程資源
  • 線程不能脫離進程單獨存在,只能依附於進程運行
  • 線程可以在不影響進程的情況下終止,但反之則不然

2. 多線程

什麼是多線程

多線程,是指從軟體或硬體層面上實現多個線程併發執行的技術。

  • 從軟體層面看,一個進程中可以有多個線程,該程式也可以稱之為多線程程式;
  • 從硬體層面看,多核處理器能夠支持在同一時間執行多個線程。

實際上,對於單核處理器,即使軟體編寫為多線程模型,同一時間也只能執行一個線程,但這並不代表此時多線程就沒有意義,因為處理器的數量不會影響程式結構,那麼多線程編程模型在程式結構上到底有哪些好處呢?

多線程模型的好處

  • 通過為每種事件類型分配單獨的處理線程,可以簡化非同步事件處理代碼
  • 可以直接共用進程的數據資源
  • 將複雜問題分解為相互獨立的任務,可以交叉進行,提高程式吞吐量
  • 通過把處理用戶輸入輸出的部分和其他部分分開,可以改善互動式程式響應時間

3. 線程標識

  • 線程ID(Thread ID)是線程的唯一標識
  • 線程ID只有在它所屬的進程上下文中才有意義
  • 線程ID類型為pthread_t,可能實現為unsigned long或結構體,依系統而定
  • 線程可以調用pthread_self獲得自身線程ID
  • 可移植的程式應該調用pthread_equal來比較兩個線程的ID
#include <pthread.h>

pthread_t pthread_self();  //返回調用線程的線程ID
int pthread_equal(pthread_t tid1, pthread_t tid2); //相等返回非0數值,否則返回0

4. 線程創建

函數原型

任意線程可以通過調用pthread_create創建新線程,start_routine為新線程的啟動常式,創建成功後,新線程和調用線程誰先運行是不確定的。

//成功返回0,失敗返回錯誤編號
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); 

參數說明

  • pthread_create成功返回後,tid指向記憶體即為新線程ID
  • attr用於定製線程屬性,若使用預設屬性則傳NULL
  • start_routine是線程啟動常式
  • arg是start_routine的參數,若參數不止一個,就把這些參數放到一個結構中,再把該結構的地址作為arg傳入

使用示例-列印線程ID

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

pthread_t tid;

void printf_tid(const char *s)
{
    pid_t     pid = getpid();
    pthread_t tid = pthread_self();    
       
    printf("%s pid = %d, tid = %lu(0x%lx)\n", s, pid, tid, tid);
}

void *pthread_start(void *arg)
{
    printf_tid("new thread:  "); //新線程用pthread_self()獲取自身ID,是因為新線程執行時pthread_create()可能還未返回,tid還未初始化完成
}

int main()
{     
    pthread_create(&tid, NULL, pthread_start, NULL);
    sleep(1); //調用線程休眠1S,讓新線程先執行
    printf_tid("main thread: ");
    
    return 0;
}

註意:使用pthread的代碼在編譯時需要指定鏈接-lpthread

5. 線程終止

在不影響整個進程的情況下,單個線程有三種終止方式:

  • 線上程啟動常式中調用return返回
  • 線上程啟動常式中調用pthread_exit退出
  • 被進程中的其他線程取消
void pthread_exit(void *value_ptr);

value_ptr是一個無類型指針,進程中的其他線程可以調用pthread_join訪問到這個指針。

6. 線程等待

函數原型

int pthread_join(pthread_t tid, void **value_ptr); //成功返回0,失敗返回錯誤編號

調用線程將一直阻塞,直到等待的線程以上述三種方式終止。

參數說明

  • tid表示等待的線程ID
  • value_ptr用於保存線程的退出狀態
  • 如果線程以return或pthread_exit方式終止,value_ptr指向記憶體就被設置為return或pthread_exit的參數
  • 如果線程被取消,value_ptr指向記憶體就被設置為PTHREAD_CANCELED
  • 如果不關心線程的返回值,就給value_ptr傳NULL

使用示例-獲得線程返回值

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

void *thread1_start(void *arg)
{
    int ret = 0;
    return ((void *)ret);
}

void *thread2_start(void *arg)
{
    char *ret = "thread 2 exit";
    pthread_exit(ret);
}

int main()
{     
    pthread_t tid1;
    pthread_t tid2;
    void *ret;
    
    pthread_create(&tid1, NULL, thread1_start, NULL);
    pthread_create(&tid2, NULL, thread2_start, NULL);
    
    pthread_join(tid1, &ret);
    printf("thread 1: %d\n", (int)ret); 
    
    pthread_join(tid2, &ret);
    printf("thread 2: %s\n", (char *)ret);
    
    return 0;
}

7. 線程分離

在預設情況下,線程的終止狀態會一直保存到對該線程調用pthread_join;但是,如果線程已經被分離,其占用的系統資源會線上程終止時被立即回收。
有兩種方式可以使線程分離:

  • 調用pthread_detach,該函數不會使調用線程阻塞
  • 修改線程屬性結構pthread_attr_t,以分離狀態創建線程

線上程被分離後,就不能再用pthread_join等待它的終止狀態了,因為對分離狀態的線程調用pthread_join會產生未定義行為。

pthread_detach

int pthread_detach(pthread_t tid); //成功返回0,失敗返回錯誤編號
#include <pthread.h>
#include <stdio.h>

void *thread_start(void *arg)
{
    sleep(2);
    printf("new thread exit\n");
    pthread_exit(NULL);
}

int main()
{     
    pthread_t tid;
 
    pthread_create(&tid, NULL, thread_start, NULL);
    pthread_detach(tid);
    
    printf("main thread: pthread_detach() return\n");
    sleep(5);
    
    return 0;
}

以分離狀態創建線程

/*4個函數的返回值:成功返回0,失敗返回錯誤編號*/

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); 
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate); 
int pthread_attr_destroy(pthread_attr_t *attr);

可以調用pthread_attr_setdetachstate來設置線程的可分離狀態:

  • detachstate = PTHREAD_CREATE_DETACHED,以分離狀態啟動線程
  • detachstate = PTHREAD_CREATE_JOINABLE,以正常狀態啟動線程
#include <pthread.h>
#include <stdio.h>

void *thread_start(void *arg)
{
    sleep(2);
    printf("new thread exit\n");
    pthread_exit(NULL);
}

int main()
{     
    pthread_t tid;
    pthread_attr_t attr;

    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);   
    pthread_create(&tid, &attr, thread_start, NULL);
    pthread_attr_destroy(&attr);
    
    printf("main thread: pthread_attr_destroy() return\n");
    sleep(5);
    
    return 0;
}

8. 線程取消

pthread_cancel

在編寫多線程代碼時,經常面臨線程安全退出問題,一般情況下,最好使用將標誌位置位的方式;
在其他線程中將標誌位置位,然後調用pthread_join等待線程退出,回收線程占用的資源。

void *thread_start(void *arg)
{
    while (!quit)
    {
        //......
    }
}

int main()
{
    quit = 1;
    pthread_join(tid, NULL);
}

但是在某些應用中,線程可能正阻塞於某個函數(如pthread_cond_wait)無法被喚醒,即使設置了標誌位也無法結束。
此時可以在其他線程中調用pthread_cancel請求取消線程,然後立即調用pthread_join等待線程退出。

int pthread_cancel(pthread_t tid); //成功返回0,失敗返回錯誤編號

tid為要取消的線程ID,需要註意的是,pthread_cancel並不等待線程終止,它僅僅是發出請求。

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

void *thread1_start(void *arg)
{
    sleep(10);
    pthread_exit(NULL);
}

void *thread2_start(void *arg)
{
    sleep(10);
    pthread_exit(NULL);
}

int main()
{     
    pthread_t tid1;
    pthread_t tid2;
    
    pthread_create(&tid1, NULL, thread1_start, NULL);
    pthread_create(&tid2, NULL, thread2_start, NULL);
    
    sleep(1);
    
    pthread_cancel(tid1);
    pthread_join(tid1, NULL);
    printf("thread 1 exit\n");
     
    sleep(1);
    
    pthread_cancel(tid2);
    pthread_join(tid2, NULL);
    printf("thread 2 exit\n");
    
    return 0;
}

線程取消屬性

線程取消有兩個屬性,分別是可取消狀態可取消類型,這兩個屬性不在pthread_attr_t結構中,但它們影響著線程在響應取消請求時的行為。

/*
 * 可取消狀態:PTHREAD_CANCEL_ENABLE(允許取消,預設屬性),PTHREAD_CANCEL_DISABLE(不允許取消,但取消請求不會丟失,而是一直處於掛起狀態)
 * 可取消類型:PTHREAD_CANCEL_DEFERRED(延遲取消,到達取消點才取消,預設屬性),PTHREAD_CANCEL_ASYNCHRONOUS(非同步取消,可在任意時刻取消)
*/
int pthread_setcancelstate(int state, int *oldstate);  //將線程可取消狀態設為state,原有可取消狀態通過oldstate返回,這兩步是原子操作
int pthread_setcanceltype(int type, int *oldtype);     //將線程可取消類型設為type,原有可取消類型通過oldtype返回

取消點

預設情況下,線程的可取消類型為延遲取消,也就是說:被取消的線程在取消請求發出後還是繼續運行,直到到達某個取消點。
取消點是線程檢查它是否被取消的一個位置,根據《UNIX環境高級編程 第3版》P362-P363描述,POSIX.1定義的取消點和可選取消點如下。

自定義取消點

如果線程在很長一段時間內都不會調用前面兩張圖中的取消點函數,那麼可以調用pthread_testcancel線上程中添加自己的取消點。
調用pthread_testcancel時,如果有某個取消請求處於掛起狀態,且可取消狀態為ENABLE,那麼線程就會被取消。

void pthread_testcancel();

使用線程取消的風險

當線程響應取消請求而終止時,主要面臨的兩大風險:

  • 線程裡面的鎖可能沒有unlock,有可能導致死鎖
  • 線程申請的資源(如堆記憶體)沒有釋放

下麵是一段由pthread_cancel引起的死鎖範例代碼。

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

static pthread_cond_t  cond;
static pthread_mutex_t mutex;

void *thread0(void *arg)
{ 
    pthread_mutex_lock(&mutex);    
    printf("thread 0 lock sucess\n");
    pthread_cond_wait(&cond, &mutex); //主線程發出取消請求時,thread1阻塞於slepp(2),thread0阻塞於此取消點,導致thread0未解鎖mutex就終止
    printf("thread 0 pthread_cond_wait return\n"); 
    pthread_mutex_unlock(&mutex); 
    
    pthread_exit(0);
}

void *thread1(void *arg)
{
    sleep(2);   
    
    printf("thread 1 start lock\n");
    pthread_mutex_lock(&mutex);       //thread0終止約1S後,thread1執行到此,由於mutex已加鎖,也沒有其他地方能夠對其解鎖,從而導致死鎖
    printf("thread 1 lock sucess\n");      
    pthread_cond_signal(&cond);    
    pthread_mutex_unlock(&mutex); 
    
    pthread_exit(0);  
}

int main()
{     
    pthread_t tid[2];
    
    pthread_cond_init(&cond, NULL);
    pthread_mutex_init(&mutex, NULL);
    
    pthread_create(&tid[0], NULL, thread0, NULL);
    pthread_create(&tid[1], NULL, thread1, NULL);

    sleep(1);  
    pthread_cancel(tid[0]);
    printf("main thread request cancel thread 0\n");

    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
  
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    
    return 0;
}

線程清理程式

可以使用線程清理程式來解決線程取消的風險問題。線程可以安排它退出時需要調用的函數,這樣的函數稱為線程清理程式。
一個線程可以註冊多個清理程式,處理程式記錄在棧中,也就是說,它們的執行順序和註冊順序是相反的。

void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

當線程執行以下動作時,清理程式是由pthread_cleanup_push函數調度的,調用時只有一個參數arg:

  • 調用pthread_exit結束線程
  • 響應pthread_cancel取消請求
  • 用非零execute參數調用pthread_cleanup_pop

註意:如果線程以return方式終止,線程清理程式不會被調用。

不管發生上述哪種情況,pthread_cleanup_pop都將刪除上次pthread_cleanup_push登記的線程清理程式。
這兩個函數有一個限制,由於它們經常實現為巨集,所以必須在與線程啟動常式相同的作用域中以配對的方式使用,否則,可能會產生編譯錯誤。

回到線程取消的風險問題上來,我們只需要線上程清理程式中解鎖和釋放資源,併在線程啟動常式的第一步就註冊清理程式
這樣,當線程因響應取消請求而終止時,線程清理程式就會得以執行。

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

static pthread_cond_t  cond;
static pthread_mutex_t mutex;

void cleanup(void *arg)
{
    pthread_mutex_unlock(&mutex); 
    printf("mutex unlock in cleanup\n");
}

void *thread0(void *arg)
{
    pthread_cleanup_push(cleanup, NULL); //註冊線程清理程式進行解鎖
    
    pthread_mutex_lock(&mutex);    
    printf("thread 0 lock sucess\n");
    pthread_cond_wait(&cond, &mutex);    //主線程發出取消請求時,thread1阻塞於slepp(2),thread0阻塞於此取消點
    printf("thread 0 pthread_cond_wait return\n"); 
    pthread_mutex_unlock(&mutex); 
    
    pthread_cleanup_pop(0);
    pthread_exit(0);
}

void *thread1(void *arg)
{
    sleep(2);   
    
    printf("thread 1 start lock\n");
    pthread_mutex_lock(&mutex);          //thread0終止約1S後,thread1執行到此
    printf("thread 1 lock sucess\n");      
    pthread_cond_signal(&cond);    
    pthread_mutex_unlock(&mutex); 
    
    pthread_exit(0);  
}

int main()
{     
    pthread_t tid[2];
    
    pthread_cond_init(&cond, NULL);
    pthread_mutex_init(&mutex, NULL);
    
    pthread_create(&tid[0], NULL, thread0, NULL);
    pthread_create(&tid[1], NULL, thread1, NULL);

    sleep(1);  
    pthread_cancel(tid[0]);
    printf("main thread request cancel thread 0\n");

    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
  
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    
    return 0;
}

最後,引用一篇由pthread_cancel引起死鎖的博客https://blog.csdn.net/xsckernel/article/details/48052425,提取核心內容如下:

“通常的說法:某某函數是Cancellation Points,這種方法是容易令人混淆的。因為函數的執行是一個時間過程,而不是一個時間點。其實真正的Cancellation Points
只是在這些函數中Cancellation Type被修改為PHREAD_CANCEL_ASYNCHRONOUS和修改回PTHREAD_CANCEL_DEFERRED中間的一段時間。”


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

-Advertisement-
Play Games
更多相關文章
  • using System;using System.Collections.Generic;using System.Configuration;using System.Data;using System.Data.SqlClient;using System.Linq;using System. ...
  • 場景 在同一個Winform窗體中,點擊一個Button按鈕時, 獲取同窗體的其他控制項的屬性。 首先需要對要獲取的控制項賦予Name屬性,然後就可以通過Name進行獲取。 實現 在Button的點擊事件中: ...
  • 在開發過程中,可能會要使用Win7 ,Win8 ,Win10等不同版本的系統去做相容性調試,也有時候會去針對特別的顯卡,無線網卡等等硬體設備的機器做優化,有一種較優的方案,那就是使用Visual Studio的遠程調試功能。 ...
  • 我用的的是VS2019 步驟1:打開VS→工具→Android→Android SDK 管理器 安裝平臺的 Android 9.0-pie下的Android SDK Platform 28 和 Google APIs Intel x86 Atom System Image 工具里的 Android ...
  • 題目:古典問題:有一對兔子,從出生後第3個月起每個月都生一對兔子,小兔子長到第三個月後每個月又生一對兔子,假如兔子都不死,問每個月的兔子總數為多少?程式分析: 兔子的規律為數列1,1,2,3,5,8,13,21…. ...
  • .NET 分散式自增Id生成組件,基於雪花Id改進版,簡潔易用 ...
  • [20190913]完善vim的bccacl插件2.txt--//繼續完善vim的bccacl插件。--//\bc 計算也可以直接使用 \bb 操作,這樣操作更快一些。--//增加直接寫好算式計算的快捷\cc(註不用輸入最後等號),這步直接調用bc,這樣算式沒有問題,都能計算正確。"" calcul ...
  • 我個人對可重入函數的理解如下: 可重入函數需要指明可重入對象,分為線程可重入函數和信號可重入函數 線程可重入函數是可以被多個線程同時調用、且保證安全的函數,也叫做線程安全函數 信號可重入函數是在信號處理程式中保證調用安全的函數,也叫做非同步信號安全函數 下圖是APUE書中所說的POSIX.1中不能保證 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...