10.多線程

預備知識

  1. 預備知識一

image-20230706223829634

  1. 預備知識二

image-20230707153747646

  1. 預備知識三

image-20230707223844738

  • 如何理解進程和線程的關系,舉一個生活中的例子

    • 家庭:進程
    • 家庭成員:線程

    每個家庭成員都會為這個家庭做貢獻,只不過大家都在做不同的事情(比如:我們在上學,父母在上班,爺爺奶奶打理屋子等等)

  • 如何證明以上我們所說的進程和線程的關系?

    • a.直接編寫代碼 ==> 見見豬跑
    • b.見下一個概念,論證上面的觀點

pthread_create()

功能:pthread_create - create a new thread// 頭文件
#include <pthread.h>// 函數
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);// 參數
pthread_t *thread : pthread_t 其本質就是一個整數
const pthread_attr_t *attr : 線程屬性,我們目前不管,直接設置為nullptrvoid *(*start_routine) (void *) : 其本質是一個函數指針,我們需要將線程所要調用的方法(也就是一個函數)的地址傳遞給這個函數指針。
void *arg :是void *(*start_routine) (void *)的參數,也就是所要調用的方法所需要傳遞的參數

makefile

//  -L指定庫的路徑,因為動態庫在系統的默認搜索路徑下,因此不需要去指定庫所在路徑
//  -l 后面加靜態庫/動態庫的名稱(名稱要去掉前綴lib,和后綴.a),指定庫的名稱
mythread:mythread.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f mythread

mythread.cc

#include <iostream>
#include <cstdio>
#include <cassert>
#include <pthread.h>
#include <unistd.h>using namespace std;// 新線程
void *thread_routine(void *args)
{const char *name = (const char *)args;while (true){cout << "我是新線程, 我正在運行! name: " << name <<  endl;sleep(1);}
}int main()
{// pthread_t其實就是一個無符號的長整型// typedef unsigned long int pthread_t;// tid 是線程idpthread_t tid;int n = pthread_create(&tid, nullptr, thread_routine, (void *)"thread one");assert(0 == n);(void)n;// 主線程while (true){char tidbuffer[64];snprintf(tidbuffer, sizeof(tidbuffer), "0x%x", tid);cout << "我是主線程, 我正在運行!, 我創建出來的線程的tid: " << tidbuffer << endl;sleep(1);}return 0;
}
  • 運行結果為:

    image-20230708202223604

  • 查看線程(輕量級進程)

    • 在Linux中,ps -aL是一個用于顯示進程信息的命令。具體來說,ps是用于報告當前運行進程的命令,-a選項表示顯示所有用戶的進程,而-L選項表示顯示每個線程的詳細信息。所以,ps -aL將顯示系統中所有用戶的所有線程的進程信息。

    image-20230708210155498

  • 線程一旦被創建,幾乎所有的資源都是被所有線程所共享的

// mythread.cc
#include <iostream>
#include <cstdio>
#include <cassert>
#include <pthread.h>
#include <unistd.h>using namespace std;// 是被主線程和新線程所共享的,一個線程修改這個變量,另一個線程立馬就可以看到對應的資源
int g_val = 0;// 主線程和新線程都可以調用這個方法
std::string fun()
{return "我是一個獨立的方法";// 新線程
void *thread_routine(void *args)
{const char *name = (const char *)args;while (true){fun();cout << "我是新線程, 我正在運行! name: " << name << " : "<< fun()  << " : " << g_val++ << " &g_val : " << &g_val << endl;sleep(1);}
}int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, thread_routine, (void *)"thread one");assert(0 == n);(void)n;// 主線程while (true){char tidbuffer[64];snprintf(tidbuffer, sizeof(tidbuffer), "0x%x", tid);cout << "我是主線程, 我正在運行!, 我創建出來的線程的tid: " << tidbuffer << " : " << g_val << " &g_val : " << &g_val << endl;sleep(1);}return 0;
}
// 運行結果如下:

image-20230708211821268

  • 線程也一定要有自己私有的資源,什么資源應該是線程私有的呢?
    • PCB屬性私有
    • 要有私有的上下文結構
    • 每一個進程都要有自己獨立的棧結構(如臨時變量就需要在棧結構上存儲,是屬于一個線程私有的資源)

什么是線程

  • 在一個程序里的一個執行路線就叫做線程(thread)。更準確的定義是:線程是“一個進程內部的控制序

列”

  • 一切進程至少都有一個執行線程

  • 線程在進程內部運行,本質是在進程地址空間內運行

  • 在Linux系統中,在CPU眼中,看到的PCB都要比傳統的進程更加輕量化

  • 透過進程虛擬地址空間,可以看到進程的大部分資源,將進程資源合理分配給每個執行流,就形成了線程

執行流

image-20230708213000657

線程的優點

  • 創建一個新線程的代價要比創建一個新進程小得多

    • 進程:創建PCB、進程地址空間、頁表、構建進程地址空間和物理空間的映射關系、加載代碼和數據等等。
    • 線程:只需要創建PCB分配資源就可以了。
  • 與進程之間的切換相比,線程之間的切換需要操作系統做的工作要少很多

    • 進程:切換頁表、切換地址空間、切換PCB、切換上下文等等
    • 線程:切換PCB、切換上下文
    • 線程切換cache只需要少量的數據更新,而進程切換,則需要全部更新

    image-20230708221500858

  • 線程占用的資源要比進程少很多

  • 能充分利用多處理器的可并行數量

  • 在等待慢速I/O操作結束的同時,程序可執行其他的計算任務

  • 計算密集型應用,為了能在多處理器系統上運行,將計算分解到多個線程中實現

    • 計算密集型應用(CPU、加密、解密、算法等),就比如對一個文件進行解壓,可以多個線程各自解壓這個文件中的一部分數據。
    • I/O密集型應用(外設、訪問磁盤、顯示器、網絡等)
  • I/O密集型應用,為了提高性能,將I/O操作重疊。線程可以同時等待不同的I/O操作。

線程的缺點

  • 性能損失

    • 一個很少被外部事件阻塞的計算密集型線程往往無法與其它線程共享同一個處理器。如果計算密集型線程的數量比可用的處理器多,那么可能會有較大的性能損失,這里的性能損失指的是增加了額外的同步和調度開銷,而可用的資源不變。(簡單來說就是一個CPU如果是單核,那么一個線程的性能是最佳的,如果是多核那么可以有多個線程,所謂的核我們可以理解為是CPU內部的運算器,而控制器只有一個)
  • 健壯性降低

    • 編寫多線程需要更全面更深入的考慮,在一個多線程程序里,因時間分配上的細微偏差或者因共享了

      不該共享的變量而造成不良影響的可能性是很大的,換句話說線程之間是缺乏保護的。

  • 缺乏訪問控制

    • 進程是訪問控制的基本粒度,在一個線程中調用某些OS函數會對整個進程造成影響。
  • 編程難度提高

    • 編寫與調試一個多線程程序比單線程程序困難得多

線程異常

  • 單個線程如果出現除零,野指針問題導致線程崩潰,進程也會隨著崩潰

  • 線程是進程的執行分支,線程出異常,就類似進程出異常,進而觸發信號機制,終止進程,進程終止,該

進程內的所有線程也就隨即退出

線程用途

  • 合理的使用多線程,能提高CPU密集型程序的執行效率

  • 合理的使用多線程,能提高IO密集型程序的用戶體驗(如生活中我們一邊寫代碼一邊下載開發工具,就是

多線程運行的一種表現)

