2

[ Linux ] 线程控制(线程创建,等待,终止)

 1 year ago
source link: https://blog.51cto.com/xingyuli/5936740
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在上一篇我们了解了Linux下线程的相关概念。而本篇的主要内容是线程控制。线程控制包括线程的创建,线程的终止,线程等待等问题,以及线程分离和Linux常见线程安全问题。

1.线程控制

线程控制和我们之前学习过的进程控制类似,包括线程创建终止等待。我们会从完成编码和验证两个方面完善线程控制。

1.1POSIX线程库

在上一篇博文我们就提到过,操作系统并没有直接提供相关的接口。而是由大多数程序员为我们开辟好了一个原生的线程库。因此我们要对线程进行控制。就要进入这个线程库。

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的。例如pthread_create,pthread_join。
  • 要使用这些函数库,要通过引入头文件<pthread.h>
  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项

1.2 创建线程

pthread_create

  • 功能:创建一个新的线程

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变量开销更小。
[ Linux ] 线程控制(线程创建,等待,终止)_线程控制

[ Linux ] 线程控制(线程创建,等待,终止)_多线程_02

1.2.1 创建线程编码

了解了线程创建的函数和参数使用方式之后,我们在代码中来体现一番:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

void *startRoutine(void *args)
{
while(true)
{
cout<<"线程正在运行......"<<endl;
sleep(1);
}
return nullptr;
}


int main()
{
//创建tid
pthread_t tid;

//创建线程
int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");

//主线程
while(true)
{
cout<<"我是主线程,我正在运行......"<<endl;
sleep(1);
}
return 0;
}
[ Linux ] 线程控制(线程创建,等待,终止)_多线程_03

我们来查看一下当前的线程

[ Linux ] 线程控制(线程创建,等待,终止)_pthread_04

1.2.2 代码相关解释

首先我们创建了一个线程id(tid),这个tid是一个整数,我们来看看线程id是什么?

[ Linux ] 线程控制(线程创建,等待,终止)_线程控制_05

我们打印完发现这个tid怎么这么大,这里所谓的id是什么?这个值是什么?我们到后面会说。现在我们可以先把这个数字转成16进制看看。

//将tid转乘16进制
static void printTid(const pthread_t& tid)
{
printf("tid: 0x%x\n",tid);

}

[ Linux ] 线程控制(线程创建,等待,终止)_多线程_06

库内我们还有一个可以获取自己id的函数pthread_self(). -- 谁掉这个函数就把线程id返回给谁

[ Linux ] 线程控制(线程创建,等待,终止)_线程控制_07

所以我们修改一下代码,当前在获取一下线程id

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

//将tid转乘16进制
static void printTid(const char *name,const pthread_t& tid)
{
printf("%s 正在运行 ,tid: 0x%x\n",name,tid);

}
void *startRoutine(void *args)
{
const char *name = static_cast<const char *>(args);
while(true)
{
printTid(name,pthread_self());
//cout<<"线程正在运行......"<<endl;
sleep(1);
}
return nullptr;
}


int main()
{
//创建tid
pthread_t tid;
//创建线程
int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
//printTid(tid);
//cout<<"tid :" <<tid<<endl;
//主线程
while(true)
{
printTid("man thread:",pthread_self());
//cout<<"我是主线程,我正在运行......"<<endl;
sleep(1);
}
return 0;
}
[ Linux ] 线程控制(线程创建,等待,终止)_pthread_08

很明显这两个线程的tid是不一样的。

1.2.3 创建多个线程

现在我们已经学会了创建线程了,那么我们如果创建多个线程呢?

我们可以和创建多进程一样,可以打循环创建。

1.2.4 线程ID及进程地址空间布局

  1. pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
  2. 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  3. pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴,线程库的后续操作,就是根据线程ID来操作线程的。
  4. 线程库NPTL提供了pthread_self()函数,可以获得线程自身的ID。 pthread_t pthread_self(void);

1.2.5 pthread_t

pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

我们在vs code中查看pthread_t的类型可以发现,当前的pthread_t其实就是一个无符号长整型的整数。

[ Linux ] 线程控制(线程创建,等待,终止)_线程控制_09
[ Linux ] 线程控制(线程创建,等待,终止)_线程控制_10

