内核中有没有很明确的线程概念呢?没有的。有的是轻量级进程的概念
不会给我直接提供线程的系统调用,只会给我们提供轻量级进程的系统调用,但是我们用户,需要线程的接口!
所以 Linux 开发者提供了 pthread 线程库--应用层--轻量级接口进行封装。为用户提供直接线程的接口
- 几乎所有的 Linux 平台,都是默认自带这个库的!
- Linux 中编写多线程代码 需要 使用第三方
pthread
库
为了保证函数能接受任意指针类型,C 进行了泛型设计void *
,之后进行强转即可
windows 指针 4 字节 (默认 32 位),Linux 指针 8 字节(uname -r 发现
x86_64)
1. 线程创建
功能:创建一个新的线程
函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);
参数:
- thread:输出型参数,获取创建成功的线程ID(是个地址)
- attr:设置线程的属性,attr为nullptr表示使用默认属性
- start_routine:是个函数地址,线程启动后要执行的函数
- arg:传给线程启动函数的参数,不需要使可以设置为++nullptr++ (要传指针,才能切实拿到)
返回值:成功返回0,失败返回错误码
测试 :可以发现有两个执行流
#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>
#include<cstdio>
using namespace std;
void* start_rountine(void* args)
{
//安全的进行强制类型转化
string name=static_cast<const char*>(args);
while(true)
{
cout<<"new thread create success, name: "<<name<<endl;
sleep(1);
}
}
int main()
{
#define NUM 10
for(int i=0;i<NUM;++i)
{
pthread_t id;
//pthread_create(&id,nullptr,start_rountine,(void*)"thread new");
char namebuffer[64];
snprintf(namebuffer,sizeof(namebuffer),"%s:%d","thread",i);
pthread_create(&id,nullptr,start_rountine,namebuffer);
}
while(true)
{
cout<<"new thread create success, name: main thread"<<endl;
sleep(1);
}
return 0;
}
多线程发生了一些错乱,但还是可以发现是同时运行的
⭕注意:格式化传参的设置
char namebuffer[64];
snprintf(namebuffer,sizeof(namebuffer),"%s:%d","thread",i);
pthread_create(&id,nullptr,start_rountine,namebuffer);
第一行: char namebuffer[64];
这一行声明了一个字符数组 namebuffer
,它可以存储最多 64 个字符(包括字符串结束符 \0
)。
第二行:
snprintf(namebuffer,sizeof(namebuffer),"%s:%d","thread",i);
这里使用了 snprintf
函数来格式化字符串并安全地写入到 namebuffer
中。snprintf
函数通常用于格式化输出,但会检查目标缓冲区的大小以防止溢出。参数解释如下:
namebuffer
: 目标缓冲区。sizeof(namebuffer)
: 缓冲区的大小(字节数),这里为 64。"
%s:%d": 格式字符串,表示一个字符串后跟一个冒号和一个整数。"thread"
: 要插入的第一个参数,是一个字符串。i
: 要插入的第二个参数,是一个整数变量。
因此,这条语句将把 "thread:i"
的格式化字符串写入 namebuffer
,其中 i
是某个整数值。
第三行:
pthread_create(&id,nullptr,start_routine,namebuffer);
namebuffer
: 这是传递给线程启动例程start_routine
的参数,即包含"thread:i"
的字符串。
线程的第三方库,不是系统调用,g++
后需要-lpthread
链接库
ps -aL 查看轻量级进程, LWP--light weight process
PID==LWP ,表面这个线程是主线程
- 任何一个线程被干掉了,进程都会被干掉
- 一个函数可以被多个执行流同时执行。叫做函数被重入了
- 全局变量是所有线程共享的,都可以访问操作
- 调度器决定运行顺序,我们并不只知道,但肯定是主线程最后退出
循环监视窗口的打开:while :; do ps -aL | grep mythread;sleep 1;done
2. 线程等待
- 类似于存在僵尸,需要等待退出
- 获取子进程退出结果
- 参数:
-
- thread:线程ID
- retval:利用其带回线程返回值,需深刻理解
- 返回值:
-
-
线程等待成功返回0,失败返回错误码
void threadRoutine(void args)
{
return (void *)233; // 返回给主线程
}int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void *)"thread 1");void *ret = nullptr; pthread_join(tid, &ret); // 默认会阻塞等待新线程退出 cout << "new thread retval:" << (long long)(ret) << endl; return 0;
}
-
- main thread 等待的时候,默认是阻塞等待的!
- 主线程需要 join --对创建线程进行回收
⭕void** retval :指向指针的指针,为了调用了接口,获取线程退出的退出码
对指针解引用,代表指针所指向的目标
将拿到的返回值空间存储到自己的地址空间中,所以就是一个二级指针了,保证了实参的传递,取地址解引用得到函数返回值,存取到用户指针中。例如:++解两次引用,就可以获取函数原值了++
- 直接调用 exit,会全部都直接退出
- exit 是用来终止进程的!不能用来直接终止进程!
3. 线程终止
仅代表线程终止
- return
- pthread_exit
放在调用函数结尾:
void* start_rountine(void* args)
{
//安全的进行强制类型转化
ThreadDate* td=static_cast<ThreadDate*>(args);
int cnt=10;
while(cnt)
{
cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;
cnt--;
sleep(1);
pthread_exit(nullptr);
}
return nullptr;
}
线程取消
- pthread_cancel -1
库设置的返回值,可以查看到 一个线程如果是被取消的,退出码是-1。
void* start_rountine(void* args)
{
//安全的进行强制类型转化
ThreadDate* td=static_cast<ThreadDate*>(args);
int cnt=5;
while(cnt)
{
cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;
cnt--;
sleep(1);
}
//正常跑完返回的100,那被取消的线程返回的是什么呢?
return (void*)100;
}
int main()
{
vector<ThreadDate*> threads;
#define NUM 10
for(int i=0;i<NUM;++i)
{
ThreadDate* td=new ThreadDate();
td->number=i+1;
snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);
pthread_create(&td->tid,nullptr,start_rountine,td);
//这样不仅每个线程数据除了自己拿到了,主线程也全部拿到了
threads.push_back(td);
}
for(auto& iter:threads)
{
cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" success"<<endl;
}
//线程取消
sleep(5);//先让线程跑起来
for(int i=0;i<threads.size()/2;++i)
{
pthread_cancel(threads[i]->tid);//创建多线程组中的某一 进行取消
cout<<"pthread_cancel: "<<threads[i]->namebuffer<<" success"<<endl;
}
for(auto& iter:threads)
{
void* ret=nullptr;//注意是void*
int n=pthread_join(iter->tid,&ret);//&地址是void**
assert(n == 0);
(void)n;
cout<<"join : "<<iter->namebuffer<<" success , number: "<<(long long)ret<<endl;
delete iter;
}
//这里就可以看出主线程式阻塞式的等待,全部等待成功,才打印这句话
cout<<"main thread quit!!"<<endl;
return 0;
}
pthread_cancel(threads[i]->tid);
//创建多线程组中的某一 进行取消
4.线程实现通信 | C++中
线程的参数和返回值,不仅仅可以用来进行传递一般参数,也可以传递类的对象!!
举例:实现通信
结构体+初始化=>类
class Request
{
public:
Request(int start, int end, const string &threadname)
: start_(start), end_(end), threadname_(threadname)
{}
public:
int start_;
int end_;
string threadname_;
};
class Response
{
public:
Response(int result, int exitcode):result_(result),exitcode_(exitcode)
{}
public:
int result_; // 计算结果
int exitcode_; // 计算结果是否可靠
};
进行测试:
将 rq 的内容传给线程函数,对 rq 获取内容执行 rsp 运算,释放 rq,返回 rsp 打印
void *sumCount(void *args) // 线程的参数和返回值,不仅仅可以用来进行传递一般参数,也可以传递对象!!
{
Request *rq = static_cast<Request*>(args); // Request *rq = (Request*)args
//强制转化
Response *rsp = new Response(0,0);
for(int i = rq->start_; i <= rq->end_; i++)
{
cout << rq->threadname_ << " is runing, caling..., " << i << endl;
rsp->result_ += i;
usleep(100000);
}
delete rq;//释放空间
return rsp;
}
int main()
{
pthread_t tid;
Request *rq = new Request(1, 100, "thread 1");//参数指针
pthread_create(&tid, nullptr, sumCount, rq);
void *ret;
pthread_join(tid, &ret);//接收指针的地址,实现传递
Response *rsp = static_cast<Response *>(ret);
cout << "rsp->result: " << rsp->result_ << ", exitcode: " << rsp->exitcode_ << endl;
delete rsp;
return 0;
}
要用地址符号接受,接收到的也是回复类的地址
pthread_join(tid, &ret);//接收指针的地址,实现传递
Response *rsp = static_cast<Response *>(ret);
也可以反映出堆空间也是被线程共享的
- 目前,我们的原生线程,pthread库,原生线程库
- C++11 语言本身也已经支持多线程了 vs 原生线程库
C++ 的多线程就是封装原生线程库
void threadrun()
{
while(true)
{
cout << "I am a new thead for C++" << endl;
sleep(1);
}
}
int main()
{
thread t1(threadrun);//调用
t1.join();//等待
return 0;
}
thread t1(threadrun);
//调用线程函数
编译时注意!两个编译后缀都要带 g++ -o $@$^
-std=c++11 -lpthread
windows 下装的是 windows c++的库,++安装的是不同的库,所以语言具有跨平台性~++
5. 打印线程自己的 id
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
cout<<"thread id:"<<pthread_self()<<endl;
//对16进制转化的实现
std::string toHex(pthread_t tid)
{
char hex[64];
snprintf(hex, sizeof(hex), "%p", tid);//重点研究理解
return hex;
}
man clone
专门用来创建轻量级进程,我们使用的库底层就是它
线程的概念,是库给我们来维护的,你用的原生线程库,要不要加载到内存里,加载到哪里?
- 要--都是基于内存的
- 线程库要维护线程概念--不用维护线程的执行流,线程库注定了要维护多个线程属性集合。线程库要不要管理这些线程呢?要,先描述再组织
- 由用户维护,OS 之上的,所以称为用户级线程
库加载到了共享区,在堆栈之间
- 每一个线程的库级别的 tcb 的起始地址,叫做线程的 tid
- 除了主线程,所有其他线程的独立栈,都在共享区,具体来讲是在 pthread 库中,tid 指向的用户 tcb 中
下篇文章将讲解线程的互斥~