Linux多線程——線程控制

目錄

1.線程知識補充

1.1 線程私有資源

1.2 線程共享資源

1.3 原生線程庫

2、線程控制接口

2.1 線程創建

2.1.1 一批線程

2.2 線程等待

2.3 線程終止

?2.4 線程實戰

2.5 其他接口

2.5.1 關閉線程pthread_cancel

2.5.2 獲取線程 ID pthread_self

2.5.3 線pthread_detach

3. 深入理解線程

3.1 理解線程庫及線程 ID

3.2 理解線程獨立棧

3.3 理解線程局部存儲

🌇前言
線程是進程內的基本執行單位,作為 CPU 執行的基本單位,線程的控制與任務執行效率息息相關。合理地進行線程管理,能大大提升程序的執行效率,掌握線程的基本操作至關重要。

🏙?正文

1.線程知識補充

在深入討論線程控制接口之前,我們首先需要補充一些關于線程的基礎知識。Linux?中沒有真線程,只有復用?PCB?設計思想的?TCB?結構

1.1 線程私有資源

在 Linux 的多線程實現中,線程本質上是輕量級進程(LWP),即通過復用 PCB 設計的 TCB 結構來模擬線程因此,盡管 Linux 系統中的多個線程共享同一進程的地址空間,但每個線程仍然需要一定的獨立性和資源

線程私有資源具體包括:

  • 線程 ID:線程的唯一標識符,由內核管理

  • 寄存器:每個線程的上下文信息,如寄存器,線程切換時需要保存這些信息

  • 獨立棧:每個線程都有獨立的棧空間,用于存儲局部變量和執行上下文

  • 錯誤碼(errno):線程異常退出時,通過錯誤碼反饋信息

  • 信號屏蔽字:各個線程對于信號的屏蔽字設置不同,確保每個線程能根據需要對信號做出響應

  • 調度優先級:線程也需要被調度,調度算法根據優先級來合理分配執行時間

其中,寄存器和獨立棧是線程最關鍵的私有資源,它們保障了線程切換的獨立性以及運行時的穩定性。

1.2 線程共享資源

除了線程的私有資源,多線程還會共享進程的部分資源。線程共享資源不需要額外的開銷,并能在各個線程間隨時訪問。

共享的定義不需要太多的額外成本,就可以實現隨時訪問資源

基于?多線程看到的是同一塊進程地址空間,理論上?凡是在進程地址空間中出現的資源,多線程都是可以看到的

但實際上為了確保線程調度、運行時的獨立性只能共享部分資源

在 Linux 中,共享資源包括:

  • 共享區、全局數據區、字符常量區、代碼區:這些區域是進程中天然支持共享的資源。

  • 文件描述符表:在多線程中進行 I/O 操作時,無需每個線程都重新打開文件,文件描述符表在多個線程間共享。

  • 信號處理方式:所有線程共同構成一個整體,信號處理必須統一。

  • 當前工作目錄:所有線程共享進程的工作目錄。

  • 用戶 ID 和組 ID:進程屬于特定的用戶和組,線程也繼承這些身份。

文件描述符表是多線程共享資源中最重要的部分,它確保了多線程 I/O 操作的高效性和協作性。

1.3 原生線程庫

當我們編譯多線程相關代碼時,通常需要添加 -lpthread 參數確保能夠使用 pthread 原生線程庫。

這是因為,在 Linux 中并沒有真正意義上的線程,而是通過輕量級進程(LWP)來模擬線程的實現。Linux 系統并不會直接提供線程控制接口,而是通過封裝輕量級進程相關操作,提供了線程控制的接口。

為了使用戶能夠方便地操作線程,Linux 提供了 pthread,這是一個標準的線程庫,也是個第三方庫,被存放在了系統及庫路徑下。封裝了操作系統底層的輕量級進程控制接口。用戶只需在編譯時添加 -lpthread 參數(告訴庫名),即可正常使用線程相關接口。

計算機哲學的體現:通過增加一層軟件抽象來簡化復雜度,解決操作系統對線程支持的不足。

