1. 线程的概念
- 线程:是进程内部的一个执行分支(执行流)、是 CPU 调度的基本单位。
Linux 线程是进程内的执行流,共享地址空间但拥有独立栈和寄存器。本文介绍 POSIX 线程库(pthread)的核心函数如 create、join、detach,对比进程与线程的资源调度差异,讲解线程局部存储(TLS)及 C++11 std::thread 封装。内容涵盖线程生命周期管理、异常处理、上下文切换优势及内存竞争问题解决方案。

提示:线程在进程内部运行,本质是在进程的地址空间内运行,意味着线程可以直接访问进程地址空间。
程序的代码段包含了所有的函数和指令,每个函数在编译后都会形成一个代码块,每个代码块都有一个入口地址,这个地址是函数名的符号地址。
在单进程中,函数的调用通常是串行调用,即:一个函数调用完后才会调用另外一个函数。将代码分成多个部分,每个部分由不同的执行流(线程或进程)执行,这样可以将原本串行执行的任务变为并行执行,提高效率。
进程需要访问的资源(如:代码、数据、静态库、动态库、系统调用等)都通过地址空间来找到,每个资源在地址空间中都有对应的虚拟地址来标识和访问,所以地址空间和地址空间上的虚拟地址本质上是进程的一种'资源'。

问题:为什么 Linux 中'线程'这么设计?
简化实现、提高效率、灵活性:Linux 设计者认为,进程和线程都是执行流,具有很多的相似性(如:都需要维护上下文数据、调度等),所以没必要为线程单独设计数据结构和算法,直接复用进程的数据结构和算法,即:用进程模拟线程。
Windows 中线程的设计:系统会为线程创建 tcb(线程控制块) 结构体对象,用来描述线程相关的属性,再加其添加到特定的数据结构中。线程管理拥有一套自己的数据结构与算法。

例如:承担分配社会资源的基本实体是家庭,家庭中每个成员都在执行自己特定的任务,但公共的任务是让这个家庭生活变得越来越好,即:家庭是进程、家庭成员是线程。
4KB 为内存管理和磁盘管理的基本单位。

多个执行流如何进行代码划分?
函数编译后可以被看作一段连续的代码块,函数名作为这个代码块的入口地址。
在链接阶段,这些函数 (代码块) 最终会被放置在程序的最终可执行文件中,链接器会将这些代码块按照一定的顺序进行排列,并分配唯一的地址范围。即:所有函数,都要按照地址空间进行统一编址,所有函数的代码块,在地址空间中都有唯一的地址范围。
OS 通过页表和内存保护机制,确保每个进程只能访问自己有权访问的内存区域。
不同执行流都有自己的执行起点,也就是线程函数入口地址 (虚拟地址),通过页表映射就可以找到对应物理内存的代码,从而执行相应的代码。
#include <pthread.h>,链接线程库函数时,编译器要使用"-lpthread"命令。Linux 中并没有为线程设计独立的结构,线程是通过轻量级进程来实现的,即:Linux 中无线程相关的系统调用,只有轻量级进程的系统调用。
用户不知道轻量级进程这个概念,只认识进程和线程,OS 就在软件层将轻量级进程的系统调用封装成原生线程库 (pthread 库),并提供给用户熟悉的线程相关接口。
POSIX 线程库 (pthread 库) 是在用户层实现的,不属于内核的一部分,所以 pthread 库也被称为用户级线程库。

