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变量的开销更小
#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>void * routine(void * args)
{std::string name=static_cast<const char*>(args);int cnt=10;while(cnt--){std::cout<<"线程名字:"<<name<<std::endl;sleep(1);}return (void*)10;
}
int main()
{pthread_t tid;
pthread_create(&tid,nullptr,routine,(void*)"thread-1");int cnt=5;
while(cnt--)
{std::cout<<"main线程名字:"<<std::endl;sleep(1);
}
void *ret=nullptr;
pthread_join(tid,&ret);
std::cout<<"新线程结束,退出码:"<<(long long)ret<<std::endl;
}
1.main函数结束,代表主线程结束,一般代表进程结束
2.新线程对应的入口函数运行结束,代表当前线程运行结束
3.给线程传递的参数和返回值,可以是任意类型
有一个任务Task,Result计算任务结果,通过参数交给线程处理,再以退出码返回得到结果
#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>class Task
{public:Task(int a,int b):_a(a),_b(b){}~Task(){}int Exectue(){return _a+_b;}private:int _a;int _b;
};class Result
{public:Result(int result):_result(result){}~Result(){}int GetResult(){return _result;}private:int _result;
};void * routine(void * args)
{Task *t =static_cast<Task*>(args);sleep(1);Result *res=new Result(t->Exectue());sleep(1);return (void *)res;
}
int main()
{pthread_t tid;
Task *t=new Task(10,20);
pthread_create(&tid,nullptr,routine,t);Result *ret=nullptr;
pthread_join(tid,(void**)&ret);
std::cout<<"新线程结束,运行结果:"<<ret->GetResult()<<std::endl;
}
线程ID
pthread_self
#include <pthread.h>
// 获取线程ID
pthread_t pthread_self(void);
打印出来的 tid 是通过 pthread 库中有函数 pthread_self 得到的,它返回一个 pthread_t 类型的
变量,指代的是调用pthread_self 函数的线程的 “ID”。
怎么理解这个“ID”呢?这个“ID”是 pthread 库给每个线程定义的进程内唯⼀标识,是 pthread 库
维持的。
由于每个进程有自己独立的内存空间,故此“ID”的作用域是进程级而非系统级(内核不认识)。
其实 pthread 库也是通过内核提供的系统调用(例如clone)来创建线程的,而内核会为每个线程创建系统全局唯一的“ID”来唯一标识这个线程。
$ ps -aL | head -1 && ps -aL | grep mythread
PID
LWP TTY
TIME CMD
2711838 2711838 pts/235 00:00:00 mythread
2711838 2711839 pts/235 00:00:00 mythread
-L 选项:打印线程信息
LWP 是什么呢?LWP 得到的是真正的线程ID。之前使用 pthread_self 得到的这个数实际上是⼀
个地址,在虚拟地址空间上的⼀个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程ID,线程栈,寄存器等属性。
在 ps -aL 得到的线程ID,有⼀个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在虚拟
地址空间的栈上,而其他线程的栈在是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的。而pthread库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。
长话短说:线程ID不同于进程ID,进程ID是操作系统给进程分配的统一标识方便进程管理,而线程ID是线程内部方便管理线程而统一标识的,操作系统不认线程ID,同一进程的不同线程PID相同,而LWD(TID)不同,对PID操作相当于对所有TID操作,对某个TID操作一般不影响同一个进程下的其他线程
线程终止
不能使用exit()来结束线程,exit是操作系统对进程的结束命令,调用后会杀手全部同一进程下的全部线程,所有得用专门的接口
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
2. 线程可以调用pthread_ exit终止自己。
3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
pthread_exit
功能:线程终⽌
原型:
void pthread_exit(void *value_ptr);
参数:
value_ptr:value_ptr不要指向⼀个局部变量。
返回值:
⽆返回值,跟进程⼀样,线程结束的时候⽆法返回到它的调⽤者(⾃⾝)
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了,再访问就是野指针。
pthread_cancel函数
功能:取消⼀个执⾏中的线程
原型:
int pthread_cancel(pthread_t thread);
参数:
thread:线程ID
返回值:成功返回0;失败返回错误码
线程等待
为什么需要线程等待?
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
pthread_join
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数:
thread:线程ID
value_ptr:它指向⼀个指针,后者指向线程的返回值
返回值:成功返回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参数。
线程分离
pthread_join
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则
无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。分离的线程在未退出的情况下,依旧在进程的地址空间中,进程的所有资源,被分离的进程依旧可以访问,可以操作。
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>void *routine(void *args)
{int cnt = 5;while (cnt--){std::cout << "新线程" << std::endl;sleep(1);}return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, routine, (void *)"pthead-1");// 主分新pthread_cancel(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;}
}
被分离的线程不能被线程等待,会失败
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>void *routine(void *args)
{int cnt = 5;while (cnt--){std::cout << "新线程" << std::endl;sleep(1);}return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, routine, (void *)"pthead-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;}
}
线程ID及进程地址空间布局
Linux本身没有线程,它是用轻量级进程模拟出来的,OS提供的接口,不会直接提供线程接口,在用户层,封装轻量级进程,形成原生线程库:
可执行程序依赖对应的库,两个都是ELF可执行程序,我们可执行程序加载,形成进程,动态链接和动态地址重定向,要将动态库加载到内存还要映射到当前进程的地址空间中
当我们运行可执行文件,首先进程创建一个test——struct的结构体,创建地址空间,链接库,将pthread加载到内存并映射到地址空间,我自己写的代码会用到pthead中的变量方法,采用库的起始地址+偏移量就能访问库的内容,线程的概念是在库里维护的,在库内部,就一定会存在多个被创建好的线程,库也要管理线程,线程也有自己独立的栈
先描述,再组织,我们没创建一个线程,就会在库内创建一个描述线程的结构体,开头就是描述线程的TCB,struct pthread存有各种属性,线程局部存储可以存放各个线程单独的变量,独立访问,再次创建线程,会在上一个线程的实例化后面紧挨着再次创建对应的TCB,而pthread_create()有一个很大的返回值,那不是LWP,而是线程在库当中对应管理块的虚拟地址,线程控制块种有一个void* ret;当这个控制块对应的代码执行完会把返回值写到对应的ret内,线程运行结束资源并没有被释放,所以线程需要pthread_join,线程结束只是函数结束完了,对应的管理块并没有被释放,pthread_join种传入的tid就是上一个执行完的线程的tid,通过该函数再把上一个线程的运行结果带出开,然后释放该控制块的资源,并解决内存泄漏
所以,我在自己的代码里面调用pthread_create,是在动态库内部帮我们创建动态库描述的控制块,管理块的开头叫TCB,里面包含了描述线程的相关信息,而TCB里包含了void *ret字段,而当前线程运行结束的时候,会把自己的退出状态写到自己的控制块ret里,然后主线程又和新线程共享地址空间,主线程也就能通过起始地址+偏移量找到对应线程的控制块,主线程通过pthread_join拿到对应的退出状态拷贝回来,进程结束LWP自动释放,但是库里的东西没被释放,LWP查不到内存泄漏
- pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
- 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
- pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
- 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
pthread_t pthread_self(void);
pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类
型的线程ID,本质就是一个进程地址空间上的一个地址。
用户线程VSLWP
调用pthread_create,在库中创建管理块,要在内核创建轻量级进程(调用系统调用clone),既要创建出对应的控制块也要创建出对应线程,第一个参数是传入多线程函数执行的入口地址,第二个传入线程私有的独栈结构地址,这样内核数据和用户数据就联动起来了,将来执行的就是传入的这部分方法和结构,将来调函数栈帧和临时变量在线程栈里执行,CPU调度时临时数据在用户空间保存
创建多线程
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include<vector>void *routine(void *args)
{ 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;
}
const int num=10;
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.emplace_back(tid);}elsecontinue;sleep(1);
}for(int i=0;i<num;i++){//一个一个等int n= pthread_join(tids[i],nullptr);if(n==0){std::cout<<"等待新线程成功"<<std::endl;}}return 0;
}