Linux進程VS線程

  • 進程是資源分配的基本單位

  • 線程是調度的基本單位

  • 線程共享進程數據,但也擁有自己的一部分數據:

    • 線程ID
    • 一組寄存器
    • errno
    • 信號屏蔽字
    • 調度優先級

進程的多個線程共享同一地址空間,因此Text Segment、Data Segment都是共享的,如果定義一個函數,在各線程

中都可以調用,如果定義一個全局變量,在各線程中都可以訪問到,除此之外,各線程還共享以下進程資源和環境:

  • 文件描述符表
  • 每種信號的處理方式(SIG_ IGN、SIG_ DFL或者自定義的信號處理函數)
  • 當前工作目錄
  • 用戶id和組id

進程和線程的關系如下圖:

image-20230708225841949

驗證線程的健壯性

  • static_cast的用法

static_cast 是 C++ 中用于進行安全的強制類型轉換的操作符。它可以在編譯時檢查類型轉換的安全性,并提供了一些類型轉換的功能。

static_cast 可以用于以下幾種類型轉換:

  1. 基本數據類型之間的轉換:例如,將整數類型轉換為浮點數類型,或者將浮點數類型轉換為整數類型。
int num = 10;
double d = static_cast<double>(num); // 將整數轉換為浮點數
  1. 類層次結構中的指針或引用類型的轉換:例如,將基類指針或引用轉換為派生類指針或引用。
class Base {};
class Derived : public Base {};Base* basePtr = new Derived();
Derived* derivedPtr = static_cast<Derived*>(basePtr); // 將基類指針轉換為派生類指針
  1. 隱式轉換的類型轉換:例如,將枚舉類型轉換為整數類型。
enum Color { RED, GREEN, BLUE };
int num = static_cast<int>(RED); // 將枚舉類型轉換為整數類型

需要注意的是,static_cast 并不能執行所有類型之間的轉換,它只能執行編譯器認為是安全的轉換。如果進行不安全的轉換,編譯器可能會給出警告或錯誤。

另外,對于類層次結構中的指針或引用類型的轉換,如果轉換不是合法的,即基類指針或引用指向的對象實際上不是派生類的對象,那么 static_cast 將無法進行安全的轉換。在這種情況下,可以考慮使用 dynamic_cast 進行運行時類型檢查和轉換。

makefile

mythread:mythread.ccg++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:rm -f mythread

mythread

  • 一個線程如果出現了異常,會影響其他線程嗎?
  • 會的,一個線程異常退出,會導致其他線程也被退出(這種情況稱為:線程的健壯性或者魯棒性較差)
  • 為什么會導致其他線程也退出呢?
  • 這是因為:進程信號,信號是整體發給進程的,而在一個進程中的所有線程都會退出
  • 使用下面的代碼來進行演示:
#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;void *start_routine(void *args)
{string name = static_cast<const char*>(args); // 安全的進行強制類型轉化while (true){cout << "new thread create success, name: " << name << endl;int *p = nullptr;*p = 0;  // 此處指針異常}
}int main()
{pthread_t id;  // 線程idpthread_create(&id, nullptr, start_routine, (void *)"thread one");while(true){cout << "new thread create success, name: main thread" << endl;sleep(1);}return 0;
}

image-20230709154300206

Linux線程控制

  • POSIX線程庫

    • 與線程有關的函數構成了一個完整的系列,絕大多數函數的名字都是以“pthread_”打頭的
    • 要使用這些函數庫,要通過引入頭文<pthread.h>
    • 鏈接這些線程函數庫時要使用編譯器命令的“-lpthread”選項
  • 創建線程

功能:創建一個新的線程 原型 :int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void (*start_routine)(void*), void *arg);參數 thread:返回線程ID attr:設置線程的屬性,attr為NULL表示使用默認屬性 start_routine:是個函數地址,線程啟動后要執行的函數 arg:傳給線程啟動函數的參數 返回值:成功返回0;失敗返回錯誤碼

錯誤檢查:

  • 傳統的一些函數是,成功返回0,失敗返回-1,并且對全局變量errno賦值以指示錯誤。

  • pthreads函數出錯時不會設置全局變量errno(而大部分其他POSIX函數會這樣做)。而是將錯誤代碼通過返回值返回

  • pthreads同樣也提供了線程內的errno變量,以支持其它使用errno的代碼。對于pthreads函數的錯誤,建議通過返回值業判定,因為讀取返回值要比讀取線程內的errno變量的開銷更小

創建多個線程

makefile

mythread:mythread.ccg++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:rm -f mythread

mythread.cc

#include <iostream>
#include <cstdlib>
#include <string>
#include <cassert>
#include <vector>
#include <pthread.h>
#include <unistd.h>using namespace std;// 當成結構體使用(都是共有的資源)
class ThreadData
{
public:int number;pthread_t tid;char namebuffer[64];
};// 1. start_routine, 現在是被幾個線程執行呢?
// 目前有10個線程都在執行這個函數, 這個函數現在是重入狀態
// 2. 該函數是可重入函數嗎?
// 是的,根據可重入函數的定義,這個函數被重復進入,但是并沒有出現二義性(也就是沒有出現問題)
// 3. 在函數內定義的變量,都叫做局部變量,具有臨時性 
// 在多線程情況下, 也是同樣使用的,這些局部變量獨屬于它的線程, 也從側面驗證了其實每一個線程都有自己獨立的棧結構
void *start_routine(void *args)
{ThreadData *td = static_cast<ThreadData *>(args); // 安全的進行強制類型轉化int cnt = 5;while (cnt){cout << "cnt: " << cnt << " &cnt: " << &cnt << endl;cnt--;sleep(1);}// 釋放我們new的資源(存儲)delete td;return nullptr; 
}int main()
{// 1. 創建一批線程,將創建線程的相關信息放入到threads中vector<ThreadData*> threads;
#define NUM 10for(int i = 0; i < NUM; i++){ThreadData *td = new ThreadData();td->number = i+1;snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);pthread_create(&td->tid, nullptr, start_routine, td);// 將線程id、線程名、和線程編號(也就是結構體td)存儲到vector<ThreadData*> threads中threads.push_back(td);}// 打印創建的新線程的名字,和線程idfor(auto &iter : threads){cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " suceesss" << endl;}while (true){cout << "new thread create success, name: main thread" << endl;sleep(1);}return 0;
}

image-20230710152827261

線程終止

  • exit() : 是不能夠用來終止線程的,因為exit()是用來終止進程的,任何一個執行流調用exit()都會讓整個進程退出。
  • 線程執行完,返回nullptr,也可以終止線程
  • 使用pthread_exit()終止線程

pthread_exit()

功能:pthread_exit - terminate calling thread  // 終止要調用的線程頭文件:#include <pthread.h>// 函數
void pthread_exit(void *retval);// 參數
void *retval :是新線程退出后的退出結果,如果不關心這個,那么直接設置為nullptr就可以了

mythread.cc

#include <iostream>
#include <cstdlib>
#include <string>
#include <cassert>
#include <vector>
#include <pthread.h>
#include <unistd.h>using namespace std;class ThreadData
{
public:int number;pthread_t tid;char namebuffer[64];
};void *start_routine(void *args)
{ThreadData *td = static_cast<ThreadData *>(args); // 安全的進行強制類型轉化int cnt = 5;while (cnt){cout << "cnt: " << cnt << " &cnt: " << &cnt << endl; cnt--;sleep(1);// 將其放在這里方便直觀的看出線程退出,只是用來觀察// 退出線程的方法一// return nullptr; // 退出線程的方法二// pthread_exit(nullptr);      }delete td;  pthread_exit(nullptr);
}int main()
{vector<ThreadData*> threads;
#define NUM 10for(int i = 0; i < NUM; i++){ThreadData *td = new ThreadData();td->number = i+1;snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);pthread_create(&td->tid, nullptr, start_routine, td);threads.push_back(td);}for(auto &iter : threads){cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " suceesss" << endl;}while (true){cout << "new thread create success, name: main thread" << endl;sleep(1);}return 0;
}
  • 退出線程的方法一和方法二的打印結果