int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void * (* start_routine)(void *), void * arg);
void*可以接收任意类型指针,可以与任意类型指针进行强制类型转换。
传统的一些函数是,成功返回 0,失败返回 -1,并且对全局变量 errno 赋值以指示错误。pthreads 函数出错时不会设置全局变量 errno(而大部分其他 POSIX 函数会这样做),而是将错误代码通过返回值返回。pthreads 同样也提供了线程内的 errno 变量,以支持其它使用 errno 的代码。对于 pthreads 函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的 errno 变量的开销更小。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* newthreadrun(void* args){
while(true) // 新线程
{
cout << "I am a newthread, pid: " << getpid() << endl;
sleep(1);
}
return nullptr;
}
int main(){
pthread_t tid;
pthread_create(&tid, nullptr, newthreadrun, nullptr); // 新线程、主线程谁先运行:不确定,由调度器决定
while(true) // 主线程
{
cout << "I am a mainthread, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}

ps -aL | head -1 && ps -aL | grep xxx;


pthread_t pthread_self(void);
#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
string ToHex(pthread_t id) // 10 进制转 16 进制 (地址)
{
char buffer[126];
snprintf(buffer, sizeof(buffer), "0x%lx", id);
return buffer;
}
void* newthreadrun(void* args){
while(true) // 新线程
{
cout << "I am a newthread, id: " << ToHex(pthread_self()) << endl;
sleep(1);
}
return nullptr;
}
int main(){
pthread_t tid;
pthread_create(&tid, nullptr, newthreadrun, nullptr);
while(true) // 主线程
{
cout << "I am a mainthread, id: " << ToHex(pthread_self()) << endl;
sleep(1);
}
return 0;
}

一对一:用户空间中的线程都会对应内核中的 LWP,意味着每个线程在内核和用户空间都有一个唯一的标识符。在用户空间中,线程库维护一个线程 ID 的映射表,将用户空间的线程 ID 与内核中轻量级进程 (LWP) ID 关联起来。
LWP 是由 OS 内核管理的,tid 是由线程库管理的。
用途:LWP 在内核调度和线程管理中使用,tid 在用户空间编程和调试中使用。
void pthread_exit(void* retval);
功能:显式地终止当前进程。 pthread_exit 不会自动释放资源,需要使用来回收资源。 retval 参数:线程退出时的返回值,其他线程可以通过 pthread_join 函数获取到这个返回值,如果线程没有设置返回值 (即:没有调用 pthread_exit 函数、没有 return、pthread_exit 函数的参数为 NULL),则 pthread_join 获取到的返回值是未定义的。

int pthread_cancel(pthread_t thread);
功能:用于请求终止指定线程的执行,这个请求不会立即生效,会在合适的时机终止,具体行为取决于目标线程是否设置了取消点,以及是否启用了取消状态。
thread 参数:要终止线程的标识符 (ID)。
返回值:成功返回 0,失败返回非 0 错误码。
前提:在调用这个函数之前,主线程要确保目标线程创建成功并启动,如果目标线程尚未启动,调用此函数将无效,甚至可能导致未定义行为。

提示:return、pthread_exit 返回的指针所指向的内存单元,必须是全局的或者 malloc 分配的,不能在线程的独立栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出了。

问:为什么需要线程等待?
当一个线程退出时,不会自动释放资源,它的资源 (如:线程栈、线程控制块等) 仍存放在进程的地址空间内,造成内存泄漏。
创建新线程,不会复用刚才退出线程的资源,导致资源浪费。
int pthread_join(pthread_t thread, void** retval);
如果 thread 线程通过 return 返回,retval 所指向的单元里存放的是 thread 函数的返回值。
如果一个线程被另一个线程调用 pthread_cancel 异常终止,retval 所指向的单元里存放的是 PTHREAD_CANCELED。
如果 thread 线程是自己调用 pthread_exit 终止的,retval 所指向的单元里存放的是传递给 pthread_exit 的参数。
如果 thread 线程对终止状态不感兴趣,可以传 NULL 给 retval 参数。
#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
string ToHex(pthread_t id) // 10 进制转 16 进制 (地址)
{
char buffer[126];
snprintf(buffer, sizeof(buffer), "0x%lx", id);
return buffer;
}
void* thread1(void* args){
cout << "thread1 is running...." << endl;
int* p1 = (int*)malloc(sizeof(int));
*p1 = 1;
return (void*)p1;
}
void* thread2(void* args){
cout << "thread2 is running...." << endl;
int* p2 = (int*)malloc(sizeof(int));
*p2 = 2;
pthread_exit((void*)p2);
}
void* thread3(void* args){
while(true){
cout << "thread3 is running..." << endl;
sleep(1);
}
return nullptr;
}
int main(){
pthread_t tid;
void* retval = nullptr;
pthread_create(&tid, nullptr, thread1, nullptr);
pthread_join(tid, &retval);
cout << "thread1 ret: " << *(int*)retval << ", thread1 id" << ToHex(pthread_self()) << endl;
pthread_create(&tid, nullptr, thread2, nullptr);
pthread_join(tid, &retval);
cout << "thread2 ret: " << *(int*)retval << ", thread2 id" << ToHex(pthread_self()) << endl;
pthread_create(&tid, nullptr, thread3, nullptr);
pthread_cancel(tid);
pthread_join(tid, &retval);
if(retval == PTHREAD_CANCELED)
cout << "thread3 return, thread3 id: " << ToHex(pthread_self()) << ", return code: PTHREAD_CANCELED\n" << endl;
else
cout << "thread3 return, thread3 id: " << ToHex(pthread_self()) << ", return code: NULL" << endl;
return 0;
}

pthread_join 不考虑线程异常情况,因为它会导致整个进程立即退出,pthread_join 无法拿到子线程退出情况。
int pthread_detach(pthread_t thread);

问:为什么线程切换在调度角度上优势比进程切换更为明显——面试题?
CPU 上集成了硬件级别的缓存 (Cache L1~L3),其工作原理如下:
缓存的设计基于局部性原理 (即:程序在运行时倾向于最近访问的数据和指令),有两种类型,时间局部性是指如果某个数据被访问了一次,近期它可能会被再次访问,空间局部性是指如果某个数据被访问了,那么其附近的数据也有可能被访问。
缓存通常以缓存行为单位进行读写。当 CPU 访问某个内存地址时,它可能会将整个地址所在的缓存行加载到缓存中,有助于利用空间局部性,因为相邻的内存地址往往被一起访问。
缓存替换策略:当缓存满了而需要加载新数据时,此策略会决定哪些数据应该被丢弃。
#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
string ToHex(pthread_t id) // 10 进制转 16 进制 (地址)
{
char buffer[126];
snprintf(buffer, sizeof(buffer), "0x%lx", id);
return buffer;
}
void* newthreadrun(void* args) // 子线程
{
int cnt = 3;
while(cnt--){
cout << "I am a newthread, id: " << ToHex(pthread_self()) << endl;
sleep(1);
}
int* p = nullptr; // 野指针异常
*p = 4;
return nullptr;
}
int main(){
pthread_t tid;
pthread_create(&tid, nullptr, newthreadrun, nullptr);
while(true) // 主线程
{
cout << "I am a mainthread, id: " << ToHex(pthread_self()) << endl;
sleep(1);
}
return 0;
}

线程栈:是一个独立的栈结构,用于存储线程执行时的局部变量、函数参数、返回地址、调用栈等信息。

#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
// 全局成员变量
int g_val = 3;
// 全局成员函数
string ToHex(pthread_t id) // 10 进制转 16 进制 (地址)
{
char buffer[126];
snprintf(buffer, sizeof(buffer), "0x%lx", id);
return buffer;
}
void* newthreadrun(void* args) // 子线程
{
while(true){
cout << "newthread id: " << ToHex(pthread_self()) << ", g_val: " << g_val << ", &g_val: " << &g_val << endl;
g_val--;
if(g_val == 0) break;
sleep(1);
}
return nullptr;
}
int main(){
pthread_t tid;
pthread_create(&tid, nullptr, newthreadrun, nullptr);
while(true) // 主线程
{
cout << "mainthread id: " << ToHex(pthread_self()) << ", g_val: " << g_val << ", &g_val: " << &g_val << endl;
if(g_val == 0) break;
sleep(1);
}
return 0;
}




同一个进程中所有虚拟地址都是不同的,因此可以根据虚拟地址来区分每一个线程,线程的后续操作,就是根据线程 ID 来进行操作的。
问:pthread_t 到底是什么类型呢?
取决于实现。对于 Linux 目前实现的 NPTL 实现而言,pthread_t 类型的线程 ID,本质就是地址空间上的一个地址。
__thread 数据类型 变量名 ;
#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
__thread int g_val = 3; // 线性局部存储变量
string ToHex(pthread_t id) // 10 进制转 16 进制 (地址)
{
char buffer[126];
snprintf(buffer, sizeof(buffer), "0x%lx", id);
return buffer;
}
void* newthreadrun(void* args) // 子线程
{
while(true){
cout << "newthread id: " << ToHex(pthread_self()) << ", g_val: " << g_val << ", &g_val: " << &g_val << endl;
g_val--;
if(g_val == 0) break;
sleep(1);
}
return nullptr;
}
int main(){
pthread_t tid;
pthread_create(&tid, nullptr, newthreadrun, nullptr);
while(true) // 主线程
{
cout << "mainthread id: " << ToHex(pthread_self()) << ", g_val: " << g_val << ", &g_val: " << &g_val << endl;
if(g_val == 0) break;
sleep(1);
}
return 0;
}

#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <vector>
using namespace std;
void* threadname(void* args) // 参数为基本类型
{
const string& name = static_cast<char*>(args);
while(true){
cout << "I am " << name << endl;
sleep(2);
}
return nullptr;
}
int main(){
vector<pthread_t> threads;
for(int i = 1; i <= 5; i++) // 多线程创建
{
// 错误代码,多线程共享同一块内存区域 (缓冲区)
char buffer[64];
snprintf(buffer, 64, "Thread-%d", i);
pthread_t tid;
pthread_create(&tid, nullptr, threadname, buffer);
threads.push_back(tid);
}
for(auto& e : threads) // 等待多线程
pthread_join(e, nullptr);
}

问:为什么创建的多线程名字都相同呢?
答:buffer 缓冲区,用于存储线程的名字,在创建线程时传递这个缓冲区的地址给线程,因为所有线程共享这块缓冲区,那么在主线程创建新线程的过程中,缓冲区的内容会被不断修改,导致前面创建的线程读取到错误的数据。
在多线程编程中,如果多个线程共享同一块内存区域 (如缓冲区),会导致数据竞争和不一致的问题,为了确保每个线程能够正确访问和修改自己的数据,应该为每个线程分配独立的内存区域 (如堆空间)。
#include <cstdio>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <vector>
using namespace std;
void* threadname(void* args) // 参数为基本类型
{
const string& name = static_cast<char*>(args);
while(true){
cout << "I am " << name << endl;
sleep(2);
}
return nullptr;
}
int main(){
vector<pthread_t> threads;
for(int i = 1; i <= 5; i++) // 多线程创建
{
// 为每个线程分配独立的内存区域,防止出现数据竞争和不一致问题
char* buffer = new char[64]; // char buffer[64];
snprintf(buffer, 64, "Thread-%d", i);
pthread_t tid;
pthread_create(&tid, nullptr, threadname, buffer);
threads.push_back(tid);
}
for(auto& e : threads) // 等待多线程
pthread_join(e, nullptr);
}

#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <vector>
#include <string>
#include <unistd.h>
#include <cstdlib>
using namespace std;
#define NUM 5
// 任务类
class Task {
public:
Task(){}
~Task(){}
void SetData(int x, int y){ _x = x; _y = y;}
int Add(){ return _x + _y;}
private:
int _x;
int _y;
};
// 线程类
class Thread {
public:
Thread(int x, int y, const string& threadname):_threadname(threadname){ _t.SetData(x, y);}
string& Threadname(){ return _threadname;}
int Run(){ return _t.Add();}
~Thread(){}
private:
string _threadname; // 名字
Task _t; // 任务
};
// 任务结果类
class Result {
public:
Result(const string& threadname, int result):_threadname(threadname),_result(result){}
void print(){ cout << _threadname << ": " << _result << endl;}
private:
string _threadname;
int _result;
};
void* handerTask(void* args){
Thread* td = static_cast<Thread*>(args); // 类型转换
const string& threadname = td->Threadname();
int result = td->Run();
Result* res = new Result(threadname, result);
return res;
}
int main(){
vector<pthread_t> id;
vector<Result*> res;
for(int i = 1; i <= NUM; i++){
char threadname[128];
snprintf(threadname, sizeof(threadname), "Thread-%d", i);
Thread* td = new Thread(10, 20, threadname);
pthread_t tid; // 创建新线程,参数和返回值为自定义类对象
pthread_create(&tid, nullptr, handerTask, td);
id.push_back(tid);
}
for(auto& e : id){
void* tmp = nullptr;
pthread_join(e, &tmp); // 获取新线程的返回值 (执行情况)
res.push_back((Result*)tmp);
}
for(auto& e : res){
e->print();
delete e;
}
return 0;
}

跨平台性:C++ 多线程库在不同的 OS 上提供了统一的接口。意味着可以在不同的平台编写相同的多线程代码,而不需要关心底层的具体实现。
封装:C++ 多线程库封装了底层的 OS 线程 API,如:在 Linux 中,使用的是 POSIX 线程 (pthread 库),pthread 库是 Linux 底层提供多线程的常用方式。在 Windows 中,多线程的实现方式是对线程调用接口进行了封装。
#include <iostream>
/* <thread> 库的实现依赖于底层的线程库,在编译和链接的时候通常需要加上 -pthread 选项,告诉编译器你需要链接 pthread 库 */
#include <thread> // 定义了与多线程编程相关的类和函数
#include <unistd.h>
using namespace std;
// 线程函数
void threadFunction(int num){
while(true){
cout << "I am newthread " << num << endl;
sleep(1);
}
}
int main(){
const int num = 5;
// thread 类,创建和管理线程
thread t(threadFunction, num);
while(true){
cout << "I am mainthread" << endl;
sleep(1);
}
// thread 类提供了 join、detach 成员函数来管理线程的声明周期
t.join();
return 0;
}


#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <string>
#include <functional>
using namespace std;
namespace zzx {
template<typename T>
using fun_c = function<void(T)>;
template<typename T>
class thread {
public:
thread(fun_c<T> func, T data, const string& name = "thread none-name")
:_func(func),_data(data),_name(name),_stop(true){}
~thread(){}
// 注意:类成员函数,默认第一个参数为 this 指针,静态成员函数无 this 指针
static void* threadroutine(void* args){
thread<T>* td = static_cast<thread<T>*>(args); // 强制类型转换
td->_func(td->_data);
return nullptr;
}
bool start(){
// 为了在静态成员函数 threadroutine 中访问成员变量,参数传递类对象指针 (this)
int n = pthread_create(&_tid, nullptr, threadroutine, this);
if(n != 0) return false;
_stop = false;
return true;
}
void detach(){
if(!_stop) pthread_detach(_tid);
}
void join(){
if(!_stop) pthread_join(_tid, nullptr);
}
string name(){ return _name;}
void stop(){ _stop = true;}
private:
pthread_t _tid;
string _name;
T _data;
fun_c<T> _func;
bool _stop;
};
}
#include <vector>
#include <unistd.h>
#include <cstdio>
#include <string>
#include <functional>
#include <iostream>
#define NUM 5
using namespace zzx;
void route(int num){
while(num){
cout << "I am newthread, num: " << num << endl;
num--;
sleep(1);
}
}
int main(){
vector<thread<int>> threads; // 创建一批线程
for(int i = 1; i <= NUM; i++){
// 堆空间,防止线程共享同一块内存区域,造成数据竞争和不一致问题
char* buffer = new char[64];
snprintf(buffer, 128, "thread-%d", i);
threads.emplace_back(route, 5, buffer);
}
// 启动一批线程
for(auto& e : threads) e.start();
// 等待一批线程
for(auto& e : threads){
e.join();
cout << "wait thread done, thread is " << e.name() << endl;
sleep(1);
}
return 0;
}


微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online