1.线程终止
#include <pthread.h>
void pthread_exit(void* retval);
-
参数:
-
retval
:线程退出时返回的值。这个值可以被其他线程通过pthread_join
函数获取。
-
-
返回值:
-
该函数不会返回,因为它的作用是终止调用它的线程。一旦调用
pthread_exit
,调用它的线程就会结束执行。
-
注意:
当线程调用pthread_exit退出后,它的资源(如线程栈)并不会立即释放,要释放这些资源,必须通过pthread_join或者pthread_detach来完成,如果线程是分离状态(pthread_detach设置),那么线程退出资源后会自动释放。
代码示例
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void* my_thread(void* arg) {
int* condition = (int*)arg;
if (*condition == 1) {
printf("Thread exits early\n");
pthread_exit((void*)1); // 返回值为 1
}
printf("Thread is running\n");
pthread_exit((void*)0); // 返回值为 0
}
int main() {
pthread_t tid;
int condition = 1;
pthread_create(&tid, NULL, my_thread, &condition);
void* status;
pthread_join(tid, &status); // 等待线程结束并获取返回值
printf("Thread has exited with status %ld\n", (long)status);
return 0;
}
void* my_thread(void* arg)
:这是线程的入口函数。每个线程的执行都从这个函数开始。arg
是传递给线程的参数。
int* condition = (int*)arg;
:将传入的参数arg
转换为int*
类型,并存储到condition
指针中。这里假设传入的参数是一个指向整数的指针。
if (*condition == 1)
:检查condition
指针所指向的值是否为1
。
如果是
1
,则执行以下操作:
printf("Thread exits early\n");
:打印一条消息,表明线程将提前退出。
pthread_exit((void*)1);
:调用pthread_exit
函数,终止当前线程的执行,并返回值1
。pthread_exit
的参数是一个void*
类型的值,这里将整数1
转换为void*
类型。如果
*condition
不等于1
,则执行以下操作:
printf("Thread is running\n");
:打印一条消息,表明线程正在正常运行。
pthread_exit((void*)0);
:调用pthread_exit
函数,终止当前线程的执行,并返回值0
。
pthread_cancel函数
#include <pthread.h>
int pthread_cancel(pthread_t thread);
-
参数:
pthread_t thread
是目标线程的线程 ID,表示要被取消的线程。 -
返回值:
-
成功时返回
0
。 -
失败时返回错误码,如
ESRCH
表示未找到目标线程。
-
功能:向目标进程发送一个取消的请求
示例代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* thread_func(void* arg) {
printf("Thread started\n");
while (1) {
sleep(1); // 取消点
}
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
sleep(2); // 给线程一些时间运行
pthread_cancel(tid); // 请求取消线程
void* status;
pthread_join(tid, &status); // 等待线程结束
if (status == PTHREAD_CANCELED) {
printf("Thread was canceled\n");
}
return 0;
}
分离线程
默认情况下,新创建的线程是joinable的,线程退出以后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关系线程的返回值,join是一种负担,就可以选择线程退出时,自动释放线程的资源。
#include <pthread.h>
int pthread_detach(pthread_t thread);
-
参数:
pthread_t thread
是要分离的线程的线程 ID。 -
返回值:
-
成功时返回
0
。 -
失败时返回错误码,例如:
-
EINVAL
:线程已经是分离状态。 -
ESRCH
:未找到指定的线程。
-
-
功能:
分离线程:将线程标记为分离状态,分离状态的线程在终止时会自动释放其占用的资源,而不会保留退出状态。
资源管理:分离线程避免了线程资源在终止后仍然占用系统资源,直到有线程调用pthread_join来回收资源。
joinable为1就是要阻塞等待资源,为0就分离状态。
示例代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* thread_func(void* arg) {
printf("Thread is running\n");
sleep(2); // 线程运行一段时间
printf("Thread is exiting\n");
pthread_exit(NULL); // 线程正常退出
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL); // 创建线程
pthread_detach(tid); // 将线程标记为分离状态
printf("Main thread continues to run\n");
sleep(3); // 主线程继续运行一段时间
printf("Main thread is exiting\n");
return 0;
}
Thread is running
Main thread continues to run
Thread is exiting
Main thread is exiting
不可逆性:一旦线程被标记为分离状态,就不能通过pthread_join获取其退出状态
可以是线程组内的其它线程对目标进行分离,也可以是线程自己分离
pthread_detach(pthread_self());
2.线程ID及进程地址空间布局
pthread_create函数会产生一个线程ID,存放在第一个参数执行的地址中。
前面讲的线程ID属于进程调度的范围,因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一标识该进程。
pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID。
线程库有pthread_self函数,可以获得线程自身的ID
pthread
轻量级进程(LWP)
在Linux系统中,线程的实现是基于轻量级进程或者内核线程,尽管线程与进程共享相同的地址空间,但Linux内核为每一个线程都维护了一个独立的task_struct结构体,用于表示线程的状态和相关信息。所以Linux可以管理线程,包括调度,优先级设置,同步等,就像管理进程一样。
Linux内核实现线程的方式主要是通过共享进程地址空间的一组线程完全的。在Linux中,线程也称为轻量级进程。,每一个线程都有一个唯一的线程ID(tid)和一个相关的task_struct结构,但所有线程共享同一进程的地址空间。
线程库
用户空间使用线程库创建线程时,这些库会为我们处理底层细节。比如pthread库会调用clone系统调用来请求内核创建一个新的LWP。
LWP和PID的区别
LWP:是线程在Linux中的一种表示方式,每一个线程都有唯一的LWP ID,在进程内部是唯一的,整个系统就不一定是唯一,别的进程可能有相同。
PID:PID是进程的唯一标识符,在整个系统是唯一的。一个进程内的所有线程共享同一个PID,因为线程是进程的一部分,共享进程的地址空间和资源。
创建一个线程时,系统会给创建的线程分配一个唯一的LWP ID,还会将其跟父进程的PID关联起来,就形成了一个组合PID和LWP ID来唯一标识进程的特定线程。
查看轻量级进程
ps -aL
解释线程创建在共享区原因
当一个进程创建新的线程时,新线程与原始线程共享相同的地址空间,是因为线程设计的初衷就是为了在共享内存空间中并发执行代码,就可以跟容易的共享数据和资源。
通过共享地址空间,线程可以更快访问和修改数据,因为它们不需要像进程依靠内核进行上下文切换和数据复制。
也会有缺点的出现,因为线程共享内存,就要做好安全措施,避免多个线程访问同一块地址,导致数据不一致,造成错误。
clone系统调用是pthread_create的底层实现,在Linux系统中使用库函数创建线程时,实际上底层可能使用clone系统调用来实现。clone系统调用允许创建一个新的进程,与fork调用不同,clone提供了更细粒度的控制。
fork()
和clone()
都可以创建新的执行上下文,并且都可以共享资源。然而,clone()
提供了更细粒度的控制,允许开发者根据具体需求选择共享哪些资源,而fork()
则提供了一种更简单但更有限的资源共享方式。这种灵活性使得clone()
在创建线程或轻量级进程时非常有用。
库函数创建线程时,操作系统会执行下面步骤找到和管理这些线程
1.库函数调用:如pthread_create类函数请求创建一个线程
2.封装clone调用:库函数内部会封装对clone系统调用的调用,clone系统调用允许出现指定要共享那些资源。
3.设置线程属性:在调用clone之前,库函数会根据提供线程属性(如栈的大小,优先级等)来设置相关属性。
4.执行clone调用:库函数会执行clone系统调用,传递必要的参数,操作系统就会处理这个调用,创建一个新的进程。
5.分配资源:操作系统为新线程分配必要的资源,共享资源和私有资源
6.将新线程加入调度队列:新线程资源分配和设置好,操作系统会将其加入到调度队列中,等待调度器选择执行。
7.线程调度和执行:调度器会根据一定的算法从调度队列选择一个线程执行,选到新创建的线程时,就开始执行线程的代码。
当然可以!根据图片内容和之前的讨论,以下是Linux中创建线程的流程:
创建线程的流程
用户态调用`pthread_create`•用户程序通过调用pthread_create函数来请求创建一个新线程。
•pthread_create函数是POSIX线程标准的实现,其原型如下:
> ```c
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
```•thread:用于存储新线程的线程ID。
•attr:线程属性,如栈大小、线程分离状态等。
•start_routine:新线程的入口函数。
•arg:传递给入口函数的参数。
线程库处理线程属性•pthread_create函数内部会处理线程的属性,包括栈大小、线程分离状态、优先级等。
调用`clone`系统调用•pthread_create函数会调用clone系统调用,将线程创建请求传递给内核。
•clone系统调用的原型如下:
> ```c
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...);
```•fn:新线程的入口函数。
•child_stack:新线程的栈空间。
•flags:指定线程的属性,如是否共享内存空间、文件描述符等。
•arg:传递给入口函数的参数。
内核接收`clone`系统调用•内核接收到clone系统调用后,开始创建新线程。
•内核会为新线程分配一个唯一的线程标识符(TID)。
内核创建线程控制块(`task_struct`)•内核会为新线程分配一个task_struct结构,用于存储线程的执行状态和上下文信息。
•task_struct中包含的内容包括:
•线程ID
•寄存器状态
•栈指针
•线程状态(就绪、运行、等待等)
•文件描述符表
•信号处理机制
内核初始化线程上下文•内核会初始化新线程的上下文,包括栈空间、寄存器状态、文件描述符表、信号处理机制等。
•新线程共享父线程的地址空间、文件描述符表和信号处理机制。
内核将线程加入调度队列•内核将新线程加入到调度队列中,等待被调度执行。
•新线程的状态被设置为就绪状态。
返回到用户空间•内核完成线程的创建后,返回到用户空间。
•pthread_create函数在父线程中返回新线程的线程ID(pthread_t),在新线程中执行入口函数start_routine。
新线程开始执行•新线程从入口函数start_routine开始执行。
•新线程可以访问父线程共享的资源,如地址空间、文件描述符等。
图片中的关键点
•`task_struct`:内核为每个线程维护一个task_struct结构,用于存储线程的控制信息。•`mmap`动态映射区域:线程可以使用mmap系统调用动态映射内存区域,这些区域可以用于共享内存、文件映射等。
•库映射:pthread库文件会被映射到进程的地址空间中,以便线程可以访问库中的函数和数据。
•物理内存:内核管理物理内存,线程的栈空间、堆空间等都会在物理内存中分配。
希望这个流程能帮助你更好地理解Linux中创建线程的过程!如果有任何具体问题或需要进一步的解释,请告诉我。
总结:线程的创建涉及到内核和用户态两个部分,
线程控制块(TCB)
是内核用来管理线程的数据结构,包含了线程的各种信息(如状态,优先级,栈信息等)。线程TCB的起始地址就是线程tid,每个线程通常都有自己独立的栈结构,这个栈结构是由操作系统在创建
时分配,并且由pthread库和内核共同维护。这个栈用于存储线程的局部变量,函数调用的信息等。在Linux上,线程的栈的通常时通过mmap系统调用分配,并且可以在创建线程时通过pthread_attr_t属性对象来设置栈的大小和其它属性。
在Linux中,mmp(Memory Map)是一个系统调用,它允许程序将一个文件或设备的一部分或其它对象映射进内存。但是在创建上下文中,mmap通常被用于动态分配内存区域,特别是为线程栈分配内存。mmap是一个强大的系统调用,它允许程序以灵活的方式管理内存。创建线程时,mmap可能被作用于一种机制来分配和管理线程栈。
TID是内核级别的线程ID,而pthread_t ID是用户级的ID。TID在整个系统是唯一的,pthread_t在同一进程内是唯一的。
用户线程和LWP联动
用户空间中的线程通过pthrad库来管理的,该库提供了创建,同步和管理线程的接口。
内核空间的线程提供task_struct结构体标识。
pthread_t是一个pthread库定义的一个类型,用于在用户空间标识唯一一个线程,它通常是一个指针,指向struct pthread结构体。task_struct包含了描述线程的数据结构,包含了线程的状态,优先级,调度信息等。struct pthread是用户空间中描述线程的数据结构,包含了线程的局部存储,状态的信息,调度信息等。内核根据task_struct来管理和调度线程。内核根据线程的状态和优先级来决定哪一个线程获得CPU时间片。
当使用 pthread_create
创建线程时,首先在用户空间中为新线程分配和初始化一个 struct pthread
结构体。这个结构体包含了线程的局部存储(TLS)、状态信息、以及指向内核线程(LWP)的引用等。
例子:张三叫李四去买饭,而张三就在屋里打游戏等饭吃,李四就替张三去买饭,在食堂排队等饭,买饭回来送给张三。
张三 代表用户空间中的一个线程,可以认为是主线程或者父线程。这个线程可以发起创建另一个线程的请求。
李四 代表由张三创建的新线程,这个新线程在用户空间中被创建后,在内核空间中对应一个轻量级进程(LWP)。
用户空间和内核空间的交互:
用户线程(张三)创建新线程(李四):
在用户空间中,张三(用户线程)调用
pthread_create
函数来创建一个新的线程,即李四。
pthread_create
函数在内部通过系统调用(如clone
)与内核交互,请求创建一个新的线程。内核创建LWP(李四):
内核接收到创建线程的请求后,会创建一个新的轻量级进程(LWP),这可以认为是李四在内核空间的表示。
这个新的LWP被赋予了执行特定任务的能力,比如“去买饭”。
线程创建信息返回:
一旦LWP(李四)被创建,内核会将控制权返回给用户空间,并且通常会通过
pthread_create
返回的pthread_t
(线程ID)来标识新创建的线程。张三(用户线程)接收到这个线程ID后,可以进一步对这个线程进行操作,比如等待它完成(
pthread_join
)或者查询它的属性。线程执行任务:
李四(LWP)开始执行分配给它的任务。在用户空间中,这可能是一个函数调用,而在内核空间中,这是LWP的调度和执行。
深层次理解回收资源
这段代码展示了如何通过
pthread_join
函数获取线程的返回值。以下是红色框框与其它部分的联系:
TCB和线程ID:
当用户调用
pthread_create
函数创建线程时,系统会分配一个TCB来存储线程的状态信息,包括线程ID(pthread_t tid
)。这个线程ID会被用于pthread_join
函数中,以标识要等待的线程。线程函数返回值:
线程函数可以返回一个值,例如
return (void*)10;
。这个返回值会被存储在TCB中,等待被主线程通过pthread_join
函数获取。动态库和TCB:
TCB存储在动态库中,动态库负责管理线程的生命周期。当线程结束时,其返回值会被存储在TCB中,直到主线程调用
pthread_join
来获取这个值。用户线程与LWP:
用户线程通过LWP实现,LWP是内核管理的最小调度单元。用户线程的创建和管理由线程库(如pthread)处理,而LWP的调度和管理则由内核负责。
pthread_join
函数在用户空间和内核空间之间进行协调,以确保线程资源的正确回收。内存布局:
线程的栈空间是线程存储局部变量和函数调用信息的地方。当线程结束时,其栈空间需要被回收。
pthread_join
函数确保了线程栈空间的正确回收,避免了资源泄漏。
3.代码演示
这里要在堆区上开辟空间,不然就会因为是临时变量而销毁空间,没次都出现在一个位置上开辟空间,往同一个位置插入元素,造成覆盖式填入,这样线程的名字就会一样,也会造成线程数据安全问题,堆区开辟空间要释放才回收,不会因为指向这个空间的指针销毁就销毁。
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <cstring>
const int num=10;
void* routine(void* args)
{
sleep(1);
std::string name=static_cast<const char*>(args);
delete (char*)args;
int cnt=5;
while(cnt--)
{
std::cout<<"new线程名字:"<<name<<std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
std::vector<pthread_t> tids;
for(int i=0;i<num;i++)
{
pthread_t tid;
char* id=new char[64];
snprintf(id,64,"thread-%d",i);
int n=pthread_create(&tid,nullptr,routine,id);
if(n==0)
tids.push_back(tid);
else
continue;
}
for(int i=0;i<num;i++)
{
int n=pthread_join(tids[i],nullptr);
if(n==0)
{
std::cout<<"等待线程成功"<<std::endl;
}
}
return 0;
}
演示2
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <cstring>
// 1. main函数结束,代表主线程结束,一般也代表进程结束
// 2. 新线程对应的入口函数,运行结束,代表当前线程运行结束
// 3. 一个问题:给线程传递的参数和返回值,可以是任意类型(包括对象)
class Task
{
public:
Task(int a, int b) : _a(a), _b(b) {}
int Execute()
{
return _a + _b;
}
~Task() {}
private:
int _a;
int _b;
};
class Result
{
public:
Result(int result) : _result(result)
{
}
int GetResult() { return _result; }
~Result() {}
private:
int _result;
};
// 线程终止的问题
// 1. 线程的入口函数,进行return就是线程终止
// 注意: 线程不能用exit()终止,因为exit是终止进程的!!!!!
// 2. pthread_exit();
// 3. 线程如果被取消,退出结果是-1【PTHREAD_CANCELED】
// void* p : 开辟空间的
void *routine(void *args)
{
pthread_detach(pthread_self());
std::cout << "新线程被分离" << std::endl;
int cnt = 5;
while (cnt--)
{
std::cout << "new线程名字: " << std::endl;
sleep(1);
}
return nullptr;
// Task *t = static_cast<Task*>(args);
// sleep(100);
// Result *res = new Result(t->Execute());
// sleep(1);
// // return res;
// // exit(13);
// pthread_exit(res);
// std::cout << "haha, 新线程不应该看到这里" << std::endl;
}
/ // 如果主线程不想再关心新线程,而是当新线程结束的时候,让他自己释放??
// 设置新线程为分离状态
// 技术层面: 线程默认是需要被等待的,joinable。如果不想让主线程等待新线程
// 想让新线程结束之后,自己退出,设置为分离状态(!joinable or detach) // TODO
// 理解层面:线程分离,主分离新,新把自己分离。
// 分离的线程,依旧在进程的地址空间中,进程的所有资源,被分离的线程,依旧可以访问,可以操作。
// 主不等待新线程。
// 分离操作
// 如果线程被设置为分离状态,不需要进行join,join会失败!!
int main()
{
pthread_t tid;
// Task *t = new Task(10, 20);
pthread_create(&tid, nullptr, routine, (void *)"thread-1");
// pthread_detach(tid);
// std::cout << "新线程被分离" << std::endl;
int cnt = 5;
while (cnt--)
{
std::cout << "main线程名字: " << std::endl;
sleep(1);
}
int n = pthread_join(tid, nullptr);
if (n != 0)
{
std::cout << "pthread_join error: " << strerror(n) << std::endl;
}
else
{
std::cout << "pthread_join success: " << strerror(n) << std::endl;
}
// sleep(3);
// pthread_cancel(tid);
// std::cout << "新线程被取消" << std::endl;
// void *ret = nullptr;
// // 默认线程无异常
// pthread_join(tid, &ret); // pthread_join: 拿到的返回值,就是线程退出设定的返回值
// std::cout << "新线程结束, 运行结果: " << (long long)ret << std::endl;
// Result* ret = nullptr;
// pthread_join(tid, (void**)&ret);
// int n = ret->GetResult();
// std::cout << "新线程结束, 运行结果: " << n << std::endl;
// delete t;
// delete ret;
return 0;
}