目录
pthread_create(创建线程)
第一个参数为系统给我们封装的结构,可以理解为新创建的线程ID,输出型参数。
第二个参数可以设置线程的属性,比如栈的大小,一般都把这个参数设置为nullptr
第三个参数(routine:例程)函数指针,返回值为oid*,参数也为void*,本质就是一个回调函数,调用新创建的线程去执行函数指针所指向的方法,值得注意的是main就是主线程的入口函数
还有一个我们需要关注的点,这里是void*而不是void,void是不能定义变量,因为void的大小是不确定的无法分配空间,void*是能用来定义变量的,并且它能够用来接收和返回任意指针类型(在C语言中可以让代码泛型),接收字符串常量时,要强转比如:(void*)"thread-1";
在windows下默认为32位,指针大小位4字节
在Linux下,默认位64位
第四个参数是输入型参数,创建线程成功,新线程回调线程函数的时候,需要参数。这个参数就是给线程函数传递的,不需要,传nullptr
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
int val=100;
void* routine(void*arg)
{
const char*name=(const char*)arg;
while(1)
{
printf("name:%s,pthread pid:%d val:%d &val:%p\n",name,getpid(),val,&val);
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,routine,(void*)"thread-1");
while(1)
{
printf("main pthread pid:%d val:%d &val:%p,tid:%p\n",getpid(),val,&val,tid);
sleep(1);
val++;
}
return 0;
}
接口返回值
成功返回0,失败返回错误码
运行代码(注意事项)
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
void* routine(void*arg)
{
while(1)
{
cout<<"new pthread:"<<getpid()<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,routine,nullptr);
while(1)
{
cout<<"main pthread:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
执行报错
命令行执行,链接时报错
记住一点,pthread_create接口不是系统调用,而是一个库方法,pthred库是一个第三方库
需要链接,才能使用,使用手册里也有所说明。因为pthread本来就在系统里,所有只需要-l
LWP(light weight process)
使用ps ajx查看进程也只看到了一个对应进程,因此我们确实是创建了一个新的执行流
ps -aL查看所有线程 L:light(轻)
在这里我们看到了两个mythread,同一个进程内的两个执行流,相同的PID
LWP:轻量级进程标识符,CPU调度时就是按照LWP调度的
我们发现有一个mythread的PID和LWP相同,这代表这个就是主线程,不同就是新线程
操作系统也是依靠这个特性区分是主线程还是新线程
线程不具有独立性
任何一个线程因为异常问题被关掉,默认整个进程都会被干掉
发送信号是发给进程,还是发给线程?
在这里,我们认为是发个进程的,因为每一个线程都是进程的一个执行分支
全局变量
无论是已初始化还是未初始化的,所有线程共享,因为和
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
int val=100;
void* routine(void*arg)
{
while(1)
{
printf("new pthread pid:%d val:%d &val:%p\n",getpid(),val,&val);
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,routine,nullptr);
while(1)
{
printf("main pthread pid:%d val:%d &val:%p\n",getpid(),val,&val);
sleep(1);
val++;
}
return 0;
}
通过打印我们发现tid并不是LWP,因此LWP只是操作系统层的概念,用户层并没有这个概念
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,routine,nullptr);
while(1)
{
printf("main pthread pid:%d val:%d &val:%p,tid:%p\n",getpid(),val,&val,tid);
//cout<<"main pthread:"<<getpid()<<endl;
sleep(1);
val++;
}
return 0;
}
线程等待—>pthread_join
我们应该明确一点,主线程和新创建的线程,谁先跑,我们可能不清楚,但谁最后退出,我们应该清楚
线程等待的两个目的:防止造成内存泄漏,获取新线程的退出状态
代码演示
main thread等待时,默认为阻塞等待
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
int val=100;
void* routine(void*arg)
{
const char*name=(char*)arg;
int cnt=5;
while(1)
{
printf("name:%s,pthread pid:%d val:%d &val:%p\n",name,getpid(),val,&val);
//cout<<"new pthread:"<<getpid()<<endl;
cnt--;
if(cnt==0)
{
break;
}
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,routine,(void*)"thread-1");
pthread_join(tid,nullptr);
sleep(5);
cout<<"main quit..."<<endl;
return 0;
}
接收线程的返回值
可以看到的是线程执行函数的返回值是void*,那里在主线程里也应该定义一个void*,要将函数里面的值获取出来,我们需要传void*类型变量的地址或引用,但在C语言里没有引用,所以这这里使用了两级指针作为参数。
补充一点:任何类型传参都会形成临时变量,进行拷贝,形参实例化
x为形参实列化,retval是我们自己定义的void*类型参数,??代表函数的返回值,这个操作是在pthread库里面操作的,因为pthread_join是库接口
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
int val=100;
void* routine(void*arg)
{
const char*name=(char*)arg;
int cnt=5;
while(1)
{
printf("name:%s,pthread pid:%d val:%d &val:%p\n",name,getpid(),val,&val);
cnt--;
if(cnt==0)
{
break;
}
sleep(1);
}
return (void*)1;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,routine,(void*)"thread-1");
void*ret;
pthread_join(tid,&ret);
sleep(5);
cout<<"main quit... ret::"<<(long long int)ret<<endl;//因为在64位机器下,指针为8字节,所以不能强转为(int4字节)
return 0;
}
为什么我们在join的时候不考虑异常呢?
做不到,一个线程出现异常,整个进程都崩了
线程终止—>pthread_exit和return
exit是用来终止进程的,不能用来终止线程
主线程退出,其它线程都要退出
代码演示
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
int val=100;
void* routine(void*arg)
{
const char*name=(char*)arg;
int cnt=5;
while(1)
{
printf("name:%s,pthread pid:%d val:%d &val:%p\n",name,getpid(),val,&val);
cnt--;
if(cnt==0)
{
break;
}
sleep(1);
}
pthread_exit((void*)100);
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,routine,(void*)"thread-1");
void*ret;
pthread_join(tid,&ret);
cout<<"main quit... ret::"<<(long long int)ret<<endl;//因为在64位机器下,指针为8字节,所以不能强转为(int4字节)
return 0;
}
线程取消——>pthread_cancel
无论新线程是否执行完,立即取消一个线程
代码演示
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
int val=100;
void* routine(void*arg)
{
const char*name=(char*)arg;
int cnt=5;
while(1)
{
printf("name:%s,pthread pid:%d val:%d &val:%p\n",name,getpid(),val,&val);
cnt--;
if(cnt==0)
{
break;
}
sleep(1);
}
pthread_exit((void*)100);
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,routine,(void*)"thread-1");
sleep(1);
pthread_cancel(tid);
void*ret;
pthread_join(tid,&ret);
cout<<"main quit... ret::"<<(long long int)ret<<endl;//因为在64位机器下,指针为8字节,所以不能强转为(int4字节)
return 0;
}
这里返回值为什么为-1呢?
如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED(宏)
语言的跨平台性
语言之所以具有跨平台性,是因为在不同平台下,安装不同的库版本
库的不同保证了语言的可移植性
拿C++11的多线程举例
在Linux下,C++11中的多线程 ,底层是用Linux中的pthread原生线程库封装实现的
在下面代码中,完全使用的是C++11多线程,但是在指令编译依旧不变
在Windows下,C++11中的多线程 ,底层是Windows的线程接口封装实现的
一般情况下,都推荐直接使用语言的线程库,因为系统调用接口是不具备可移植性的。
获取自己的线程tid----->pthread_self
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
string to_hex(pthread_t tid)
{
char hex[64];
snprintf(hex,sizeof(hex),"%p",tid);
return hex;
}
void* func(void* arg)
{
const char* name=static_cast<const char*> (arg);
while(1)
{
cout<<"name:"<<name<<" tid:"<<to_hex(pthread_self())<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,func,(void*)"thread-1");
printf("main thread,new thread id:%p\n",tid);
pthread_join(tid,nullptr);
return 0;
}
从线程tid谈起
clone接口创建轻量级线程,pthread原生线程库里封装了这个接口,我们直接使用pthread_create即可
OS层是没有线程的概念的,只有轻量级进程的概念,线程的概念是线程库维护的
线程的tid就是线程控制块的在块当中的起始地址
另外除了主线程,所有其他线程的独立栈,都在共享区具体来讲是在pthread库中,tid指向的用户tcb(线程控制块)中。
保证了多线程调度了,不会互相干扰。
测试独立栈
下面的多线程代码,虽然调用的都是同一个函数,但是函数栈帧在不同的线程栈里面形成,里面的地址都是不同的
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<vector>
#include<string>
using namespace std;
#define Num 3
//线程数据
struct thread_data{
string threadname;
};
void Init(thread_data*td,int i)
{
td->threadname="thread-"+to_string(i);
}
//转换为16进制
string tohex(pthread_t tid)
{
char buffer[128];
snprintf(buffer,sizeof(buffer),"0x%x",tid);
return buffer;
}
//线程入口函数,此函数为可重入函数(严格来说,有临界资源)
void* routine(void*arg)
{
thread_data* td=static_cast<thread_data*> (arg);//获取参数
int i=0;
int test_i=0;
while(i<10)
{
cout<<"pid:"<<getpid()<<",name:"<<td->threadname
<<",tid:"<<tohex(pthread_self())<<",i:"<<i
<<",test_i:"<<test_i<<",&test_i:"<<tohex((pthread_t)&test_i)<<endl;//测试独立栈
i++;
test_i++;
sleep(1);
}
return nullptr;
}
int main()
{
//保存tid方便后续线程等待
vector<pthread_t> tids;
//创建线程
for(int i=1;i<=Num;i++)
{
pthread_t tid;
thread_data* td=new thread_data();//错误写法thread_data td;td是当前作用域的局部变量,后续会销毁,&td时会出现错误
Init(td,i);//初始化
pthread_create(&tid,nullptr,routine,td);
tids.push_back(tid);
}
//线程等待
for(int i=0;i<tids.size();i++)
{
pthread_join(tids[i],nullptr);
}
return 0;
}
如何在主线程中获新线程的数据?(禁止操作)
定义一个全局指针变量,然后把在新线程的执行函数中,将该全局指针变量指向新线程的数据,即可访问到你需要访问的数据。
线程之间没有秘密,哪是线程的栈上数据,也是可以被其他线程看到并访问的,只不过我们要求线程要有独立栈(而非私有)和上下文数据
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<vector>
#include<string>
using namespace std;
#define Num 3
int* get;//全局指针变量
//线程数据
struct thread_data{
string threadname;
};
void Init(thread_data*td,int i)
{
td->threadname="thread-"+to_string(i);
}
//转换为16进制
string tohex(pthread_t tid)
{
char buffer[128];
snprintf(buffer,sizeof(buffer),"0x%x",tid);
return buffer;
}
//线程入口函数,此函数为可重入函数
void* routine(void*arg)
{
thread_data* td=static_cast<thread_data*> (arg);//获取参数
int i=0;
int test_i=0;
//线程判断
if(td->threadname=="thread-2")
{
get=&test_i;
}
while(i<10)
{
test_i++;
cout<<"pid:"<<getpid()<<",name:"<<td->threadname
<<",tid:"<<tohex(pthread_self())<<",i:"<<i
<<",test_i:"<<test_i<<",&test_i:"<<tohex((pthread_t)&test_i)<<endl;//测试独立栈
i++;
sleep(1);
}
return nullptr;
}
int main()
{
//保存tid方便后续线程等待
vector<pthread_t> tids;
//创建线程
for(int i=1;i<=Num;i++)
{
pthread_t tid;
thread_data* td=new thread_data();//错误写法thread_data td;td是当前作用域的局部变量,后续会销毁,&td时会出现错误
Init(td,i);//初始化
pthread_create(&tid,nullptr,routine,td);
tids.push_back(tid);
}
sleep(1);//确保线程创建完毕
cout<<"main pthread;test_i:"<<*get<<",&test_i:"<<tohex((pthread_t)get)<<endl;
//线程等待
for(int i=0;i<tids.size();i++)
{
pthread_join(tids[i],nullptr);
}
return 0;
}
线程局部存储
全局变量是被所有线程看到并访问的
这样操作有没有什么问题呢?
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<vector>
#include<string>
using namespace std;
#define Num 3
int g_val=0;
//线程数据
struct thread_data{
string threadname;
};
void Init(thread_data*td,int i)
{
td->threadname="thread-"+to_string(i);
}
//转换为16进制
string tohex(pthread_t tid)
{
char buffer[128];
snprintf(buffer,sizeof(buffer),"0x%x",tid);
return buffer;
}
//线程入口函数,此函数为可重入函数
void* routine(void*arg)
{
thread_data* td=static_cast<thread_data*> (arg);//获取参数
int i=0;
while(i<10)
{
cout<<"pid:"<<getpid()<<",name:"<<td->threadname
<<",tid:"<<tohex(pthread_self())<<",i:"<<i
<<"g_val:"<<g_val<<",&g_val:"<<&g_val<<endl;
i++,g_val++;
sleep(1);
}
return nullptr;
}
int main()
{
//保存tid方便后续线程等待
vector<pthread_t> tids;
//创建线程
for(int i=1;i<=Num;i++)
{
pthread_t tid;
thread_data* td=new thread_data();//错误写法thread_data td;td是当前作用域的局部变量,后续会销毁,&td时会出现错误
Init(td,i);//初始化
pthread_create(&tid,nullptr,routine,td);
tids.push_back(tid);
}
//线程等待
for(int i=0;i<tids.size();i++)
{
pthread_join(tids[i],nullptr);
}
return 0;
}
共享资源
在上述代码演示中,里面的全局变量g_val就是共享资源,能够被多个线程同时看到并访问
怎么给线程分配私有的全局变量呢?(线程局部存储)
__thread 变量,线程的局部存储,__thread是一个编译选项,注意事项:__thread只能用来定义内置类型,也认为线程存储只能用来定义内置类型,不能用来修饰自定义类型。。
还有一个细节,通过地址对比我们也可以看出,上面的全局变量是在全局变量区,而下面的“全局变量”是在堆栈之间
线程局部存储的作用是什么?
举例:减少系统接口的调用
线程不是有独立栈嘛,为什么不直接使用一个局部变量保存?
线程入口函数里嵌套函数呢?传参吗?
因此我们利用__thread,在线程局部存储里定义了一个线程级别的全局变量,无论嵌套多少层都可以直接调用,和别人互不干扰
tid与LWP
Linux中的线程是用户级线程,并不是内核级进程
LWP是操作系统管理底层轻量级进程(给操作系统看)
tid是线程库中管理线程对应的线程控制块起始虚拟地址(给用户看)
LWP:tid=1:1
用户级执行流:内核lwp=1:1
每一个执行流的本质就是一条调用链,线程的独立栈的存在,就是保障调用链不被其他执行流打扰
当然了,不同的执行流在堆区申请的数据,其他线程都能想办法看到,因为堆区是共享的,但是我们一般不会这么做