image-20230710161437129

線程等待

  • 線程也是需要被等待的,如果不等待,也會造成類似于僵尸進程的問題(內存泄漏,也就是PCB沒有被回收)
    • 獲取新線程的退出信息
    • 回收新線程對應的PCB等內核資源,防止內存泄漏

pthread_join

功能:pthread_join - join with a terminated thread// 頭文件
#include <pthread.h>// 函數
int pthread_join(pthread_t thread, void **retval);// 參數
pthread_t thread :線程id
void **retval :輸出型參數,用來獲取線程結束時,返回的退出結果// 返回值
success, pthread_join() returns 0; on error, it returns an error number.

mythread.cc

#include <iostream>
#include <cstdlib>
#include <string>
#include <cassert>
#include <vector>
#include <pthread.h>
#include <unistd.h>using namespace std;class ThreadData
{
public:int number;pthread_t tid;char namebuffer[64];
};void *start_routine(void *args)
{ThreadData *td = static_cast<ThreadData *>(args); // 安全的進行強制類型轉化int cnt = 5;while (cnt){cout << "cnt: " << cnt << " &cnt: " << &cnt << endl; cnt--;sleep(1);}pthread_exit(nullptr);
}int main()
{vector<ThreadData*> threads;
#define NUM 10for(int i = 0; i < NUM; i++){ThreadData *td = new ThreadData();td->number = i+1;snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);pthread_create(&td->tid, nullptr, start_routine, td);threads.push_back(td);}for(auto &iter : threads){cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " suceesss" << endl;}// 等待回收新線程for(auto &iter : threads){int n = pthread_join(iter->tid, nullptr); assert(n == 0);cout << "join : " << iter->namebuffer << " success " << endl;delete iter;}while (true){cout << "new thread create success, name: main thread" << endl;sleep(1);}return 0;
}

image-20230710221827783

線程的返回值

mythread.cc

#include <iostream>
#include <cstdlib>
#include <string>
#include <cassert>
#include <vector>
#include <pthread.h>
#include <unistd.h>using namespace std;class ThreadData
{
public:int number;pthread_t tid;char namebuffer[64];
};class ThreadReturn
{
public:int exit_code;int exit_result;
};void *start_routine(void *args)
{ThreadData *td = static_cast<ThreadData *>(args); // 安全的進行強制類型轉化int cnt = 5;while (cnt){cout << "cnt: " << cnt << " &cnt: " << &cnt << endl; cnt--;sleep(1);}// 因為返回值的類型為void*,因此我們將其強轉為void*類型// Linux為64位,因此其指針是8byte,而整型是4byte,所以編譯時,此處會報warning// 相當于 void* ret = (void*)td->numberreturn (void*)td->number;// 使用pthread_exit也可以返回退出結果// pthread_exit((void*)321);// 也可以返回堆空間的地址,或者是對象的地址, // 但是不能夠返回在棧上面創建的結構對象,因為棧幀被銷毀,這個對象也就不存在了,所以一定要new,在堆空間上創建這個對象// ThreadReturn * tr = new ThreadReturn();// tr->exit_code = 1;// tr->exit_result = 321;// return (void*)tr;
}int main()
{vector<ThreadData*> threads;
#define NUM 10for(int i = 0; i < NUM; i++){ThreadData *td = new ThreadData();td->number = i+1;snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);pthread_create(&td->tid, nullptr, start_routine, td);threads.push_back(td);}for(auto &iter : threads){cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " suceesss" << endl;}for(auto &iter : threads){void* ret = nullptr;// pthread_join的第二個參數是輸出型參數// void** retp = &ret;// 解引用,就可以拿到新線程的返回值,也就是線程結束后,返回的退出結果// *retp,也就是ret(對二級指針解引用,指向其存儲的一級指針變量)// *retp = return (void*)td->numberint n = pthread_join(iter->tid, &ret); assert(n == 0);// 此處的ret必須強轉為long long類型而不是int類型,因為64位的指針是8字節的cout << "join : " << iter->namebuffer << " success,number : " << (long long)ret << endl;delete iter;}// 針對堆空間返回值的打印,因為堆空間返回的是結構體對象的地址,因此接收返回值的參數的變量的類型也要被修改,具體如下面這段代碼所示// for(auto &iter : threads)// {//     ThreadReturn* ret = nullptr;//     int n = pthread_join(iter->tid, (void**)&ret); //     assert(n == 0);//     // 此處的ret必須強轉為long long類型而不是int類型,因為64位的指針是8字節的//     cout << "join : " << iter->namebuffer << " success,exit_code: " << ret->exit_result << endl;//     delete iter;// }while (true){cout << "new thread create success, name: main thread" << endl;sleep(1);}return 0;
}
  • 退出結果,返回過程的理解

image-20230710231020529

  • 程序運行的結果

image-20230710231552548

線程取消(終止)

pthread_cancel

功能:pthread_cancel - send a cancellation request to a thread頭文件: #include <pthread.h>// 函數 
int pthread_cancel(pthread_t thread);

pthread.cc

#include <iostream>
#include <cstdlib>
#include <string>
#include <cassert>
#include <vector>
#include <pthread.h>
#include <unistd.h>using namespace std;class ThreadData
{
public:int number;pthread_t tid;char namebuffer[64];
};void *start_routine(void *args)
{ThreadData *td = static_cast<ThreadData *>(args); // 安全的進行強制類型轉化int cnt = 5;while (cnt){cout << "cnt: " << cnt << " &cnt: " << &cnt << endl; cnt--;sleep(1);}return (void*)321;
}int main()
{vector<ThreadData*> threads;
#define NUM 10for(int i = 0; i < NUM; i++){ThreadData *td = new ThreadData();td->number = i+1;snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);pthread_create(&td->tid, nullptr, start_routine, td);threads.push_back(td);}for(auto &iter : threads){cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " suceesss" << endl;}// 線程是可以被cancel取消的// 注意:線程要被取消,前提是這個線程已經跑起來了// 線程如果是被取消的,退出碼:-1sleep(5);for(int i = 0; i < threads.size()/2; i++){pthread_cancel(threads[i]->tid);cout << "pthread_cancel : " << threads[i]->namebuffer << " success" << endl;}for(auto &iter : threads){void* ret = nullptr;int n = pthread_join(iter->tid, (void**)&ret); assert(n == 0);cout << "join : " << iter->namebuffer << " success,exit_code: " << (long long)ret << endl;delete iter;}while (true){cout << "new thread create success, name: main thread" << endl;sleep(1);}return 0;
}

image-20230711135304080

c++11接口(線程)

#include <iostream>
#include <unistd.h>
#include <thread>void thread_run()
{while (true){std::cout << "我是新線程..." << std::endl;sleep(1);}
}int main()
{// 任何語言,在linux中如果要實現多線程,必定要是用pthread庫// 如何看待C++11中的多線程呢?// C++11 的多線程,在Linux環境中,本質是對pthread庫的封裝!std::thread t1(thread_run);while (true){std::cout << "我是主線程..." << std::endl;sleep(1);}t1.join();return 0;
}

image-20230711141443095

