目录
等待线程pthread_join的参数(value_ptr)
POSIX线程库
- 与线程有关的函数构成了⼀个完整的系列,绝⼤多数函数的名字都是以“pthread_”打头的
- 要使⽤这些函数库,要通过引入头文件<pthread.h>
- 链接这些线程函数库时要使⽤编译器命令的“-lpthread”选项 eg: g++ -o $@ $^ -lpthread
- 这个pthread库是linux自带的原生线程库 , C++11也有线程<thread>库,在Linux上本质就是封装了<pthread>库,
C++11 的
<thread>库
是跨平台的,在 Windows 上可能封装CreateThread
,在 macOS 上封装pthread
,在 Linux 上通常封装pthread
。
创建线程
功能:创建⼀个新的线程
原型:
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)
vfork已经基本被废除了
在linux内核看来 ,只有LWP ,所以向外只提供了clone一种系统调用, 通过封装clone, fork可以创建子进程 ,pthread_creat可以创建线程.
但是在Windows内核下,提供了创建进程的syscall 和创建线程的syscall (因为windos中确实单独实现了线程 和进程) linux下只有LWP
错误检查:
- 传统的⼀些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指⽰错误。
- pthreads函数出错时不会设置全局变量errno(⽽⼤部分其他POSIX函数会这样做)。⽽是将错误代码通过返回值返回
- pthreads同样也提供了线程内的errno变量,以⽀持其它使⽤errno的代码。对于pthreads函数的错误,建议通过返回值l来判定,因为读取返回值要⽐读取线程内的errno变量的开销更⼩
创建线程pthread_create的传参(arg)
参数 arg是传入的函数指针(回调函数)的对应的参数(void*)
32位机器下 void* 是4Byte 64位下是8Byte ,这时void * 与地址变量的大小一样
为什么传void *呢?
这个参数可以传递任意类型(通过指针+类的封装) ,一般传的是指针
1. 传递单个参数
如果线程函数只需要一个参数,可以直接传递该参数的地址。例如:
#include <pthread.h>
#include <stdio.h>
void* thread_task(void* arg) {
int num = *(int*)arg;
printf("Thread received parameter: %d\n", num);
return NULL;
}
int main() {
pthread_t tid;
int value = 45;
pthread_create(&tid, NULL, thread_task, (void*)&value);
pthread_join(tid, NULL);
return 0;
}
2. 传递多个参数
如果需要传递多个参数,通常会将这些参数封装到一个结构体中,然后传递该结构体的地址。例如
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
class ThreadData
{
public:
ThreadData(const std::string &name ,int a ,int b)
:_name(name)
,_a(a)
,_b(b)
{}
~ThreadData()
{}
int excute()
{
return _a+_b;
}
std::string name()
{
return _name;
}
private:
std::string _name;
int _a;
int _b;
};
std::string ToHex(pthread_t tid)
{
char buff [64];
snprintf(buff ,sizeof(buff) ,"0x%lx",tid);
return buff;
}
void *routine (void* args)
{
//static_cast 是 C++ 中的 静态类型转换运算符,用于在 编译时 进行类型检查的显式转换。它比 C 风格的强制转换 (type)value 更安全
//使用方法:static_cast<目标类型>(表达式)
ThreadData* td =static_cast<ThreadData* >(args);
while(true)
{
std::cout<<"我是子线程 我的名字是 "<<td->name()<<", my id is"<< ToHex(pthread_self()) <<std::endl;
std::cout<<"task result is"<<td->excute()<<std::endl;
sleep(1);
}
return 0;
}
int main()
{
ThreadData *td =new ThreadData("thread-1" , 10,20);
pthread_t tid;
int n =pthread_create(&tid ,nullptr ,routine,(void*)td);
if(n!=0 )
{
std::cout<<"creat thread error"<<std::endl;
return 1;
}
while(true)
{
std::cout<<"我是main线程 "<<", my id is"<< ToHex(pthread_self()) <<std::endl;
sleep(2);
}
return 0;
}
创建一个线程
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
void* routine (void* args)
{
//static_cast 是 C++ 中的 静态类型转换运算符,用于在 编译时 进行类型检查的显式转换。它比 C 风格的强制转换 (type)value 更安全
//使用方法:static_cast<目标类型>(表达式)
std::string name = static_cast<const char *>(args);// 将 args(某种类型)显式转换为 const char *,并用它构造一个 std::string 对象 name
while(true)
{
std::cout<<"我是子线程 我的名字是 "<<name<<std::endl;
sleep(1);
}
return 0;
}
int main()
{
pthread_t tid;
int n =pthread_create(&tid ,nullptr ,routine,(void*)"thread-1");
if(n!=0 )
{
std::cout<<"creat thread error"<<std::endl;
return 1;
}
while(true)
{
std::cout<<"我是main线程 "<<std::endl;
sleep(1);
}
return 0;
}
每个线程都有自己的ID ,在创建时就以第一个参数形式输出了,也可以使用函数pthread_self( ) 获取自己线程的ID
两个线程同时访问同一个公共资源有可能引发错误(像显示器打印,在不加保护的情况下,显示器也是公共资源)下面可以看到打印错乱,就是这个原因,就是我们以前讲的重入.
我们可以看到main线程和子线程的ID不同
总结:
- 新线程和main线程谁先运行 ,不确定
- 线程要瓜分进程的资源,包括时间片
- 不加保护的情况下 ,显示器文件就是公共资源
- 对于多线程代码 ,进程内的函数 ,被所有线程共享,只要这个函数不使用全局变量(显示器文件就是全局变量),就是安全的
- 全局变量能被此进程中的全部线程看到
- 一旦一个线程出现异常,进程就崩了(被信号杀死了),其它线程也就全崩了 例如:一个线程中使用了野指针 ,查页表时失败了,MMU报错 ,导致CPU触发软中断 ,OS发送信号杀死进程.
- 线程创建后也是要被等待和回收的(主线程像父进程一样一定是最后退出的)
- 线程的栈 ,是独立的栈 ,不是私有的栈 ,如果将线程1中栈上的数据的地址给同一个进程的其它线程2 ,线程2也可以使用或修改其中数据(不会这么用 )
终止线程
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从新线程return。 这种⽅法对主线程不适⽤, 从main函数return相当于调用exit ,整个进程就退出来。
- 线程可以调用 pthread_exit终⽌⾃⼰。
- ⼀个线程可以调用 pthread_cancel 终⽌同⼀进程中的另⼀个线程。(一般都是主线程调用pthread_cannel( ) 终止其他进程 ) 不推荐使用
- 重要重要重要: 进程中不管任何地方调用exit , 整个进程就会终止.
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;失败返回错误码
等待线程
为什么需要线程等待?
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复⽤刚才退出线程的地址空间。
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数:
thread:线程ID
输出型参数value_ptr:它指向⼀个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码(一般大于0)
调用该函数的线程将挂起等待(阻塞),直到 id 为thread的线程终止 。thread线程以不同的方法终止(三种方式), 通过 pthread_join得到的终⽌状态是不同的,总结如下
- 如果thread线程通过return返回 ,value_ptr所指向的单元(将void*作为了容器)⾥存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调⽤pthread_cancel异常终掉,value_ptr所指向的单元⾥存放的是常数PTHREAD_CANCELED,也就是 -1。
- 如果thread线程是⾃⼰调⽤pthread_exit终⽌的,value_ptr所指向的单元(将void*作为了容器)存放的是传给 pthread_exit的参数。
- 如果对thread线程的终⽌状态不感兴趣,可以传NULL给value_ptr参数。
等待线程pthread_join的参数(value_ptr)
返回时产生了void* 的8Byte(64位)的空间,将100写入这个空间, 等待的线程将退出的线程的返回值空间的内容拷贝到他自己value_ptr所指的空间中
也就是*value_ptr = (void*) k
一般等待线程pthread_join的value_ptr所指空间(*value_ptr)接受到的是指针. (例如k是堆指针)
理论上:线程共享地址空间(包含堆)
一个线程在内部申请了堆空间,就认为这个空间被它独占(谁拿到堆空间的地址,谁就能访问).
如果想让其他线程访问它的堆空间,只需要将堆空间地址传给他即可.(应用于示例2)
示例1:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
class ThreadData
{
public:
ThreadData(const std::string &name ,int a ,int b)
:_name(name)
,_a(a)
,_b(b)
{}
~ThreadData()
{}
int excute()
{
return _a+_b;
}
std::string name()
{
return _name;
}
private:
std::string _name;
int _a;
int _b;
};
std::string ToHex(pthread_t tid)
{
char buff [64];
snprintf(buff ,sizeof(buff) ,"0x%lx",tid);
return buff;
}
void *routine (void* args)
{
//static_cast 是 C++ 中的 静态类型转换运算符,用于在 编译时 进行类型检查的显式转换。它比 C 风格的强制转换 (type)value 更安全
//使用方法:static_cast<目标类型>(表达式)
ThreadData* td =static_cast<ThreadData* >(args);
while(true)
{
std::cout<<"我是子线程 我的名字是 "<<td->name()<<", my id is"<< ToHex(pthread_self()) <<std::endl;
std::cout<<"task result is"<<td->excute()<<std::endl;
sleep(1);
break;
}
return (void *)100;
}
int main()
{
ThreadData *td =new ThreadData("thread-1" , 10,20);
pthread_t tid;
int n =pthread_create(&tid ,nullptr ,routine,(void*)td);
if(n!=0 )
{
std::cout<<"creat thread error"<<std::endl;
return 1;
}
void* ret =nullptr;
int n2 =pthread_join(tid ,&ret);
if(n2!=0)
{
std::cout<<"wait thread error"<<std::endl;
return 1;
}
//此时ret为8Byte (64位机器)
std::cout<<"wait thread succcess, ret "<<(long)ret<<std::endl;
return 0;
}
示例2:(重要)
这个join 可以保证只要join成功,新线程就执行完毕了,结果一定可信
如果创建全局变量,让新线程去处理 ,主线程要不停检查是否执行完了,用主线程的堆空间 ,传递给新线程,新线程只要返回堆指针 ,就证明处理完了.主进程拿结果即可.
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
class ThreadData
{
public:
ThreadData(const std::string &name ,int a ,int b)
:_name(name)
,_a(a)
,_b(b)
{}
~ThreadData()
{}
void excute()
{
_result = _a+_b;
}
int result()
{
return _result;
}
std::string name()
{
return _name;
}
private:
std::string _name;
int _a;
int _b;
int _result;
};
std::string ToHex(pthread_t tid)
{
char buff [64];
snprintf(buff ,sizeof(buff) ,"0x%lx",tid);
return buff;
}
void *routine (void* args)
{
ThreadData* td =static_cast<ThreadData* >(args);
while(true)
{
std::cout<<"我是子线程 我的名字是 "<<td->name()<<", my id is"<< ToHex(pthread_self()) <<std::endl;
td->excute();
sleep(1);
break;
}
return td;
}
int main()
{
ThreadData *td =new ThreadData("thread-1" , 10,20);
pthread_t tid;
int n =pthread_create(&tid ,nullptr ,routine,(void*)td);
if(n!=0 )
{
std::cout<<"creat thread error"<<std::endl;
return 1;
}
ThreadData* rtd =nullptr;
int n2 =pthread_join(tid ,(void**)&rtd);
if(n2!=0)
{
std::cout<<"wait thread error"<<std::endl;
return 1;
}
//此时ret为8Byte (64位机器)
std::cout<<"wait thread succcess, ret "<<rtd->result()<<std::endl;
delete(td);
return 0;
}
创建多线程实例:
创建多线程过程(多线程如何操作):
1. 处理数据
2. 创建多线程
3. 等待多线程
4. 汇总数据
// class ThreadData
// {
// public:
// ThreadData()
// {}
// void Init(const std::string &name, int a, int b)
// {
// _name = name;
// _a = a;
// _b = b;
// }
// void Excute()
// {
// _result = _a + _b;
// }
// int Result(){ return _result; }
// std::string Name(){ return _name; }
// void SetId(pthread_t tid) { _tid = tid;}
// pthread_t Id() { return _tid; }
// int A(){return _a;}
// int B(){return _b;}
// ~ThreadData()
// {}
// private:
// std::string _name;
// int _a;
// int _b;
// int _result;
// pthread_t _tid;
// };
// #define NUM 10
// int main()
// {
// // ThreadData td[NUM];
// // // 准备我们要加工处理的数据
// // for(int i = 0; i < NUM; i++)
// // {
// // char id[64];
// // snprintf(id, sizeof(id), "thread-%d", i);
// // td[i].Init(id, i*10, i*20);
// // }
// // // 创建多线程
// // for(int i = 0; i < NUM; i++)
// // {
// // pthread_t id;
// // pthread_create(&id, nullptr, routine, &td[i]);
// // td[i].SetId(id);
// // }
// // // 等待多个线程
// // for(int i =0; i< NUM;i++)
// // {
// // pthread_join(td[i].Id(), nullptr);
// // }
// // // 汇总处理结果
// // for(int i =0;i < NUM; i++)
// // {
// // printf("td[%d]: %d+%d=%d[%ld]\n", i, td[i].A(), td[i].B(), td[i].Result(), td[i].Id());
// // }
// }
线程分离
想让主线程不等待新线程(主线程去做自己的事)
注意:我们要保证在多执行流下 ,主执行流是最后退出的
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进⾏pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 线程两种被等待的状态: 1. joinable :线程需要被主线程等待 2. detach:线程被分离(主线程不需要等待新线程)
- 如果不关心线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
线程分离pthread_detach
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对⽬标线程进⾏分离,也可以是线程⾃⼰分离:
pthread_detach(pthread_self());
或者
pthread_detach(tid);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_run( void * arg )
{
pthread_detach(pthread_self());
printf("%s\n", (char*)arg);
return NULL;
}
int main( void )
{
pthread_t tid;
if ( pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0 ) {
printf("create thread error\n");
return 1;
}
int ret = 0;
sleep(1);//很重要,要让线程先分离,再等待 分离后 等待会失败
if ( pthread_join(tid, NULL ) == 0 ) {
printf("pthread wait success\n");
ret = 0;
} else {
printf("pthread wait failed\n");
ret = 1;
}
return ret;
}
线程创建进程
任何一个线程不能调用exec函数进行程序的替换
但是线程可以先创建(fork)一个子进程,再调用exec函数
创建出来的子进程 ,会将线程所属当前进程的页表 ,地址空间全都拷贝一份(不会拷贝PCB),形成的子进程只有他自己的PCB
void *start(void *args)
{
//可以的!
pid_t id = fork();
if(id == 0)
{
// ....
}
return 0;
}
线程库
- pthread_create函数第⼀个参数指向⼀个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
- 同一进程的线程共享地址空间,线程库加载在地址空间的共享区 ,被加载到地址空间中的线程库,对应的进程中有几个线程 ,其线程库中就会有几个线程属性结构,线程退出 ,地址空间中的线程库中线程属性结构体也会随之释放。
- 线程ID(tid)是此线程在线程库中属性结构体在地址空间的地址 ,具有唯一性
- linux内核中只认识LWP ,线程库pthread库封装了Linux的LWP的创建接口以及LWP属性接口 ,在用户层面产生了线程的概念
- 线程库NPTL提供了pthread_self函数,可以获得线程⾃⾝的ID:
- 一个线程退出 ,将退出码写在他的tcb中,主线程通过jion可以拿到
动态库中存在代码中数据的属性集合是如何做到的呢?
FILE
由 C 标准库<stdio.h>
维护。
这个其实原来接触过 ,例如文件操作中的FILE就是存储在C 标准库中的 ,是在C 标准库中malloc的一块空间 ,打开多少个文件就malloc了多少个FILE
pthread_t 到底是什么类型呢?取决于实现。对于Linux⽬前实现的NPTL实现⽽⾔,pthread_t类 型的线程ID,本质就是⼀个进程地址空间上的⼀个地址。
线程栈
虽然Linux将线程和进程不加区分的统⼀到了task_struct ,但是对待其地址空间的 stack 还是有些区别的。
地址空间中的栈是主线程的栈,其他线程的栈在共享区的动态库中(动态申请)
- 对于Linux进程或者说主线程,简单理解就是main函数的栈空间,在fork的时候,实际上就是复制了⽗亲的 stack 空间地址,然后写时拷⻉(cow)以及动态增⻓。如果扩充超出该上限则栈溢出会报段错误(发送段错误信号给该进程)。进程栈是唯⼀可以访问未映射⻚⽽不⼀定会发⽣段错误⸺⸺超出扩充上限才报。
- 然⽽对于主线程⽣成的⼦线程⽽⾔,其 stack 将不再是向下⽣⻓的,⽽是事先固定下来的。线程栈⼀般是调⽤glibc/uclibc等的 pthread 库接⼝ pthread_create 创建的线程,在⽂件映射区(或称之为共享区)。其中使⽤ mmap 系统调⽤,这个可以从 glibc 的 nptl/allocatestack.c 中的 allocate_stack 函数中看到:
mem = mmap (NULL, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
- 此调⽤中的 size 参数的获取很是复杂,你可以⼿⼯传⼊stack的⼤⼩,也可以使⽤默认的,⼀般⽽ ⾔就是默认的 8M 。这些都不重要,重要的是,这种stack不能动态增⻓,⼀旦⽤尽就没了,这是和⽣成进程的fork不同的地⽅。在glibc中通过mmap得到了stack之后,底层将调⽤ sys_clone 系 统调⽤
int sys_clone(struct pt_regs *regs)
{
unsigned long clone_flags;
unsigned long newsp;
int __user *parent_tidptr, *child_tidptr;
clone_flags = regs->bx;
//获取了mmap得到的线程的stack指针
newsp = regs->cx;
parent_tidptr = (int __user *)regs->dx;
child_tidptr = (int __user *)regs->di;
if (!newsp)
newsp = regs->sp;
return do_fork(clone_flags, newsp, regs, 0, parent_tidptr, child_tidptr);
}
因此,对于⼦线程的 stack ,它其实是在进程的地址空间中map出来的⼀块内存区域,原则上是 线程私有的,但是同⼀个进程的所有线程⽣成的时候,是会浅拷⻉⽣成者的 task_struct 的很多 字段,如果愿意,其它线程也还是可以访问到的,于是⼀定要注意