1.3 线程等待

我们在主执行流(main)内创建线程之后,我们也要等待线程,类似于进程部分的父进程等待子进程。不等待的话可能会引发内存泄漏问题。

1.3.1为什么需要线程等待

为什么要有线程等待主要有两点原因:

  1. 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  2. 创建新的线程不会复用刚才退出线程的地址空间

因此有可能引发内存泄漏的问题。

1.3.2 pthread_join介绍及其编码

如何等待一个线程呢?我们可以使用pthread_join函数,首先我们先了解一下这个函数

  • 功能:等待线程结束
  • 原型:int pthread_join(pthread_t thread, void **value_ptr);
  • thread:线程ID
  • value_ptr:它指向一个指针,后者指向线程的返回值
  • 返回值:成功返回0,失败返回错误码。
[ Linux ] 线程控制(线程创建,等待,终止)_pthread_11

[ Linux ] 线程控制(线程创建,等待,终止)_多线程_12

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

//将tid转乘16进制
static void printTid(const char *name,const pthread_t& tid)
{
printf("%s 正在运行 ,tid: 0x%x\n",name,tid);

}
void *startRoutine(void *args)
{
const char *name = static_cast<const char *>(args);
int cnt = 5;
while(true)
{
printTid(name,pthread_self());
//cout<<"线程正在运行......"<<endl;
sleep(1);
if(!(cnt--)) break;
}
cout<<"线程退出啦........"<<endl;
return nullptr;
}


int main()
{
pthread_t tid;
int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");

sleep(10);

pthread_join(tid,nullptr);
return 0;
}

我们再写一个监控脚本 查看当前线程个数

while :; do ps -aL | head -1 && ps -aL | grep mythread; sleep 1; done

[ Linux ] 线程控制(线程创建,等待,终止)_多线程_13

因此线程退出的时候必须要join,如果不join就会造成类似于进程那样的内存泄漏问题。

1.3.3 join的第二个参数 value_ptr

我们查看文档发现,join的第二个参数是一个二级指针,而且是一个输出型参数,指向的是线程的返回值。我们所写的回调函数的返回值是一个void*,如果我们返回一个void*的值,主线程可以接受的。我们使用编码来进行验证。

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;


//将tid转乘16进制
static void printTid(const char *name,const pthread_t& tid)
{
printf("%s 正在运行 ,tid: 0x%x\n",name,tid);


}
void *startRoutine(void *args)
{
const char *name = static_cast<const char *>(args);
int cnt = 5;
while(true)
{
printTid(name,pthread_self());
//cout<<"线程正在运行......"<<endl;
sleep(1);
if(!(cnt--))
{
break;
// int *p = nullptr;
// *p = 100;//野指针问题
}
}
cout<<"线程退出啦........"<<endl;
return (void*)111;
}

int main()
{
pthread_t tid;
int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
(void)n;


void *ret = nullptr;// void* -> 字节
pthread_join(tid,&ret);//void **retval是一个输出型参数
cout<<"main thread join sucess , *ret:" <<(long long)ret<<endl;

sleep(10);
//主线程
while(true)
{
printTid("man thread:",pthread_self());
//cout<<"我是主线程,我正在运行......"<<endl;
sleep(1);
}


return 0;
}
[ Linux ] 线程控制(线程创建,等待,终止)_多线程_14

我们能够发现主线程是可以收到线程的退出码的。

1.4 线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return,这种方法对于主线程不适用,从mian函数return相当于调用exit。
  2. 线程可以调用pthread_exit终止自己
  3. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程

其中方法一正是我们1.3.3所提到的。这里我们再来了解剩下两个函数,线程调用pthread_exit()函数终止自己和线程调用pthread_cancel终止同进程内的另一个线程。

1.4.1pthread_exit 介绍和编码

pthread_exit函数

  • 功能:线程终止
  • 原型:void pthread_exit(void *value_ptr);
  • 参数:value_ptr:value_ptr不要指向一个局部变量
  • 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

需要注意的是,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能是线程函数在栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了。有可能造成野指针问题。

[ Linux ] 线程控制(线程创建,等待,终止)_线程控制_15
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