分離線程

  • 默認情況下,新創建的線程是joinable的,線程退出后,需要對其進行pthread_join操作,否則無法釋放資源,從而造成系統泄漏。

  • 如果不關心線程的返回值,join是一種負擔,這個時候,我們可以告訴系統,當線程退出時,自動釋放線程資源。

pthread_self()

功能:pthread_self - obtain ID of the calling thread  // 獲取調用線程的id// 頭文件
#include <pthread.h>// 函數
pthread_t pthread_self(void);// 返回值
This function always succeeds, returning the calling thread's ID.

pthread_detach()

功能:pthread_detach - detach a thread   // 分離一個線程頭文件: #include <pthread.h>// 函數
int pthread_detach(pthread_t thread);

mythread.cc

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <pthread.h>std::string changId(const pthread_t &thread_id)
{char tid[128];snprintf(tid, sizeof(tid), "0x%x", thread_id);return tid;
}void* start_routine(void* args)
{std::string  threadname = static_cast<const char*>(args);int cnt = 5;while(cnt--){// 當新線程調用pthread_self(),則得到的返回值就是新線程的線程idstd::cout << threadname << " running......,其線程id為 "<< changId(pthread_self()) << std::endl;sleep(1); }
}int main()
{// 輸出型參數,是創建的新線程的線程idpthread_t tid;pthread_create(&tid, nullptr, start_routine, (void*)"thread 1");// 可以用主線程分離新線程,也可以用新線程分離自己,效果是一樣的// 此處,為了在主線程運行到pthread_join()前,就讓主線程檢測到新線程就被分離,所以在此處用主線程分離新線程// 分離創建的新線程,分離新線程之后,主線程就不需要再等待回收新線程的資源了// 新線程運行結束之后,OS會自動釋放新線程的資源pthread_detach(tid);// 主線程調用ptherad_self(),則返回值為主線程的idstd::string main_id = changId(pthread_self());// tid為創建的新線程的idstd::cout << "main thread running......,其創建的新線程id為 : " << changId(tid) << ";  mian thread id : " << main_id << std::endl;// 等待回收創建的新線程// 一個線程默認是joinable的,如果設置了分離狀態,就不能夠進行等待了,否則就會等待失敗// pthread_join()返回錯誤碼int n = pthread_join(tid, nullptr);std::cout << "result" << n << " : " << strerror(n) << std::endl;while(1){std::cout << "main thread running......" << std::endl;sleep(1);}return 0;
}

image-20230711155315863

解析線程id

image-20230711170259321

線程局部存儲

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <pthread.h>// g_val為線程局部存儲
// 添加__thread, 可以將一個內置類型(如:int,char)設置為線程局部存儲
// 在線程庫中,線程結構體中存在線程局部存儲,將這個變量設置為局部存儲,也就是將這個變量給每一個線程都設置一份到對應線程的線程局部存儲中
// 雖然這個變量依舊是全部變量,但是每個線程之間是不會相互影響的
__thread int g_val = 100;// g_val為全部變量
// int g_val = 100;std::string changId(const pthread_t &thread_id)
{char tid[128];snprintf(tid, sizeof(tid), "0x%x", thread_id);return tid;
}void* start_routine(void* args)
{std::string  threadname = static_cast<const char*>(args);while(true){// 當新線程調用pthread_self(),則得到的返回值就是新線程的線程idstd::cout << threadname << " running......,其線程id為 "<< changId(pthread_self()) << "; g_val " << (int)g_val << " &g_val " << &g_val << std::endl;sleep(1); g_val++;}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, start_routine, (void*)"thread 1");while(true){std::cout << "main thread running.....;  g_val :" << (int)g_val << " &g_val :" << &g_val << std::endl;sleep(1);}return 0;
}
  • 當int g_val為全局變量時

image-20230711171705244

  • 當 g_val為線程局部存儲中的變量時

image-20230711172446625

線程的封裝

Thread.hpp

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <functional>
#include  <pthread.h>// 聲明這個類,因為在class Context中使用了class Thread,但是此時class Thread還沒有被創建
class Thread;//上下文,當成一個大號的結構體
class Context
{
public:Thread *this_;   // Thread類的this指針void *args_;     // 新線程將要調用函數方法的參數
public:// 構造函數Context():this_(nullptr), args_(nullptr){}// 析構函數~Context(){}
};class Thread
{
public:// 定義函數的類型為func_t,函數的參數和返回值的類型都為void*typedef std::function<void*(void*)> func_t;// 存儲線程名字數組的容量const int num = 1024;public:   // 構造函數(構造一個線程)// 參數為,線程需要調用的方法func_t func, 這個方法需要傳遞的參數void *args = nullptr, // 存儲線程名字數組的容量Thread(func_t func, void *args = nullptr, int number = 0): func_(func), args_(args){char buffer[num];snprintf(buffer, sizeof buffer, "thread-%d", number);name_ = buffer;// 開始創建線程// 新線程所要執行的方法// 在類內創建線程,想讓線程執行對應的方法,需要將方法設置成為static// 為什么需要將其設置為static呢?// 這是因為此時的start_routine是類內成員,除了一個顯示的參數void *args之外,還有一個參數是this指針(也就是實例對象的地址)// 因此如果在類內調用這個函數,是需要通過將類的實例指針作為參數傳遞給線程創建函數。// 靜態成員函數在類的所有對象實例之間共享,它們沒有this指針,因此可以直接通過類名調用。// 這使得靜態成員函數可以在沒有類的實例的情況下執行。// 解決方案// 通過定義結構體class Context,將this指針,還有參數args_都定義在里面start_routine的兩個參數// 這樣傳遞參數時,只需要通過一個結構體對象,就可以調用Context *ctx = new Context();ctx->this_ = this;ctx->args_ = args_;// 創建線程的系統調用函數為pthread_create()// 通過傳遞對象ctx可以同時將this指針和start_routine線程調用方法的參數進行傳遞int n = pthread_create(&tid_, nullptr, start_routine, ctx);assert( n==0 );// 編譯debug的方式發布的時候存在,release方式發布,assert就不存在了,n就是一個定義了,但是沒有被使用的變量// 在有些編譯器下會有warning(void)n;}// 靜態方法不能夠調用成員變量或者成員方法,/* 如果需要在靜態方法中訪問成員變量或成員方法,可以通過以下兩種方式實現:1.將成員變量或成員方法聲明為靜態:將需要在靜態方法中訪問的成員變量或成員方法聲明為靜態,這樣它們就可以在靜態方法中直接訪問,因為它們屬于類而不是類的實例。2.創建類的實例并調用成員變量或成員方法:在靜態方法中創建類的實例,并使用該實例來訪問成員變量或成員方法。通過實例化類,就可以獲得對非靜態成員的訪問權限。需要注意的是,靜態方法中只能直接訪問靜態成員,而不能直接訪問非靜態成員。*/static void *start_routine(void *args) {// 將args安全的強轉為Context *類型Context *ctx = static_cast<Context *>(args);// 通過實例來訪問成員變量或成員方法。通過實例化類,就可以獲得對非靜態成員的訪問權限。void* ret = ctx->this_->run(ctx->args_);// 釋放ctx的空間delete ctx;return ret;}void join(){int n = pthread_join(tid_, nullptr);assert(n == 0);(void)n;}void *run(void *args){// 運行線程將要執行的方法func_t funcreturn func_(args);}~Thread(){//do nothing}private:std::string name_;  // 線程名func_t func_;       // 新線程所要調用的方法void *args_;        // 新線程所要調用的方法的函數的參數,也就是func_的參數pthread_t tid_;     // 新線程的線程id
};

mythread.cc

#include "Thread.hpp"
#include <memory>void* thread_run(void* args)
{std::string work_type = static_cast<const char*>(args);while(true){std::cout << "我是一個新線程,我正在做: " << work_type << std::endl;sleep(1);}return nullptr;
}int main()
{std::unique_ptr<Thread> thread(new Thread1(thread_run, (void*)"hellothread", 1));std::unique_ptr<Thread> thread(new Thread2(thread_run, (void*)"countthread", 2));std::unique_ptr<Thread> thread(new Thread3(thread_run, (void*)"logthread", 3));thread1->join();thread2->join();thread3->join();return 0;
}

image-20230716210635910

5. Linux線程互斥

購買火車票

usleep()

功能:usleep - suspend execution for microsecond intervals   // 以微秒為間隔暫停執行// 頭文件
#include <unistd.h>// 函數    
int usleep(useconds_t usec);

mythread.cc

// 使用我們線程封裝的Thread.hpp
#include "Thread.hpp"// 全局變量,多個線程的共享資源
// tickets代表火車票的剩余數量
int tickets = 10000;void* getTicket(void* args)
{std::string username = static_cast<const char*>(args);while(true){if(tickets > 0){// 只有當剩余票數大于0,搶票才是有意義的std::cout << username << "正在進行搶票" << tickets-- << std::endl;// 1秒 = 1000毫秒 = 1000 000 微秒 = 10^9納秒// usleep()是以微秒為單位的// 用這段時間來模擬真實的搶票要花費的時間usleep(1000);}else{// 當沒有票時,那么就直接跳出循環break;}}return nullptr;
}int main()
{std::unique_ptr<Thread> thread1(new Thread(getTicket, (void*)"user1", 1));std::unique_ptr<Thread> thread2(new Thread(getTicket, (void*)"user2", 2));std::unique_ptr<Thread> thread3(new Thread(getTicket, (void*)"user3", 3));std::unique_ptr<Thread> thread4(new Thread(getTicket, (void*)"user4", 4));thread1->join();thread2->join();thread3->join();thread4->join();return 0;
}
  • 運行結果

image-20230717182717284

  • 此時,我們想要模擬一種搶票的極端環境(就是讓多個線程不是串聯的執行搶票,而是并聯的執行),那么就需要盡可能的讓多個線程交叉執行(也就是并聯執行)

    • 所謂的串聯執行就是一個線程執行完之后,另一個線程再繼續執行,而并聯則是幾個線程同時進行執行

    • 多個線程交叉執行的本質:就是讓調度器盡可能的頻繁發生線程調度與切換

    • 線程一般在時間片到了、來了更高優先級的線程、線程等待的時候發生線程切換。

    • 線程是在什么時候檢測上面的問題呢? ==》從內核態返回用戶態的時候,線程要對調度狀態進行檢測,如果可以,就直接發生線程切換

#include "Thread.hpp"int tickets = 10000;void* getTicket(void* args)
{std::string username = static_cast<const char*>(args);while(true){// 串聯執行的話,某一個線程將票搶完之后,下一個線程會檢測到tickets已經不大于0了,因此不會進入// 4個線程是不可以同時執行判斷ticket > 0的,假設只有一個cpu,那么在任何一個時刻,只允許有一個線程來執行這個判斷// 判斷的本質邏輯: 1.讀取內存數據到cpu的寄存器當中  2.進行判斷if(tickets > 0){// 當線程在進行搶票前,先讓這個線程進行休眠,那么這個線程就會被CPU切換走// 這樣當多個線程都執行到這里都被休眠的話,當休眠過后,哪個線程先被喚醒是由調度器決定的// 是一個不確定的結果,這樣就可以保證多個線程交叉執行了(也就是并聯執行)usleep(1000);std::cout << username << "正在進行搶票" << tickets-- << std::endl;}else{break;}}return nullptr;
}int main()
{std::unique_ptr<Thread> thread1(new Thread(getTicket, (void*)"user1", 1));std::unique_ptr<Thread> thread2(new Thread(getTicket, (void*)"user2", 2));std::unique_ptr<Thread> thread3(new Thread(getTicket, (void*)"user3", 3));std::unique_ptr<Thread> thread4(new Thread(getTicket, (void*)"user4", 4));thread1->join();thread2->join();thread3->join();thread4->join();return 0;
}
  • 運行結果如下:

image-20230719141131606

  • 原因如下圖所示:

    • 圖1image-20230719144829257
    • 圖2image-20230719145605596
  • 我們定義的全局變量,在沒有保護的時候,往往是不安全的,像上面多個線程在交替執行造成的數據安全問題,發生了數據不一致問題。

    • 提出解決方案:加鎖
  • 補充知識點:

    • 1.多個執行流進行安全訪問的共享資源,我們將其稱為臨界資源
    • 2.我們把多個執行流中,訪問臨界資源的代碼稱為臨界區(臨界區往往是線程代碼的很小的一部分)
    • 3.想讓多個線程串行訪問共享資源,是需要多個線程之間存在互斥的(也就是只有當一個執行流訪問完臨界資源之后,另一個線程才可以對其進行訪問)
    • 4.對一個資源進行訪問的時候,要么不做,要么做完,我們將這種行為稱為原子性
    • 5.如上述我們所說進行tickets–,對應的匯編語言是三條,但是在執行完兩條匯編語言之后,線程就被切換走了,像這種沒有執行完對應匯編語言的行為,這就不是原子性的。基于這些,我們對原子性下一個定義: 如果只用一條匯編語言就可以完成,就稱為原子性,反之就不是原子的(當前理解,方便表述)。

加鎖

pthread_mutex_init()

// 功能 
pthread_mutex_destroy, pthread_mutex_init - destroy and initialize a mutex // 銷毀并初始化互斥對象// mutex n.互斥// 頭文件
#include <pthread.h>// 如果這把鎖是局部的,那么必須使用init來初始化,用destory來銷毀
int pthread_mutex_destroy(pthread_mutex_t *mutex);// pthread_mutex_t *restrict mutex 是需要初始化的鎖
// const pthread_mutexattr_t *restrict attr 是屬性,目前設置為nullptr就可以
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);// 如果這把鎖是全局的或者是靜態的,那么我們只需要用如下的方式來對鎖初始化,而不需要對其進行銷毀
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock()

// 功能
pthread_mutex_lock, pthread_mutex_trylock, pthread_mutex_unlock - lock and unlock a mutex  // ,鎖定和解鎖互斥鎖// 頭文件
#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);     // 加鎖
int pthread_mutex_trylock(pthread_mutex_t *mutex);	// 嘗試申請加鎖,如果加鎖成功,那么就會擁有鎖,如果不成功,則會出錯返回
int pthread_mutex_unlock(pthread_mutex_t *mutex);   // 解鎖

mythread.cc

#include "Thread.hpp"// 設置一個全局的鎖 或者靜態的鎖,只需要用如下的方式將其進行初始化,且不需要對其進行銷毀
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;int tickets = 10000;void* getTicket(void* args)
{std::string username = static_cast<const char*>(args);while(true){// 對臨界區的資源進行加鎖pthread_mutex_lock(&lock);if(tickets > 0){usleep(1000);std::cout << username << "正在進行搶票" << tickets-- << std::endl;// 對臨界區資源進行解鎖pthread_mutex_unlock(&lock);}else{// 對臨界區資源進行解鎖,必須在break之前進行解鎖,如果執行了break,跳出了循環,那么將無法進行解鎖pthread_mutex_unlock(&lock);break;}}return nullptr;
}int main()
{std::unique_ptr<Thread> thread1(new Thread(getTicket, (void*)"user1", 1));std::unique_ptr<Thread> thread2(new Thread(getTicket, (void*)"user2", 2));std::unique_ptr<Thread> thread3(new Thread(getTicket, (void*)"user3", 3));std::unique_ptr<Thread> thread4(new Thread(getTicket, (void*)"user4", 4));thread1->join();thread2->join();thread3->join();thread4->join();return 0;
}
  • 運行結果

image-20230719165456771

  • 設置一個局部的鎖
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
#include  <pthread.h>
#include <memory>
#include <unistd.h> int tickets = 10000;class ThreadData
{
public:// 構造函數ThreadData(const std::string &threadname, pthread_mutex_t *mutex):threadname_(threadname),mutex_p_(mutex){}// 析構函數~ThreadData(){}public:std::string threadname_;  // 線程名字pthread_mutex_t *mutex_p_; // 鎖
};void* getTicket(void* args)
{ThreadData *td = static_cast<ThreadData *>(args);while(true){// 加鎖和解鎖的過程,多個線程是串行執行的,因此程序的運行速度就變慢了// 鎖只規定互斥訪問,沒有規定讓那個線程優先執行// 因此,那個線程先申請到鎖,這是多個執行流進行競爭的結果// 對臨界區的資源進行加鎖pthread_mutex_lock(td->mutex_p_);if(tickets > 0){usleep(1000);std::cout << td->threadname_ << "正在進行搶票" << tickets << std::endl;tickets--;// 對臨界區資源進行解鎖pthread_mutex_unlock(td->mutex_p_);}else{// 對臨界區資源進行解鎖,必須在break之前進行解鎖,如果執行了break,跳出了循環,那么將無法進行解鎖pthread_mutex_unlock(td->mutex_p_);break;}// 搶完票之后,還要形成一個訂單發送給用戶,因此停頓一下來模擬這個過程// 當我們這個停頓一下,對應的線程就會被切換走,就不會出現一個線程(執行流)一直在搶票的情況了usleep(1000);}return nullptr;
}int main()
{
#define NUM 4pthread_mutex_t lock;// 對于局部的鎖,我們需要用init對其進行初始化,解鎖之后我們要使用destory對其進行銷毀pthread_mutex_init(&lock, nullptr);// 將創建的線程id放入tids中;  pthread_t是unsigned long int(無符號的長整型)// 初始化NUM個pthread_t的空間std::vector<pthread_t> tids(NUM);for(int i = 0; i < NUM; i++){char buffer[64];// 將線程名字格式化到buffer中snprintf(buffer, sizeof(buffer), "thread %d", i);// 通過td這個對象,我們就可以同時傳遞線程名字,和線程所需要的鎖兩個參數ThreadData *td = new ThreadData(buffer, &lock);// tids[i]是一個輸出型參數,是線程idpthread_create(&tids[i], nullptr, getTicket, td);}// 等待回收線程資源for(const auto &tid : tids){pthread_join(tid, nullptr);}// 局部的鎖被解鎖之后,還需要被銷毀pthread_mutex_destroy(&lock);return 0;
}
  • 運行結果如下:
    • image-20230719180324909

深層理解鎖(內涵鎖的封裝)

  1. 如何看待鎖?
  • a.鎖,本身就是一個共享資源,全局的變量是要被保護的,鎖是用來保護全局的資源的,但是鎖本身也是全局資源,那么鎖的安全是誰來保護呢?

  • b.pthread_mutex_lock,pthread_mutex_unlock:加鎖和解鎖的過程必須是安全的。因為加鎖和解鎖的過程是原子的,因此其本質就是安全的(后續會說到)

  • c. 如果申請鎖申請成功,那么就繼續向后執行,如果鎖申請暫時沒有成功,那么執行流將會如何?(執行流會被阻塞)

  • #include <iostream>
    #include <vector>
    #include <string>
    #include <cstring>
    #include  <pthread.h>
    #include <memory>
    #include <unistd.h> // 設置一個全局的鎖 或者靜態的鎖,只需要用如下的方式將其進行初始化,且不需要對其進行銷毀
    pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;int tickets = 10000;void* getTicket(void* args)
    {std::string username = static_cast<const char*>(args);while(true){// 對臨界區的資源進行加鎖pthread_mutex_lock(&lock);// 如果我們在申請一把鎖之后,我們再申請一把鎖那么會怎么樣呢?// 我們會發現程序運行阻塞,原因我們后續再說pthread_mutex_lock(&lock);if(tickets > 0){usleep(1000);std::cout << username << "正在進行搶票" << tickets-- << std::endl;// 對臨界區資源進行解鎖pthread_mutex_unlock(&lock);}else{// 對臨界區資源進行解鎖,必須在break之前進行解鎖,如果執行了break,跳出了循環,那么將無法進行解鎖pthread_mutex_unlock(&lock);break;}// 搶完票之后,還要形成一個訂單發送給用戶,因此停頓一下來模擬這個過程// 當我們這個停頓一下,對應的線程就會被切換走,就不會出現一個線程(執行流)一直在搶票的情況了usleep(1000);}return nullptr;
    }int main()
    {pthread_t t1, t2, t3, t4;pthread_create(&t1, nullptr, getTicket, (void *)"thread 1");pthread_create(&t2, nullptr, getTicket, (void *)"thread 2");pthread_create(&t3, nullptr, getTicket, (void *)"thread 3");pthread_create(&t4, nullptr, getTicket, (void *)"thread 4");pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);pthread_join(t4, nullptr);return 0;
    }
    
  • 運行結果如下:

  • image-20230719205355439

  • d. 哪個線程持有鎖,哪個線程才可以進入臨界區

  • image-20230719214055833

  1. 如何理解加鎖和解鎖的本質?

    • 加鎖的過程是原子的
    • 經過上面的例子,我們已經意識到單純的 i++ 或者 ++i 都不是原子的,有可能會有數據一致性問題
    • 為了實現互斥鎖操作,大多數體系結構都提供了swapexchange指令,該指令的作用是把寄存器和內存單元的數據相交換,由于只有一條指令,保證了原子性。
    • image-20230720163335064
  2. 如果我們想簡單的使用鎖,該如何進行封裝設計?

makefile

mythread:mythread.ccg++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:rm -f mythread

Mutex.hpp

#pragma once#include <iostream>
#include <pthread.h>class Mutex
{
public:// 構造函數Mutex(pthread_mutex_t *lock_p = nullptr):lock_p_(lock_p){}// 加鎖void lock(){// 如果lock_p_不是空指針,這說明已經傳遞進來了一把鎖了if(lock_p_)pthread_mutex_lock(lock_p_);   // 進行加鎖}// 解鎖void unlock(){if(lock_p_)pthread_mutex_unlock(lock_p_);   // 進行解鎖}// 析構函數~Mutex(){}
private:pthread_mutex_t *lock_p_;
};class LockGuard
{
public:LockGuard(pthread_mutex_t *mutex):mutex_(mutex){mutex_.lock(); // 在構造函數中進行加鎖}~LockGuard(){mutex_.unlock(); // 在析構函數中進行解鎖}private:Mutex mutex_;
};

mythread.cc

#include "Mutex.hpp" 
#include <unistd.h>// 設置一個全局的鎖 或者靜態的鎖,只需要用如下的方式將其進行初始化,且不需要對其進行銷毀
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;int tickets = 10000;void* getTicket(void* args)
{std::string username = static_cast<const char*>(args);while(true){// RAII風格的加鎖 : 資源獲取即初始化(Resource Acquisition Is Initialization)// lockgaurd是定義在while循環中的一個對象// 在創建這個對象時,會調用這個這個對象的構造函數對臨界區進行加鎖// 當運行到lockguard的生命周期結束,則會調用析構函數來解鎖// lockguard的生命周期就是while的一次循環,while每一次循環都會創建一個LockGuard對象LockGuard lockguard(&lock);if(tickets > 0){usleep(1000);std::cout << username << "正在進行搶票" << tickets-- << std::endl;}else{break;}// 搶完票之后,還要形成一個訂單發送給用戶,因此停頓一下來模擬這個過程usleep(1000);}return nullptr;
}int main()
{pthread_t t1, t2, t3, t4;pthread_create(&t1, nullptr, getTicket, (void *)"thread 1");pthread_create(&t2, nullptr, getTicket, (void *)"thread 2");pthread_create(&t3, nullptr, getTicket, (void *)"thread 3");pthread_create(&t4, nullptr, getTicket, (void *)"thread 4");pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);pthread_join(t4, nullptr);return 0;
}

運行結果如下:

image-20230720174708907

可重入VS線程安全

概念

  • 線程安全:多個線程并發同一段代碼時,不會出現不同的結果。常見對全局變量或者靜態變量進行操作,并且沒有鎖保護的情況下,會出現該問題。

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

常見的線程不安全的情況

  • 不保護共享變量的函數
  • 函數狀態隨著被調用,狀態發生變化的函數
  • 返回指向靜態變量指針的函數
  • 調用線程不安全函數的函數

常見的線程安全的情況

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

常見不可重入的情況

  • 調用了malloc/free函數,因為malloc函數是用全局鏈表來管理堆的
  • 調用了標準I/O庫函數,標準I/O庫的很多實現都以不可重入的方式使用全局數據結構
  • 可重入函數體內使用了靜態的數據結構

常見可重入的情況

  • 不使用全局變量或靜態變量
  • 不使用用malloc或者new開辟出的空間
  • 不調用不可重入函數
  • 不返回靜態或全局數據,所有數據都有函數的調用者提供
  • 使用本地數據,或者通過制作全局數據的本地拷貝來保護全局數據

可重入與線程安全聯系

  • 函數是可重入的,那就是線程安全的
  • 函數是不可重入的,那就不能由多個線程使用,有可能引發線程安全問題
  • 如果一個函數中有全局變量,那么這個函數既不是線程安全的也不是可重入的。

可重入與線程安全區別

  • 可重入函數是線程安全函數的一種

  • 線程安全不一定是可重入的,而可重入函數則一定是線程安全的。

  • 如果將對臨界資源的訪問加上鎖,則這個函數是線程安全的,但如果這個重入函數的鎖還未釋放則會產生死鎖,因此是不可重入的。

6. 常見鎖概念

死鎖

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

  • 舉一個簡單的例子:你有一塊錢,你朋友也有一塊錢,你們都想要對方的一塊錢且你們都不想給對方自己的一塊錢,然而你們相互不停的詢問對方要這一塊錢,則雙方都處于一種永久的等待狀態。

死鎖四個必要條件

  • 互斥條件:一個資源每次只能被一個執行流使用
  • 請求與保持條件:一個執行流因請求資源而阻塞時,對已獲得的資源保持不放
  • 不剝奪條件:一個執行流已獲得的資源,在末使用完之前,不能強行剝奪
  • 循環等待條件:若干執行流之間形成一種頭尾相接的循環等待資源的關系

避免死鎖

  • 破壞死鎖的四個必要條件
  • 加鎖順序一致
  • 避免鎖未釋放的場景
  • 資源一次性分配

image-20230720195017758
ullptr, getTicket, (void *)“thread 1”);
pthread_create(&t2, nullptr, getTicket, (void *)“thread 2”);
pthread_create(&t3, nullptr, getTicket, (void *)“thread 3”);
pthread_create(&t4, nullptr, getTicket, (void *)“thread 4”);

pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_join(t4, nullptr);return 0;

}


運行結果如下:[外鏈圖片轉存中...(img-z56XRtLR-1743561038766)]# 可重入VS線程安全## 概念> - 線程安全:多個線程并發同一段代碼時,不會出現不同的結果。常見對全局變量或者靜態變量進行操作,并且沒有鎖保護的情況下,會出現該問題。
>
> - 重入:同一個函數被不同的執行流調用,當前一個流程還沒有執行完,就有其他的執行流再次進入,我們稱之為重入。一個函數在重入的情況下,運行結果不會出現任何不同或者任何問題,則該函數被稱為可重入函數,否則,是不可重入函數。## 常見的線程不安全的情況> - 不保護共享變量的函數
> - 函數狀態隨著被調用,狀態發生變化的函數
> - 返回指向靜態變量指針的函數
> - 調用線程不安全函數的函數## **常見的線程安全的情況**> - 每個線程對全局變量或者靜態變量只有讀取的權限,而沒有寫入的權限,一般來說這些線程是安全的
> - 類或者接口對于線程來說都是原子操作
> - 多個線程之間的切換不會導致該接口的執行結果存在二義性## **常見不可重入的情況**> - 調用了malloc/free函數,因為malloc函數是用全局鏈表來管理堆的
> - 調用了標準I/O庫函數,標準I/O庫的很多實現都以不可重入的方式使用全局數據結構
> - 可重入函數體內使用了靜態的數據結構## **常見可重入的情況**> - 不使用全局變量或靜態變量
> - 不使用用malloc或者new開辟出的空間
> - 不調用不可重入函數
> - 不返回靜態或全局數據,所有數據都有函數的調用者提供
> - 使用本地數據,或者通過制作全局數據的本地拷貝來保護全局數據## **可重入與線程安全聯系**> - 函數是可重入的,那就是線程安全的
> - 函數是不可重入的,那就不能由多個線程使用,有可能引發線程安全問題
> - 如果一個函數中有全局變量,那么這個函數既不是線程安全的也不是可重入的。## **可重入與線程安全區別**> - 可重入函數是線程安全函數的一種
>
> - 線程安全不一定是可重入的,而可重入函數則一定是線程安全的。
>
> - 如果將對臨界資源的訪問加上鎖,則這個函數是線程安全的,但如果這個重入函數的鎖還未釋放則會產生死鎖,因此是不可重入的。# **6.** **常見鎖概念**## **死鎖**> - 死鎖是指在一組進程中的各個進程均占有不會釋放的資源,但因互相申請被其他進程所占用不會釋放的資源而處于的一種永久等待狀態。
>
> - 舉一個簡單的例子:你有一塊錢,你朋友也有一塊錢,你們都想要對方的一塊錢且你們都不想給對方自己的一塊錢,然而你們相互不停的詢問對方要這一塊錢,則雙方都處于一種永久的等待狀態。## 死鎖四個必要條件> - 互斥條件:一個資源每次只能被一個執行流使用
> - 請求與保持條件:一個執行流因請求資源而阻塞時,對已獲得的資源保持不放
> - 不剝奪條件:一個執行流已獲得的資源,在末使用完之前,不能強行剝奪
> - 循環等待條件:若干執行流之間形成一種頭尾相接的循環等待資源的關系## 避免死鎖> - 破壞死鎖的四個必要條件
> - 加鎖順序一致
> - 避免鎖未釋放的場景
> - 資源一次性分配[外鏈圖片轉存中...(img-llIEaloi-1743561038767)]

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/76539.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/76539.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/76539.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Python入門(8):文件

1. 文件基本概念 文件&#xff1a;存儲在計算機上的數據集合&#xff0c;Python 通過文件對象來操作文件。 文件類型&#xff1a; 文本文件&#xff1a;由字符組成&#xff0c;如 .txt, .py 二進制文件&#xff1a;由字節組成&#xff0c;如 .jpg, .mp3 2. 文件打開與關閉…

市場交易策略優化與波動管理

市場交易策略優化與波動管理 在市場交易中&#xff0c;策略的優化和波動的管理至關重要。市場價格的變化受多種因素影響&#xff0c;交易者需要根據市場環境動態調整策略&#xff0c;以提高交易的穩定性&#xff0c;并有效規避市場風險。 一、市場交易策略的優化方法 趨勢交易策…

HTTP數據傳輸的幾個關鍵字Header

本文著重針對http在傳輸數據時的幾種封裝方式進行描述。 1. Content-Type(描述body內容類型以及字符編碼) HTTP的Content-Type用于定義數據傳輸的媒體類型&#xff08;MIME類型&#xff09;&#xff0c;主要分為以下幾類&#xff1a; (一)、?基礎文本類型? text/plain? …

面向教育領域的實時更新RAG系統:核心模塊設計與技術選型實踐指南

目錄 面向教育領域的實時更新RAG系統&#xff1a;核心模塊設計與技術選型實踐指南 一、業務需求分析 二、系統架構設計&#xff08;核心模塊&#xff09; 三、核心模塊詳解與技術選型建議 &#xff08;一&#xff09;實時更新向量知識庫 &#xff08;二&#xff09;教材與…

k8s patch方法更新deployment和replace方法更新deployment的區別是什么

在Kubernetes中&#xff0c;patch 和 replace 方法用于更新資源&#xff08;如 Deployment&#xff09;&#xff0c;但它們的實現方式和適用場景有顯著差異。以下是兩者的核心區別&#xff1a; 1. 更新范圍 replace 方法 完全替換整個資源配置。需要用戶提供完整的資源定義&…

解決安卓手機WebView無法直接預覽PDF的問題(使用PDF.js方案)

在移動端開發中&#xff0c;通過 webview 組件直接加載PDF文件時&#xff0c;不同平臺的表現差異較大&#xff1a; iOS & 部分安卓瀏覽器&#xff1a;可正常內嵌預覽&#xff08;依賴系統內置PDF渲染能力&#xff09; 大多數安卓設備&#xff1a;由于缺乏原生PDF插件&…

基于javaweb的SSM+Maven機房管理系統設計與實現(源碼+文檔+部署講解)

技術范圍&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬蟲、數據可視化、小程序、安卓app、大數據、物聯網、機器學習等設計與開發。 主要內容&#xff1a;免費功能設計、開題報告、任務書、中期檢查PPT、系統功能實現、代碼編寫、論文編寫和輔導、論文…

7-6 混合類型數據格式化輸入

本題要求編寫程序&#xff0c;順序讀入浮點數1、整數、字符、浮點數2&#xff0c;再按照字符、整數、浮點數1、浮點數2的順序輸出。 輸入格式&#xff1a; 輸入在一行中順序給出浮點數1、整數、字符、浮點數2&#xff0c;其間以1個空格分隔。 輸出格式&#xff1a; 在一行中…

【GPIO8個函數解釋】

函數解釋 void GPIO_DeInit(GPIO_TypeDef* GPIOx); 作用&#xff1a;將指定GPIO端口的所有寄存器恢復為默認值。這會清除之前對該端口的所有配置&#xff0c;使其回到初始狀態。使用方法&#xff1a;傳入要復位的GPIO端口指針&#xff0c;例如GPIOA、GPIOB等。 void GPIO_AF…

將圖表和表格導出為PDF的功能

<template><div><divref"pdfContent"style"position: relative; width: 800px; margin: 0 auto"><!-- ECharts 圖表 --><div id"chart" style"width: 100%; height: 400px" /><!-- Element UI 表格 …

C++中的鏈表操作

在C中&#xff0c;鏈表是一種常見的數據結構&#xff0c;它由一系列節點組成&#xff0c;每個節點包含數據部分和指向下一個節點的指針。C標準庫&#xff08;STL&#xff09;中提供了std::list和std::forward_list兩種鏈表實現&#xff0c;分別對應雙向鏈表和單向鏈表。此外&am…

蛋白設計 ProteinMPNN

傳統方法的局限性是什么&#xff1f; 傳統蛋白質設計方法的局限性&#xff1a; 基于物理的傳統方法&#xff0c;例如羅塞塔&#xff0c;面臨計算難度&#xff0c;因為需要計算所有可能結構的能量&#xff0c;包括不需要的寡聚態和聚合態。 設計目標與顯式優化之間缺乏一致性通…

有哪些開源的視頻生成模型

1. 阿里巴巴通義萬相2.1&#xff08;WanX 2.1&#xff09; 技術架構&#xff1a;基于Diffusion Transformer&#xff08;DiT&#xff09;架構&#xff0c;結合自研的高效變分自編碼器&#xff08;VAE&#xff09;和Flow Matching訓練方案&#xff0c;支持時空上下文建模。參數…

【動態規劃】最長上升子序列模板

最長上升子序列 題目傳送門 一、題目描述 給定一個長度為 N 的數列&#xff0c;求數值嚴格單調遞增的子序列的長度最長是多少。 輸入格式 第一行包含整數 N。 第二行包含 N 個整數&#xff0c;表示完整序列。 輸出格式 輸出一個整數&#xff0c;表示最大長度。 數據范圍 …

LeetCode 891 -- 貢獻度思想

題目描述 子序列寬度之和 思路 ref 代碼 相似題 子數組范圍和 acwing

化工行業如何通過定制化工作流自動化實現25-30%成本優化?

作者&#xff1a;Mihir Jhaveri 編譯&#xff1a;李升偉 發布日期&#xff1a;2024年10月30日 在化工生產領域&#xff0c;數字化轉型正以顛覆性態勢重塑產業格局。通過集成定制化軟件、ERP系統、工業物聯網&#xff08;IIoT&#xff09;傳感網絡、機器人流程自動化&#xff0…

Compose組件轉換XML布局

文章目錄 學習JetPack Compose資源前言&#xff1a;預覽界面的實現Compose組件的布局管理一、Row和Colum組件&#xff08;LinearLayout&#xff09;LinearLayout&#xff08;垂直方向 → Column&#xff09;LinearLayout&#xff08;水平方向 → Row&#xff09; 二、相對布局 …

RAG測試數據集資源

一、通用問答基準數據集 HotpotQA 特點:包含11萬+多跳問答對最佳用途:測試復雜推理能力數據示例:{"question": "Were Scott Derrickson and Ed Wood of the same nationality?","answer": "Yes, both are American" }MS MARCO 特點…

快速掌握MCP——Spring AI MCP包教包會

最近幾個月AI的發展非常快&#xff0c;各種大模型、智能體、AI名詞和技術和框架層出不窮&#xff0c;作為一個業余小紅書博主的我最近總刷到MCP這個關鍵字&#xff0c;看著有點高級我也來學習一下。 1.SpringAI與functionCall簡單回顧 前幾個月我曾寫過兩篇關于SpringAI的基礎…

學習筆記--(6)

import numpy as np import matplotlib.pyplot as plt from scipy.special import erfc# 設置參數 rho 0.7798 z0 4.25 # 確保使用大寫 Z0&#xff0c;與定義一致def calculate_tau(z, z_prime, rho, s_values):return np.log(rho * z * z_prime * s_values / 2)# 定義 chi_…