目录
说线程的时候说过,liunx没有选择单独定义线程的数据结构和适配算法,而是用轻量级进程来实现线程,有人将轻量级进程的系统调用进行了封装,在应用层实现了线程的相关功能,目前大部分的liunx平台都默认安装了第三方库,pthread.h。
创建线程:pthread_create();
man 一下该函数可以出现相关介绍:
pthread_t *thread,输出型参数,一个进程可以创建多个线程,为了方便控制,该参数返回线程的id。这个是你在该函数之前需要定义的一个变量,pthread_t是一个长整型。
const pthread_attr_t *attr,用来设置创建线程时的属性,一般我们传递一个空指针,线程的属性使用系统默认的就可以。
void *(*start_routine) (void *),函数指针,该函数返回值是void*,参数是void*。我们暂时把它叫做功能函数。
void *arg,第三个参数传递了一个函数,这个参数将来会传给你的功能函数做参数。
线程创建成功返回0,线程创建失败以返回值的形式返回错误码。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void *Function(void *arg)
{
while (true)
{
cout << "new thread: " << getpid() << endl
<< endl;
sleep(2);
}
}
int main()
{
pthread_t pid = 0;
int err = pthread_create(&pid, nullptr, Function, nullptr);
if (err == 0)
{
cout << " create thread successfully! " << endl;
}
while (true)
{
cout << "main thread: " << getpid() << endl
<< endl;
sleep(1);
}
return 0;
}
上面的代码演示了怎么创建一个线程,运行:
ldd 可以查看你的程序链接了哪些库:
查看进程也能看到只有一个mythread:
从这也能说明,一个进程可以有多个线程,多个线程只是一个进程不同的执行流。
但是查看线程的话,你可以明显看到这两个执行流:
LWP,light weight process id,轻量级进程id。PID=LWP的线程就是主线程,也就是程序运行最先创建的PCB。
当你任意的干掉一个线程,整个进程也会被干掉:
我们kill 的不是主线程,而是进程中的一个新线程,进程中的任意一个线程崩溃都会导致整个进程的崩溃。专业点说,多线程较单线程,程序的健壮性降低。
还要说一下创建线程成功后的第一个参数的值,用%p格式打印::
tid不等于LWP,实际上是一个地址,至于为什么,稍后说。
线程等待:pthread_join();
要等待某个线程,第一个参数就是创建该线程时的tid,第二个参数是用来取出你传入的功能函数的返回值。
等待成功返回0,失败返回错误码。
void *Function(void *arg)
{
int cnt=5;
while (cnt--)
{
cout << "new thread: " << getpid() << endl
<< endl;
sleep(1);
}
}
int main()
{
pthread_t pid = 0;
int err = pthread_create(&pid, nullptr, Function, nullptr);
if (err == 0)
{
cout << " create thread successfully! " << endl;
}
int err_join=pthread_join(pid,nullptr);
if(err_join==0)
{
cout<<" new thread wait successfully! main thread exit....."<<endl;
}
return 0;
}
运行:
主线程在等待新线程5秒运行完成后才退出,默认是阻塞等待。如果主线程比新线程先退出,会造成类似于僵尸进程的情况,也就是新线程的创建的一些资源无法回收,造成内存泄露的问题。
然后再说第二个参数。我们要知道一个函数的执行情况是根据函数的返回值,假如说我们没法在应用层接收功能函数的返回值,请问你如何知道函数的执行情况?或者你说可以通过错误码,但是一个进程只有一个错误码,可你有多个线程。其次我们无法捕捉到线程的异常,线程异常后整个进程都退出了,程序崩溃之后你只能知道有异常,至于哪个线程抛出的,没有办法捕捉。还有就是我们不主要用这个参数来查看线程的执行情况,他还有其他用途,后面说。在说为什么是一个二级指针。
我们的功能函数返回一个void*值,这个值在应用层,我们就假设功能函数执行完成的返回值返回到了pthread_join()函数里,你想要拿到一个函数内部的void*类型的值,那你的输出型参数就应该是void** retval,在函数内部进行复制*retval(void*)=void* ;这样类型就匹配了。这样用:
线程退出:pthread_exit();
线程退出你直接可以用return,也可以用这个,哪个线程调用这个函数,哪个线程就退出。
线程取消:pthread_cancel()
在主线程里不等新线程运行结束就直接取消。
int main()
{
pthread_t tid = 0;
int err = pthread_create(&pid, nullptr, Function, nullptr);
if (err == 0)
{
cout << " create thread successfully! " << endl;
}
sleep(1);//等待一秒确保线程创建成功
pthread_cancel(tid);
void* retval=nullptr;
pthread_join(tid,&retval);
cout<<" new thread wait successfully! "<<"exit:"<<(long long int)retval<<"main thread exit....."<<endl;
return 0;
}
pthread_create()第三个参数以及返回值的用法:
前面说到,pthread_join()中的第二个参数接收功能函数的返回值,我们可以将你要实现的功能封装成一个类,create时将该类作为参数传递给功能函数,在功能函数内部调用类的相关方法,并将执行结果和代码执行状态定义成成员变量。
我们创建一个线程,求和功能用一个类实现,在功能函数内部调用该类,执行结果定义为成员变量:
// 将你要实现的功能封装
class sum
{
public:
sum(int start, int end) : start_(start), end_(end)
{
cout << "initialize the class sum" << endl;
}
void count()
{
for (int i = start_; i <= end_; i++)
{
cout << "result+= " << i << endl;
result += i;
usleep(100000);
}
exit_code = 0;
// if(...)
// {
// //出现错误情况
// int exit_code= ?;
// pthread_exit((void*)1);
// }
}
private:
int start_;
int end_;
public:
int result;
int exit_code;
};
// 功能函数调用你设计好用于实现功能的类;
void *Function(void *arg)
{
sum *p = static_cast<sum *>(arg);
p->count();
return p;
}
int main()
{
// 创建线程
pthread_t tid1 = 0;
pthread_t tid2 = 0;
sum *p1 = new sum(1,20);
sum *p2 = new sum(20,40);
int err1 = pthread_create(&tid1, nullptr, Function, p1);
int err2 = pthread_create(&tid2, nullptr, Function, p2);
if (err1 == 0 && err2 == 0)
{
cout << " create thread successfully! " << endl;
}
sleep(1);
// 等待线程
void *retval1 = nullptr;
void *retval2 = nullptr;
int err_join1 = pthread_join(tid1, &retval1);
int err_join2 = pthread_join(tid2, &retval2);
sum *ret1 = static_cast<sum *>(retval1);
sum *ret2 = static_cast<sum *>(retval2);
int end = ret1->result + ret2->result;
cout << " new thread wait successfully! "
<< "result: " << end << " exit_code: " << ret1->exit_code
<< " main thread exit....." << endl;
return 0;
}
运行:
这里创建了两个线程,一个计算1-20的和,一个计算20-40的和,最后相加,计算的就是1-40的和。代码很挫,只是为了说明一下线程创建时的参数可以传递一个类 。
用户级别的线程标识tid:
线程在用户层。我们 先谈一下调用的库函数是怎么执行的。一个文件需要执行首先需要加载到内存中,编译器会根据你包含的库文件根据系统路径将你调用的库文件加载到内存,你调用的函数有一个虚拟地址,操作系统会在页表中建立一个该函数从虚拟地址到物理地址的映射,这个虚拟地址在你程序的一个叫共享区的位置,到你执行库中的函数时,找到虚拟地址相应的映射,将需要的数据压栈,然后跳转到内存中执行,执行完成给你返回一个结果。这个过程是动态库的加载方式。
我们说pthread是一个第三方线程库,是在用户层,底层调用了进程的相关的系统调用,意思就是说,我自己定义了一个线程结构体,其中包括线程的一系列属性,包括线程的独立的栈空间,局部变量等等:
通过将进程的系统调用进程封装适配,在一个库文件中建立线程相关结构,然后再定义线程的调度方法,执行方法等等,线程的概念就在用户层建立并且维护起来了。创建线程时给你返回的tid,就是调用库函数时创建一个线程结构体在你共享区的虚拟地址。真正的实际创建是在内存中,创建好后由于用户程序不能直接访问物理内存,所以需要将该物理地址映射成虚拟地址再给你返回,库中的地址我们统一放到你程序空间的共享区中,我们用这个地址,来唯一的标识你创建的线程。