【Linux】解锁操作系统潜能,高效线程管理的实战技巧
一文带你由浅入深掌握线程基础知识、实现C+11线程封装!
目录
1. 线程的概念
1.线程:是进程内部的一个执行分支(执行流)、是CPU调度的基本单位。
- 由于线程是进程内部的执行分支,多个线程可以在同一进程内部并发执行,提高程序的执行效率和响应速度。
- 由于线程是CPU调度的基本单位,使得OS能够高效地管理和分配线程的执行,提高了资源利用率和响应时间。
Tips:线程在进程内部运行,本质是在进程的地址空间内运行,意味着线程可以直接访问进程地址空间。
程序的代码段包含了所有的函数和指令,每个函数在编译后都会形成一个代码块,每个代码块都有一个入口地址,这个地址是函数名的符号地址。
在单进程中,函数的调用通常是串行调用,即:一个函数调用完后才会调用另外一个函数。将代码分成多个部分,每个部分由不同的执行流(线程或进程)执行,这样可以将原本串行执行的任务变为并行执行,提高效率。
进程需要访问的资源(如 : 代码、数据、静态库、动态库、系统调用等)都通过地址空间来找到,每个资源在地址空间中都有对应的虚拟地址来标识和访问,所以地址空间和地址空间上的虚拟地址本质上是进程的一种"资源"。
2. 线程的理解
一、linux、windows对于线程设计
- 创建新的线程,系统只需要创建task_struct,不需要为新的线程分配地新的址空间和页表资源,新线程与其所属的进程共享同一份地址空间和页表,所以进程的地址空间对于线程来说是可见的,线程之间可以直接地共享数据,无需通过进程间通信(IPC)机制, 即:同一进程内的多个线程之间共享地址空间和其他资源,这使得线程的创建和切换更加高效。
问1:为什么Linux中"线程"这么设计?
简化实现、提高效率、灵活性:Linux设计者认为,进程和线程都是执行流,具有很多的相似性(如 : 都需要维护上下文数据、调度等),所以没必要为线程单独设计数据结构和算法,直接复用进程的数据结构和算法,即:用进程模拟线程。
windows中线程的设计:系统会为线程创建tcb(线程控制块)结构体对象,用来描述线程相关的属性,再加其添加到特定的数据结构中。线程管理拥有一套自己的数据结构与算法。
二、进程本质概念、轻量级进程
- 不要站在调度角度理解进程,而应该站在内核角度理解理解进程:进程是承担分配系统资源的基本实体。
eg:承担分配社会资源的基本实体是家庭,家庭中每个成员都在执行自己特定的任务,但公共的任务是让这个家庭生活变得越来越好,即:家庭是进程、家庭成员是线程。
- 关于调度问题:中,所有调度执行的执行流都被称为轻量级进程,线程也被称为轻量级进程。
3. 地址空间和页表
一、OS管理内存、页框
1.页框或页帧:物理内存中一个固定大小的区域,通常大小为4KB,它是OS系统进行内存管理的基本单位,用于存放数据和指令。
4KB为内存管理和磁盘管理的基本单位。
二、虚拟地址到物理地址的转化
-
在Linux系统中,虚拟地址到物理地址的转化是通过两级页表来实现的,地址空间的大小为4GB,虚拟地址通常被划分为三个部分:高10位作为页目录索引、中间10位作为页表索引、低12位作为页内偏移量。
-
页目录的查找:使用虚拟地址的高10位作为页目录索引,在页目录表中查找对应的页目录项。页目录表是一个数组,其中的每一项指向页表的物理地址。
-
页表的查找:用虚拟地址的中间10位作为页表索引,在对应的页表中查找对应的页表项。页表是一个数组,其中的每一项指向物理页框地址。
-
线程要访问的代码,在内存中物理地址的计算:物理地址 = 页表项中的物理页框地址 + 页内偏移量(虚拟地址的低12位)。
多个执行流如何进行代码划分?
函数编译后可以被看作一段连续的代码块,函数名作为这个代码块的入口地址。
在链接阶段,这些函数(代码块)最终会被放置在程序的最终可执行文件中,链接器会将这些代码块按照一定的顺序进行排列,并分配唯一的地址范围。即:所有函数,都要按照地址空间进行统一编址,所有函数的代码块,在地址空间中都有唯一的地址范围。
OS通过页表和内存保护机制,确保每个进程只能访问自己有权访问的内存区域。
不同执行流都有自己的执行起点,也就是线程函数入口地址(虚拟地址),通过页表映射就可以找到对应物理内存的代码,从而执行相应的代码。
4. 线程的控制
4.1. POSIX线程库
- POSIX线程库:是POSIX标准中定义的线程库,提供了一套标准的线程函数。
-
与线程有关的函数构成了一个完整的序列,绝大多数函数的名字都是以"pthread_"开头的。
-
使用线程库函数,必须加上头文件#include<pthread.h>,链接线程库函数时,编译器要使用"-lpthread"命令。
Linux中并没有为线程设计独立的结构,线程是通过轻量级进程来实现的,即:Linux中无线程相关的系统调用,只有轻量级进程的系统调用。
用户不知道轻量级进程这个概念,只认识进程和线程,OS就在软件层将轻量级进程的系统调用封装成原生线程库(pthread库),并提供给用户熟悉的线程相关接口。
POSIX线程库(pthread库)是在用户层实现的,不属于内核的一部分,所以pthread库也被称为用户级线程库。
4.2 线程创建 — pthread_create
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void *( * start_routine)(void *), void * arg);
-
功能:创建新线程。
-
参数:thread:输出型参数,存储新线程标识符(ID); attr:设置新线程的属性,如果不需要设置特殊属性,可以传入NULL,NULL表示使用默认属性; start_rountin:新线程的入口函数; arg:传递给入口函数的参数。
void*可以接收任意类型指针,可以与任意类型指针进行强制类型转换。
- 返回值:成功返回0,失败返回错误码。
传统的一些函数是,成功返回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 ;
-
功能:显示当前进程xxx的所有轻量级进程(线程)的详细信息。
-
ps -aL是显示系统中所有进程的所有轻量级进程(线程)的详细信息。
- 对于进程内部只有一个执行流的进程(单进程),LWP == PID,所以OS使用LWP来进行调度。
4.3. 获取线程ID — pthread_self
pthread_t pthread_self(void);
- 功能:获取当前进程的标识符(ID)。
#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(轻量级进程):是内核中用于标识一个执行流的唯一标识符。
- 在大多数OS内核中,线程是通过轻量级进程(LWP)来实现的,LWP提供了线程在内核中的表示,并且每个LWP都有唯一的标识符(LWP ID)。
- tid:用户空间中线程唯一标识符。
- 原生线程库维护这些标识符tid,在创建线程时,线程库会分配唯一的tid给每个新线程。
- LWP与tid关系
一对一:用户空间中的线程都会对应内核中的LWP,意味着每个线程在内核和用户空间都有一个唯一的标识符。在用户空间中,线程库维护一个线程ID的映射表,将用户空间的线程ID与内核中轻量级进程(LWP) ID 关联起来,
LWP是由OS内核管理的,tid是由线程库管理的。
用途:LWP在内核调度和线程管理中使用,tid在用户空间编程和调试中使用。
4.4. 线程终止
- return:线程函数执行完最后一行代码或遇到return语句,线程会自动终止。
- 这种方法对于主线程不适用,因为main函数return相当于调用了exit,exit函数终止整个进程,会导致其他线程都被终止。
- 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错误码。
前提:在调用这个函数之前,主线程要确保目标线程创建成功并启动,如果目标线程尚未启动,调用此函数将无效,甚至可能导致未定义行为。
💡Tips:return、pthread_exit返回的指针所指向的内存单元,必须是全局的或者malloc分配的,不能在线程的独立栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出了。
4.5. 线程等待 — pthread_join
问:为什么需要线程等待?
当一个线程退出时,不会自动释放资源,它的资源(如 : 线程栈、线程控制块等)仍存放在进程的地址空间内,造成内存泄漏。
创建新线程,不会复用刚才退出线程的资源,导致资源浪费。
int pthread_join(pthread_t thread,void** retval);
-
功能:等待指定的线程终止,阻塞等待。
-
参数:thread:要等待的线程标识符(ID); retval:接收被等待线程的返回值,获取此线程的执行情况。
-
返回值:成功返回0,失败返回错误码。
-
调用pthread_join的线程将挂起等待,直到id为thread的线程终止,thread线程以不同的方式终止,则pthread_join得到的终止状态是不同的。
如果thread线程通过return返回,retval所指向的单元里存放的是thread函数的返回值。
如果一个线程被另一个线程调用pthread_cancel异常终止,retval所指向的单元里存放的是<font 常数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 <<"throw3 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无法拿到子线程退出情况。
4.6. 线程分离 — pthread_detach
int pthread_detach(pthread_t thread);
-
功能:分离一个进程。
-
返回值:成功返回0,失败返回错误码。
-
默认情况下,新建的线程是joinable的,线程退出后,必须对其进行pthread_join操作,否则无法释放资源,从而造成内存泄漏。
-
如果主线程不关心新线程的执行情况(即: 返回值),join是一种负担,新线程可以被设置为分离状态,则主线程不需要等待新线程完成,新线程在完成其工作后会自动释放其资源,不需要使用pthread_join来回收资源,如果使用了pthread_join会报错,因为无法获取其退出状态。
-
可以是线程组内其他线程对目标线程进行分离:pthread_detach(pthread_t thread)、也可以是线程自己分离:pthread_detach(pthread_self( ))。
-
线程被创建后,无论是否分离,它都会运行在主线程所在的地址空间中,这意味着新线程和主线程仍共享相同的地址空间。如果主线程退出,则分离后的线程也会退出。即:分离后的线程仅仅不需要主线程join,其他都与线程的特性保持一致。
5. 线程的特点
5.1. 优点
-
创建角度 — 创建一个新线程的代价要比创建一个新进程小得多。
-
调度角度 — 与进程之间的切换相比,线程之间的切换需要OS做的工作要少很多。
-
释放资源角度 — 线程占用的资源要比进程少得多。
-
能充分利用多处理器的可并行数量。
-
在等待I/O操作结束的同时,程序可执行其他计算任务。
-
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
-
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
问:为什么线程切换在调度角度上优势比进程切换更为明显—— 面试题?
一、资源共享
-
每个进程都有独立的地址空间、系统资源(如:文件、设备、信号处理器等)。进程间通信需要借助IPC机制,这增加了通信的复杂性和开销。在进行进程切换时,OS需要保存和恢复整个进程的状态,包括内存管理信息、文件描述符、信号处理状态等,这进一步增加了切换的开销。
-
由于线程共享地址空间、系统资源、内存布局,因此通信开销较小。在进行线程切换时,OS无需保存和恢复整个进程的状态,只需要保存和恢复线程的私有数据(如:寄存器、栈指针等)。
二、上下文切换
-
进程上下文切换:包括CPU上下文切换(如:寄存器、程序计数器等)、内存上下文切换(如:页表、文件描述符表等),所以在进行进程切换时,OS既需要保存和恢复进程的执行上下文(寄存器、程序计数器等)、还需要重新加载页表、文件描述符表等,这进一步增加了切换的复杂性。
-
线程上下文切换:由于线程共享地址空间、内存布局,不涉及内存上下文的切换。在进行线程切换时,OS只需要保存和恢复线程的执行上下文(寄存器、程序计数器等)、不需要重新加载页表、文件描述符表等,大大减少了切换的开销。
三、局部性原理 (主要问题)
-
进程:由于每个进程有自己的独立地址空间、内存布局,缓存中的数据在进程切换时通常会失效,需要更换当前正在使用的缓存内容,这会导致缓存命中率下降,CPU需要重新从主存储器中加载代码、数据到缓存中,增加了延迟和开销。
-
线程:由于线程共享地址空间、内存布局,缓存中的数据在线程切换时通常不会被丢弃,不需要更换当前正在使用的内容,线程使用的代码和数据很可能仍然在缓存中,CPU可以立即使用缓存中的数据执行指令,而无需等待从主存储器中加载数据,这提高了缓存的命中率、CPU执行速度。
CPU上集成了硬件级别的缓存(Cache L1~L3),其工作原理如下:
缓存的设计基于局部性原理( 即:程序在运行时倾向于最近访问的数据和指令),有两种类型,时间局部性是指如果某个数据被访问了一次,近期它可能会被再次访问,空间局部性是指如果某个数据被访问了,那么其附近的数据也有可能被访问。
缓存通常以缓存行为单位进行读写。当CPU访问某个内存地址时,它可能会将整个地址所在的缓存行加载到缓存中,有助于利用空间局部性,因为相邻的内存地址往往被一起访问。
缓存替换策略:当缓存满了而需要加载新数据时,此策略会决定哪些数据应该被丢弃。
5.2. 缺点
-
性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型 线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的 同步和调度开销,而可用的资源不变。
-
健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了 不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
-
缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
-
编程难度提高:编写与调试一个多线程程序比单线程程序困难得多。
5.3. 线程异常
-
多线程中,任何一个线程出了异常(除零、野指针等),都会导致整个进程退出。— 从而得知多线程代码往往健壮性不好。线程安全问题。
-
主线程退出 = = 进程退出 = = 所有线程退出。所以往往我们需要主线程最后退出。
-
总结:线程是进程的执行分支,线程出异常,就类似于进程出异常,进而触发信号机制,终止进程,则该进程内所有的线程也就随即退出。
-
多线程中,公共函数如果被多个进程同时进入,则该函数被重入了。
#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;
}
6. 进程 VS 线程
-
进程是资源分配的基本单位,线程是调度的基本单位。
-
尽管线程之间共享数据,但线程也有私有数据。
-
硬件上下文 (一组寄存器) —— 调度
-
线程栈 —— 常规运行
-
线程ID
-
errno
-
调度优先级
-
信号屏蔽字
线程栈:是一个独立的栈结构,用于存储线程执行时的局部变量、函数参数、返回地址、调用栈等信息。
- 进程的多个线程共享同一地址空间,所以代码段(Text Segment)、数据段(Data Segment)是共享的。
-
代码和全局数据 ( 全局函数、全局变量 )
-
文件描述符表
-
每种信号的处理方式 (信号的handler表)
-
当前工作目录pwd
-
用户id和组id
#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;
}
7. 线程管理
7.1. 线程ID的本质
- 线程ID定义:线程ID是OS用于唯一标识一个线程的标识符。
- 在POSIX线程库(pthreads)中,线程ID的类型为pthread_t 。
-
线程管理:OS为了管理进程,Linux内核设计了一套专用于进程的数据结构、算法,Linux为了系统的简洁性和轻量性,并没有为线程设计一套新的数据结构和算法,而是复用了进程的数据结构和算法。
-
线程库通常是一个动态库,通过ldd命令可以看得到,进程运行时,动态库需要被加载到内存中,然后通过页表映射到地址空间中的共享区,地址空间的共享区可以被进程内所有的线程访问到。
- 线程库:提供了管理线程的一系列接口函数,实现了描述线程的数据结构和一些管理工作,即:对于线程的管理工作,由线程库来完成。
5. 线程控制块(TCB):每个线程都有自己的TCB,包含对应线程的各种属性和状态信息。
-
线程栈:每个线程都有自己私有的独立栈,主线程采用的是进程地址空间中的原生栈,而其余的线程采用的是共享区中的线程库中的栈。
-
线程ID的本质:在NTLP线程库中,线程ID本质是一个指向线程控制块(TCB)的指针,这个指针指向共享区中的一个内存块,这个内存块(TCB)的起始地址就是线程ID。
同一个进程中所有虚拟地址都是不同的,因此可以根据虚拟地址来区分每一个线程,线程的后续操作,就是根据线程ID来进行操作的。
问:pthread_s到底是什么类型呢?
取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是地址空间上的一个地址。
7.2. 线程局部存储(TLS)
-
线程局部存储:是一种机制,允许每个线程拥有自己变量的副本,这些变量在每个线程中独立存在、互不影响。这种机制确保了线程数据的独立性,从而避免了全局变量或静态变量在并发环境下竞态条件和数据不一致的问题。
-
线程局部存储的优点:数据隔离、减少了同步开销、提高了性能。
-
__thread关键字:用于声明线程局部存储变量,使用__thread关键字声明的变量在每个线程中都有一个独立的副本,这些副本互不影响,有助于避免线程间的竞态条件或数据不一致问题,提高线程安全性。
__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;
}
8. 实现多线程任务
- 在多线程中,线程函数的参数和返回值,我们可以传递 / 返回 基本数据类型(int、float、char)、指针、结构体对象、自定义类型对象作为参数 / 返回值。参数通常需要通过指针传递 / 返回值通常通过void*类型的指针返回。
#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;
}
9. C++11线程的封装
- C++11通过引入头文件,为开发者提供了一套统一且高效的线程API。
- 在编译和链接的时候通常需要加上-pthread选项,告诉编译器你需要链接pthread库。
- std: : thread类是C++标准库中用于创建和管理线程的核心类,它对底层线程API进行了抽象和封装,使得开发者无需关注特定OS的细节,只需要使用C++标准接口就可以创建和控制线程。这种封装方式提高了C++程序的跨平台性和可移植性。
跨平台性: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"Thread.hpp"
#include<vector>
#include<unistd.h>
#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;
}
更多推荐
所有评论(0)