?在?Linux?中,封裝輕量級進程操作相關接口的庫稱為?pthread?庫,即?原生線程庫,這個庫文件是所有?Linux?系統都必須預載的,用戶使用多線程控制相關接口時,只需要指明使用?-lpthread?庫,即可正常使用多線程控制相關接口


2、線程控制接口

2.1 線程創建

要想控制線程,得先創建線程。對于原生線程庫來說,創建線程使用的是 pthread_create 這個接口。

#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

參數詳解:

  • 參數1 pthread_t* 線程 ID,用于標識線程,本質上它是一個 unsigned long int 類型。
    注: pthread_t* 表示這是一個輸出型參數,用于在創建線程后獲取新線程的 ID。

  • 參數2 const pthread_attr_t* 用于設置線程的屬性,如優先級、狀態、私有棧大小等。通常不需要特別處理,傳遞 nullptr 使用默認設置即可。

  • 參數3 void *(start_routine) (void ): 這是一個非常重要的參數,類型為返回值為 void*、參數也為 void* 的函數指針。線程啟動時會自動回調此函數(類似于 signal 函數中的參數2)。

  • 參數4 void* 顯然,這個類型與回調函數中的參數類型相匹配,它是線程運行時傳遞給回調函數的參數。

返回值:
成功返回 0,失敗返回錯誤號。錯誤檢查:

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

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

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

理解了創建線程函數的各個參數后,就可以嘗試創建一個線程了:

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;void* threadRun(void *arg) {while(true) {cout << "我是次線程,我正在運行..." << endl;sleep(1);}return nullptr;
}int main() {pthread_t t;pthread_create(&t, nullptr, threadRun, nullptr);while(true) {cout << "我是主線程 " << " 我創建了一個次線程 " << t << endl;sleep(1);}return 0;
}

這段代碼非常簡單。如果直接編譯,可能會引發報錯:

錯誤信息: 未定義 pthread_create 這個函數

原因: 沒有指定使用原生線程庫,解決方法是在編譯時添加 -lpthread 來鏈接線程庫。

驗證原生線程庫是否存在:
你可以通過 ldd 命令查看已編譯程序的庫鏈接情況。例如,使用 ldd mythread 命令來查看是否成功鏈接到原生線程庫。

  • ps -al 命令中的 LWP 是內核中線程的 ID,也可以看作是線程在進程中的唯一標識符。

  • 用戶層的 pthread_t是用戶空間的線程標識符,表示線程控制塊(TCB)的地址它與 LWP ID 是映射的。

程序運行時主線程和次線程的順序如何?
線程的執行順序由操作系統的調度器決定。多線程程序中的主線程和次線程執行順序不確定,具體執行順序依賴于調度器的調度策略。


2.1.1 一批線程