//将tid转乘16进制
static void printTid(const char *name,const pthread_t& tid)
{
printf("%s 正在运行 ,tid: 0x%x\n",name,tid);

}
void *startRoutine(void *args)
{
const char *name = static_cast<const char *>(args);
int cnt = 5;
while(true)
{
printTid(name,pthread_self());
//cout<<"线程正在运行......"<<endl;
sleep(1);
if(!(cnt--))
{
break;
// int *p = nullptr;
// *p = 100;//野指针问题
}
}
cout<<"线程退出啦........"<<endl;
//return (void*)111;
//pthread_exit
pthread_exit((void*)2222);
}


int main()
{
pthread_t tid;
int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
(void)n;

void *ret = nullptr;// void* -> 字节
pthread_join(tid,&ret);//void **retval是一个输出型参数
cout<<"main thread join sucess , *ret:" <<(long long)ret<<endl;

return 0;
}
[ Linux ] 线程控制(线程创建,等待,终止)_pthread_16

1.4.2 pthread_cancel 介绍和编码

这个方法不太常用,但是还是介绍一下

pthread_cancel

  • 功能:取消一个执行中的线程
  • 原型:int pthread_cancel(pthread_t thread);
  • thread :线程ID
  • 返回值:成功返回0,失败返回错误码。
[ Linux ] 线程控制(线程创建,等待,终止)_线程控制_17
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

//将tid转乘16进制
static void printTid(const char *name,const pthread_t& tid)
{
printf("%s 正在运行 ,tid: 0x%x\n",name,tid);

}
void *startRoutine(void *args)
{
const char *name = static_cast<const char *>(args);
while(true)
{
printTid(name,pthread_self());
//cout<<"线程正在运行......"<<endl;
sleep(1);
}
cout<<"线程退出啦........"<<endl;
//return (void*)111;
//pthread_exit((void*)2222);
}


int main()
{
pthread_t tid;
int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
(void)n;

sleep(3);//代表main thread对应的工作
//3.给线程发送取消请求 如果线程是被取消的,退出结果是:-1
pthread_cancel(tid);
cout<<"new thread been canceled"<<endl;

void *ret = nullptr;// void* -> 字节
pthread_join(tid,&ret);//void **retval是一个输出型参数
cout<<"main thread join sucess , *ret:" <<(long long)ret<<endl;

return 0;
}
[ Linux ] 线程控制(线程创建,等待,终止)_pthread_18

我们发现返回的结果是-1,这里我们需要知道如果线程是被取消的,退出结果是:-1。

-1 是库里面给我提供的一个宏

#define PTHREAD_CANCELED ((void *) -1)

[ Linux ] 线程控制(线程创建,等待,终止)_多线程_19

1.5 线程控制总结

至此我们了解了线程的创建,线程的等待,以及线程终止的三种方式。在线程等待中,我们要调用pthread_join函数,调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数-1(PTHREAD_ CANCELED)
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
[ Linux ] 线程控制(线程创建,等待,终止)_线程控制_20

1.6 验证 线程异常问题

至此我们了解了线程创建和线程等待,那么如果线程异常了会怎么办呢?这和线程的健壮性相关,在上篇线程介绍中我们提到过线程异常,我们当时说

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

现在我们验证一下线程异常。

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

//将tid转乘16进制
static void printTid(const char *name,const pthread_t& tid)
{
printf("%s 正在运行 ,tid: 0x%x\n",name,tid);

}
void *startRoutine(void *args)
{
const char *name = static_cast<const char *>(args);
int cnt = 5;
while(true)
{
printTid(name,pthread_self());
//cout<<"线程正在运行......"<<endl;
sleep(1);
if(!(cnt--))
{
int *p = nullptr;
*p = 100;//野指针问题
}
}
cout<<"线程退出啦........"<<endl;
return nullptr;
}


int main()
{
pthread_t tid;
int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");


pthread_join(tid,nullptr);

sleep(10);
while(true)
{
printTid("man thread:",pthread_self());
//cout<<"我是主线程,我正在运行......"<<endl;
sleep(1);
}
return 0;
}
[ Linux ] 线程控制(线程创建,等待,终止)_pthread_21

通过结果我们发现,线程如果异常了,整个进程异常退出。线程异常 == 进程异常。线程一旦退出,线程会影响其他线程。 -- 健壮性(鲁棒性)较低.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK