当前位置 : 主页 > 操作系统 > centos >

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

来源:互联网 收集:自由互联 发布时间:2023-02-04
在上一篇我们了解了Linux下线程的相关概念。而本篇的主要内容是线程控制。线程控制包括线程的创建,线程的终止,线程等待等问题,以及线程分离和Linux常见线程安全问题。 1.线程控

在上一篇我们了解了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 ] 线程控制(线程创建,等待,终止)_pthread_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 ] 线程控制(线程创建,等待,终止)_多线程_08

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

1.2.3 创建多个线程

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

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

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

  • pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
  • 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴,线程库的后续操作,就是根据线程ID来操作线程的。
  • 线程库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.3.2 pthread_join介绍及其编码

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

    • 功能:等待线程结束
    • 原型:int pthread_join(pthread_t thread, void **value_ptr);
    • 参数
    • thread:线程ID
    • value_ptr:它指向一个指针,后者指向线程的返回值
    • 返回值:成功返回0,失败返回错误码。

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

    [ Linux ] 线程控制(线程创建,等待,终止)_pthread_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 线程终止

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

  • 从线程函数return,这种方法对于主线程不适用,从mian函数return相当于调用exit。
  • 线程可以调用pthread_exit终止自己
  • 一个线程可以调用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 ] 线程控制(线程创建,等待,终止)_pthread_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 ] 线程控制(线程创建,等待,终止)_线程控制_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 ] 线程控制(线程创建,等待,终止)_线程控制_18

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

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

    #define PTHREAD_CANCELED ((void *) -1)

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

    1.5 线程控制总结

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

  • 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  • 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数-1(PTHREAD_ CANCELED)
  • 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
  • 如果对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 ] 线程控制(线程创建,等待,终止)_线程控制_21

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

    上一篇:Linux学习之管道符和重定向详解
    下一篇:没有了
    网友评论