接下來我們演示如何創建一批線程。

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;#define NUM 5void* threadRun(void *name) {while(true) {cout << "我是次線程 " << (char*)name << endl;sleep(1);}return nullptr;
}int main() {pthread_t pt[NUM];for(int i = 0; i < NUM; i++) {// 注冊新線程的信息char name[64];snprintf(name, sizeof(name), "thread-%d", i + 1);pthread_create(pt + i, nullptr, threadRun, name);}while(true) {cout << "我是主線程,我正在運行" << endl;sleep(1);}return 0;
}

細節:
在傳遞 pthread_create 的參數時,可以通過 起始地址+偏移量 的方式進行傳遞,這樣每個線程就能接收到不同的參數信息。

預期結果: 打印出 thread-1thread-2thread-3 等。

實際結果: 五個次線程在運行,但打印出來的都是 thread-5

原因: char name[64] 是主線程棧區中的局部變量,多個線程共享這塊空間,最后一次的覆蓋導致每個線程都讀取到相同的數據。
解決方法: 在堆區動態分配空間,為每個線程分配獨立的內存區域,以確保信息的獨立性。

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;#define NUM 5void* threadRun(void *name) {while(true) {cout << "我是次線程 " << (char*)name << endl;sleep(1);}delete[] (char*)name;return nullptr;
}int main() {pthread_t pt[NUM];for(int i = 0; i < NUM; i++) {// 注冊新線程的信息char *name = new char[64];snprintf(name, 64, "thread-%d", i + 1);pthread_create(pt + i, nullptr, threadRun, name);}while(true) {cout << "我是主線程,我正在運行" << endl;sleep(1);}return 0;
}

?通過這種方式,程序運行將符合預期,每個線程都會打印出自己獨立的名稱。


2.2 線程等待

?線程等待 為什么需要線程等待?

已經退出的線程,其空間沒有被釋放,仍然在進程的地址空間內

創建新的線程不會復用剛才退出線程的地址空間

主線程需要等待次線程。在原生線程庫中,提供 pthread_join 來等待一個線程的運行結束。

#include <pthread.h>int pthread_join(pthread_t thread, void **retval);

參數說明:

  • 參數1 pthread_t: 待等待的線程 ID,本質上是一個無符號長整型類型。

  • 參數2 void:這是一個輸出型參數,用于獲取次線程的退出結果。如果不關心返回值,可以傳遞 nullptr

返回值: 成功返回 0,失敗返回錯誤號。

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;#define NUM 5void* threadRun(void *name) {while(true) {cout << "我是次線程 " << (char*)name << endl;sleep(1);}delete[] (char*)name;return nullptr;
}int main() {pthread_t pt[NUM];for(int i = 0; i < NUM; i++) {// 注冊新線程的信息char *name = new char[64];snprintf(name, 64, "thread-%d", i + 1);pthread_create(pt + i, nullptr, threadRun, name);}// 等待次線程運行結束for(int i = 0; i < NUM; i++) {int ret = pthread_join(pt[i], nullptr);if(ret != 0)cerr << "等待線程 " << pt[i] << " 失敗!" << endl;}cout << "所有線程都退出了" << endl;return 0;
}

該程序確保了主線程在等待所有次線程結束后才會退出,確保了線程的正常結束。

調用該函數的線程將掛起等待,直到id為thread的線程終止。thread線程以不同的方法終止,通過pthread_join得到的終止狀態是不同的,總結如下:

1. 如果thread線程通過return返回,value_ ptr所指向的單元里存放的是thread線程函數的返回值。

2. 如果thread線程被別的線程調用pthread_ cancel異常終掉,value_ ptr所指向的單元里存放的是常數PTHREAD_ CANCELED。

3. 如果thread線程是自己調用pthread_exit終止的,value_ptr所指向的單元存放的是傳給pthread_exit的參數。

4. 如果對thread線程的終止狀態不感興趣,可以傳NULL給value_ ptr參數?


2.3 線程終止

如果需要只終止某個線程而不終止整個進程,可以有三種方法:

1. 從線程函數return。這種方法對主線程不適用,從main函數return相當于調用exit。(主線程return 0 要開始合理使用了)

2. 線程可以調用pthread_ exit終止自己。

3. 一個線程可以調用pthread_ cancel終止同一進程中的另一個線程。

pthread_exit函數

功能:線程終止原型

void pthread_exit(void *value_ptr);

參數

value_ptr:value_ptr不要指向一個局部變量。

返回值:無返回值,跟進程一樣,線程結束的時候無法返回到它的調用者(自身)

需要注意,pthread_exit或者return返回的指針所指向的內存單元必須是全局的或者是用malloc分配的,不能在線程函數的棧上分配,因為當其它線程得到這個返回指針時線程函數已經退出了
pthread_join?中的?void **retval?是一個輸出型參數,可以把一個?void *?指針的地址傳遞給?pthread_join?函數,當線程調用?pthread_exit?退出時,可以根據此地址對?retval?賦值,從而起到將退出信息返回給主線程的作用

為什么 pthread_join 中的參數2類型為 void**?

因為主線程和次線程此時并不在同一個棧幀中,要想遠程修改值就得傳地址,類似于 int -> &int,不過這里的 retval 類型是 void*
注意: 直接在 回調方法 中 return 退出信息,主線程中的 retval 也是可以得到信息的,因為類型都是 void*,彼此相互呼應

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;#define NUM 5void* threadRun(void *name)
{cout << "我是次線程 " << (char*)name << endl;sleep(1);delete[] (char*)name;pthread_exit((void*)"EXIT");// 直接return "EXIT" 也是可以的// return (void*)"EXIT";
}int main()
{pthread_t pt[NUM];for(int i = 0; i < NUM; i++){// 注冊新線程的信息char *name = new char[64];snprintf(name, 64, "thread-%d", i + 1);pthread_create(pt + i, nullptr, threadRun, name);}// 等待次線程運行結束void *retval = nullptr;for(int i = 0; i < NUM; i++){int ret = pthread_join(pt[i], &retval);if(ret != 0)cerr << "等待線程 " << pt[i] << " 失敗!" << endl;cout << "線程 " << pt[i] << " 等待成功,退出信息是 " << (const char*)retval << endl;}cout << "所有線程都退出了" << endl;return 0;
}

void*?非常之強大,可以指向任意類型的數據,甚至是一個對象

?2.4 線程實戰

無論是 pthread_create 還是 pthread_join,它們的參數都有一個共同點:包含了一個 void* 類型的參數。這意味著我們可以通過傳遞對象指針給線程,并在其中執行某些特定任務處理。

我們首先創建一個線程信息類,用于計算從 0N 的累加和。線程信息包括:

  • 線程名字(包括 ID)

  • 線程編號

  • 線程創建時間

  • 待計算的值 N

  • 計算結果

  • 狀態

為了方便訪問成員,權限設置public

// 線程信息類的狀態
enum class Status
{OK = 0,ERROR
};// 線程信息類
class ThreadData
{
public:ThreadData(const string &name, int id, int n):_name(name), _id(id), _createTime(time(nullptr)), _n(n), _result(0), _status(Status::OK) {}public:string _name;int _id;time_t _createTime;int _n;int _result;Status _status;
};

此時就可以編寫回調方法中的業務邏輯了:

void* threadRun(void *arg)
{ThreadData *td = static_cast<ThreadData*>(arg);// 業務處理for(int i = 0; i <= td->_n; i++)td->_result += i;// 如果業務處理過程中出現異常,可以設置 _status 為 ERRORcout << "線程 " << td->_name << " ID " << td->_id << " CreateTime " << td->_createTime << " 完成..." << endl;pthread_exit((void*)td);
}

主線程在創建線程及等待線程時,使用 ThreadData 對象。在后續修改業務邏輯時,只需修改類及回調方法,而不需要更改創建及等待邏輯,這有效地做到了邏輯解耦。

int main()
{pthread_t pt[NUM];for(int i = 0; i < NUM; i++){// 注冊新線程的信息char name[64];snprintf(name, sizeof(name), "thread-%d", i + 1);// 創建對象ThreadData *td = new ThreadData(name, i, 100 * (10 + i));pthread_create(pt + i, nullptr, threadRun, td);sleep(1); // 盡量拉開線程創建時間}// 等待次線程運行結束void *retval = nullptr;for(int i = 0; i < NUM; i++){int ret = pthread_join(pt[i], &retval);if(ret != 0)cerr << "等待線程 " << pt[i] << " 失敗!" << endl;ThreadData *td = static_cast<ThreadData*>(retval);if(td->_status == Status::OK)cout << "線程 " << pt[i] << " 計算 [0, " << td->_n << "] 的累加和結果為 " << td->_result << endl;delete td;}cout << "所有線程都退出了" << endl;return 0;
}

程序運行時,各個線程能夠正確計算累加和。此示例展示了線程如何利用傳遞的對象指針進行任務處理。線程不僅可以用于計算,還可以擴展到其他領域,如網絡傳輸、密集型計算、多路 I/O 等,關鍵在于修改業務邏輯。

2.5 其他接口

與多線程相關的還有一批簡單但重要的接口,我們將一并介紹。

2.5.1 關閉線程pthread_cancel

線程不僅可以被創建,還可以被關閉。我們可以使用 pthread_cancel 來關閉已經創建并正在運行的線程。

#include <pthread.h> int pthread_cancel(pthread_t thread);

參數說明:
pthread_t thread:表示被關閉的線程 ID。
返回值:
成功返回 0,失敗返回錯誤號。

該函數使用成功后,線程會被異常信號殺死,退出碼為PTHREAD_CANCELED(-1),?pthread_join()函數會等待成功,回收資源一樣成功,與pthread_detach.

detach是明確表明推出資源直接交由操作系統直接釋放,一種不關心的狀態,如果在用pthread_join等待的話,資源已經沒有,所以會等待失敗。

#include <iostream>
#include <string>
#include <ctime>
#include <unistd.h>
#include <pthread.h>using namespace std;void *threadRun(void *arg)
{const char *ps = static_cast<const char*>(arg);while(true){cout << "線程 " << ps << " 正在運行" << endl;sleep(1);}pthread_exit((void*)10);
}int main()
{pthread_t t;pthread_create(&t, nullptr, threadRun, (void*)"Hello Thread");// 3秒后關閉線程sleep(3);pthread_cancel(t);void *retval = nullptr;pthread_join(t, &retval);cout << "線程 " << t << " 已退出,退出信息為 " << (int64_t)retval << endl;return 0;
}

運行結果:
程序運行 3 秒后,可以看到退出信息為 -1,這是因為 pthread_cancel 關閉的線程,其退出信息統一為 PTHREAD_CANCELED-1


2.5.2 獲取線程 ID pthread_self

線程 ID 是線程的唯一標識符,我們可以通過 pthread_self 獲取當前線程的 ID。

#include <pthread.h> pthread_t pthread_self(void); 
#include <iostream>
#include <string>
#include <ctime>
#include <unistd.h>
#include <pthread.h>using namespace std;void *threadRun(void *arg)
{cout << "當前次線程的ID為 " << pthread_self() << endl;return nullptr;
}int main()
{pthread_t t;pthread_create(&t, nullptr, threadRun, nullptr);pthread_join(t, nullptr);cout << "創建的次線程ID為 " << t << endl;return 0;
}

結果:
pthread_self 返回當前線程的 ID,而 t 顯示的是主線程創建時的線程 ID。


2.5.3 線pthread_detach

父進程需要阻塞式等待子進程退出,主線程等待次線程時也是阻塞式等待。如果希望避免一直阻塞,我們可以使用線程分離。

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

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

注意:?如果線程失去了?joinable?屬性,就無法被?join,如果?join?就會報錯

#include <pthread.h> 
int pthread_detach(pthread_t thread);

參數說明:
pthread_t thread:待分離的線程 ID。
返回值:
成功返回 0,失敗返回錯誤號。

#include <iostream>
#include <string>
#include <ctime>
#include <unistd.h>
#include <pthread.h>using namespace std;void *threadRun(void *arg)
{int n = 3;while(n){cout << "次線程 " << n-- << endl;sleep(1);}
}int main()
{pthread_t t;pthread_create(&t, nullptr, threadRun, nullptr);pthread_detach(t);int n = 5;while(n){cout << "主線程 " << n-- << endl;sleep(1);}return 0;
}

運行結果:
主線程和次線程并發執行,不需要擔心次線程的退出會導致主線程阻塞。


3. 深入理解線程

3.1 理解線程庫及線程 ID

在見識過原生線程庫提供的一批便利接口后,我們不禁感嘆庫的強大。那么,這樣一個強大的庫究竟是如何工作的呢?

原生線程庫本質上是一個存儲/lib64 目錄下的動態庫,想要使用這個庫,在編譯時必須加上 -lpthread 來指定鏈接動態庫。

程序運行時,原生線程庫需要從磁盤加載到內存中,并通過進程地址空間映射到共享區供線程使用。

由于用戶并不會直接操作輕量級進程的接口,因此需要借助第三方庫進行封裝,就像用戶可能不了解操作系統提供的文件接口一樣,而使用 C 語言封裝的 FILE 庫。

對于原生線程庫來說,線程不僅僅是一個,而是多個。因此,在線程庫中創建 TCB(線程控制塊)結構,類似于進程的 PCB(進程控制塊),其中存儲線程的各種信息,例如線程獨立棧信息等。

在內存中,整個線程庫就像一個“數組”,每一塊空間存儲了 TCB 信息,每個 TCB 的起始地址就表示當前線程的 ID。由于地址是唯一的,因此線程 ID也是唯一的。LWP ID 是內核為每個線程分配的唯一標識符,線程在內核中的標識。

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>using namespace std;string toHex(pthread_t t)
{char id[64];snprintf(id, sizeof(id), "0x%x", t);return id;
}void *threadRun(void *arg)
{cout << "我是[次線程],我的ID是 " << toHex(pthread_self()) << endl;return (void*)0;
}int main()
{pthread_t t;pthread_create(&t, nullptr, threadRun, nullptr);pthread_join(t, nullptr);cout << "我是[主線程],我的ID是 " << toHex(pthread_self()) << endl;return 0;
}

我們之前打印 pthread_t 類型的線程 ID 時,實際打印的就是地址,不過它是以十進制顯示的。我們可以通過一個函數將其轉換為十六進制顯示:

運行結果:
線程 ID 確實能轉換為地址(虛擬進程地址空間上的地址)。

注意: 即便是 C++11 提供的 thread 線程庫,在 Linux 平臺中運行時,也需要帶上 -lpthread 選項,因為它本質上是對原生線程庫的封裝。


3.2 理解線程獨立棧

線程之間存在獨立棧,保證它們在執行任務時不會相互干擾。我們可以通過以下代碼來驗證這一點:

多個線程使用同一個入口函數,并打印其中臨時變量的地址:

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>using namespace std;string toHex(pthread_t t)
{char id[64];snprintf(id, sizeof(id), "0x%x", t);return id;
}void *threadRun(void *arg)
{int tmp = 0;cout << "thread " << toHex(pthread_self()) << " &tmp: " << &tmp << endl;return (void*)0;
}int main()
{pthread_t t[5];for(int i = 0; i < 5; i++){pthread_create(t + i, nullptr, threadRun, nullptr);sleep(1);}for(int i = 0; i < 5; i++)pthread_join(t[i], nullptr);return 0;
}

運行結果:
可以看到,五個線程打印出的臨時變量地址不相同,證明每個線程都有獨立的棧空間。

為什么 CPU 能夠區分這些棧結構呢?

答案是:通過棧頂指針 ebp 和棧底指針 esp 來進行切換。ebpesp 是 CPU 中兩個非常重要的寄存器,即使是程序啟動時,也需要借助這兩個寄存器來為 main 函數開辟對應的棧區。

除了移動 esp 擴大棧區外,還可以同時移動 ebpesp 來更改當前棧區。因此,在多線程中,棧區的切換是通過這兩個寄存器來完成的。


3.3 理解線程局部存儲

線程之間共享全局變量,操作全局變量時會影響其他線程:

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>using namespace std;int g_val = 100;string toHex(pthread_t t)
{char id[64];snprintf(id, sizeof(id), "0x%x", t);return id;
}void *threadRun(void *arg)
{cout << "thread: " << toHex(pthread_self()) << " g_val: " << ++g_val << " &g_val: " << &g_val << endl;return (void*)0;
}int main()
{pthread_t t[3];for(int i = 0; i < 3; i++){pthread_create(t + i, nullptr, threadRun, nullptr);sleep(1);}for(int i = 0; i < 3; i++)pthread_join(t[i], nullptr);return 0;
}

運行結果:
在三個線程的影響下,g_val 最終變成了 103

如果想讓每個線程看到不同的全局變量可以使用 __thread 修飾符,這樣全局變量就不再存儲在全局數據區,而是存儲到每個線程的局部存儲區中

__thread int g_val = 100;

運行結果:
通過 __thread 修飾后,每個線程看到的 g_val 都是不同的,并且地址變大了。

解釋:
“全局變量” 的地址變大是因為它不再存儲在全局數據區,而是存儲在線程的局部存儲區中。線程的局部存儲區位于共享區,并且共享區的地址天然大于全局數據區

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

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

相關文章

Python爬蟲實戰:研究spiderfoot工具,構建網絡情報收集系統

1. 引言 1.1 研究背景 在數字化時代,互聯網公開信息已成為國家治理、企業決策與學術研究的戰略資源。據 Statista 統計,2023 年全球互聯網數據總量突破 120ZB,其中可通過公開渠道獲取的情報信息占比超 30%。傳統人工信息收集方式受限于效率與廣度,難以應對海量數據處理需…

在路由器openwrt上安裝openclas

在路由器openwrt上安裝openclas 名詞解釋 las: lash 運行效果圖 安裝 安裝教程參考&#xff1a; 官方&#xff1a;github.com 官方2&#xff1a;openclas.net 如果安裝完成后菜單上沒有&#xff0c;重啟路由后在“服務”菜單下 點擊運行會提示下載內核&#xff0c;按提示…

HIVE 窗口函數處理重復數據

窗口函數row_number()&#xff0c;結合OVER子句中的PARTITION BY和ORDER BY&#xff0c;為數據分組內的每一行生成一個唯一的序號。具體分析如下&#xff1a;函數作用&#xff1a;row_number()&#xff1a;為每個分組內的行分配一個唯一的連續序號&#xff08;從1開始&#xff…

自動駕駛控制算法——MPC控制算法

自動駕駛控制算法——MPC控制算法 文章目錄自動駕駛控制算法——MPC控制算法一、MPC 是什么&#xff1f;1.1 定義與核心思想1.2 MPC 與傳統控制器&#xff08;如 PID&#xff09;的區別1.3 自動駕駛中 MPC 的典型應用場景&#x1f697; 軌跡跟蹤控制&#xff08;Trajectory Tra…

【DL學習筆記】各種卷積操作總結(深度可分離、空洞、轉置、可變形)

Vanilla Convolution 普通卷積 卷積通道數&#xff1a; 卷積核的數量決定輸出的張量的通道數nnn&#xff0c;輸入的張量和每一個核Kernel做卷積運算得到一個channel的輸出。輸入通道數CinC_{in}Cin?決定每一個卷積核的通道數 卷積輸出feature map的尺寸的計算公式&#xff1…

【Ubuntu】請問,`ip -a`顯示的“wlo1”是什么呀?

商量AI wlo1 是 Linux 系統中 無線網絡接口&#xff08;Wi-Fi 網卡&#xff09;的名稱&#xff0c;其命名遵循現代 Linux 的 可預測網絡接口命名規則&#xff08;Predictable Network Interface Names&#xff09;。以下是詳細解析&#xff1a;命名規則拆解 wlo1 的結構由三部分…

Excel商業智能分析報表 【銷售管理分析儀】

1.銷售漏斗&#xff08;Sales Funnel&#xff09;分析&#x1f31f;&#x1f31f; 銷售漏斗定義&#xff1a;科學反映商機狀態及銷售效率的銷售管理模型。適用場景&#xff1a;關系型銷售運營&#xff08;需長期維護客戶關系的銷售模式&#xff09;。核心功能&#xff1a;按銷售…

【論文閱讀|V2M: VISUAL 2-DIMENSIONAL MAMBA FOR IMAGE REPRESENTATION LEARNING】

論文題目:V2M: VISUAL 2-DIMENSIONAL MAMBA FOR IMAGE REPRESENTATION LEARNING 年份:2024 期刊會議: arXiv 代碼鏈接:https://github.com/wangck20/V2M 目錄 現階段存在的問題 1. 二維結構信息丟失 2. 一維 Mamba 架構的局限性 3. 提升視覺任務表現 相關研究 方法 二維…

服務器數據安全:利用阿里云OSS/騰訊云COS實現網站數據自動備份

更多云服務器知識&#xff0c;盡在hostol.com你的網站&#xff0c;就像一座你親手在數字海灘上堆砌起來的、精美絕倫的“沙堡”。你為它設計了獨特的風格&#xff0c;添置了豐富的內容&#xff0c;吸引了越來越多的游客前來參觀。每一篇文章&#xff0c;每一條評論&#xff0c;…

AdGuard 安卓修改版:全方位廣告攔截與隱私保護專家

AdGuard 安卓版是一款功能強大的廣告攔截軟件&#xff0c;能夠有效阻止網頁和應用程序中的廣告和跟蹤器&#xff0c;提升用戶的瀏覽體驗。它不僅提供了廣泛的廣告攔截功能&#xff0c;還通過多種隱私保護功能&#xff0c;確保用戶的個人信息安全。以下是 AdGuard 安卓版的詳細介…

Mysql中的鎖到底是什么?鎖的是什么?

MySQL InnoDB 的鎖&#xff1a;一次從“守衛”到“交通指揮中心”的深度之旅 MySQL InnoDB 的鎖。這個概念常常讓人覺得復雜抽象&#xff0c;但我們需要抓住它的底層設計哲學 忘記那些代碼和術語定義&#xff0c;我們先從最底層的問題開始思考&#xff1a; 思考一&#xff1a;為…

CVE-2020-24557

一、漏洞原理 CVE-2020-24557 是 D-Link 路由器&#xff08;如 DIR-816L2&#xff09; 中存在的一個 命令注入&#xff08;Command Injection&#xff09; 漏洞&#xff0c;其核心原理為&#xff1a; ①路由器的管理界面&#xff08;Web 或 CGI 接口&#xff09;在處理某些用戶…

proxychains——Linux代理工具

簡介 ProxyChains是一個UNIX程序&#xff0c;通過預加載動態庫&#xff08;dlsym(),LD_PRELOAD&#xff09;劫持動態鏈接程序中與網絡相關的libc函數&#xff0c;將連接重定向至SOCKS4a/5或HTTP代理。僅支持TCP協議&#xff08;不支持UDP/ICMP等&#xff09;。它的工作方式基本…

精確調控建筑環境,樓宇自控系統大幅提升居住舒適度

在現代城市化進程中&#xff0c;建筑環境的舒適度已成為衡量生活質量的重要指標。隨著科技的飛速發展&#xff0c;樓宇自控系統&#xff08;Building Automation System, BAS&#xff09;正以前所未有的精準度重新定義人與空間的互動關系。這套集成了物聯網、大數據和人工智能的…

Echarts中的水波圖、水球圖、水半球實現的詳細步驟(vue)

目錄 一、實現效果 二、實現步驟 1. 安裝ECharts和Liquid Fill插件 2. 創建一個組件 3.在創建的vue中引入ECharts和Liquid Fill插件 4.在組件中初始化ECharts和Liquid Fill插件 5.完整代碼 一、實現效果 Echarts中的水位圖&#xff08;水波圖、水球圖、水半球&#xff09;…

Vue父組件向子組件傳遞一個動態的值,子組件如何保持實時更新實時更新?

父組件 通過 :issueDeptId this.form109.issueDeptId傳數據到子組件 <inv-info ehco-data"selectOutInvId" :purposeId this.form109.purposeId:issueDeptId this.form109.issueDeptId:projectNo this.form109.projectNo:invPhysicIds this.form109.issuePh…

如何通過主數據治理重構企業系統競爭力

在當前企業數字化轉型持續深化的背景下&#xff0c;IT系統復雜度與數據規模呈指數級增長。CRM、ERP、HRM、供應鏈、電商平臺等多系統并行運作已成為常態。然而&#xff0c;隨之而來的主數據&#xff08;Master Data&#xff09;管理難題&#xff0c;正日益成為制約系統穩定性、…

c++ 中 原子鎖、互斥鎖、自旋鎖的區別和詳細用法

用最生活化的比喻來解釋 C 中原子鎖、互斥鎖和自旋鎖的區別和用法&#xff0c;讓小白也能秒懂&#xff01;&#x1f604;想象你 ??&#xff08;線程&#xff09;?? 要去公共更衣室 ??&#xff08;共享資源&#xff0c;如變量、數據結構&#xff09;?? 換衣服。這個更衣…

RabbitMQ面試精講 Day 12:鏡像隊列與Quorum隊列對比

【RabbitMQ面試精講 Day 12】鏡像隊列與Quorum隊列對比 開篇&#xff1a;面試價值與核心要點 在RabbitMQ集群環境中&#xff0c;如何保證消息的高可用性是最常被問及的面試問題之一。今天我們將深入探討RabbitMQ提供的兩種高可用隊列實現方案&#xff1a;經典鏡像隊列(Mirror…

Maven 常用命令詳解

前言 Apache Maven 是 Java 項目管理和構建自動化工具&#xff0c;它通過一個項目對象模型&#xff08;POM, Project Object Model&#xff09;來管理項目的構建、報告和文檔。Maven 的核心優勢在于其強大的依賴管理、標準化的項目結構以及豐富的插件生態系統。掌握 Maven 的常…