欢迎光临
我们一直在努力

【Linux 篇】数字世界的线程工坊 —— Linux 线程控制与自研线程库深度实战解析

于此止?:个人主页

本文选自专栏: 《Linux篇》

❄️个人专栏传送门: 《QT百日筑基篇》 《算法篇》 《Linux网络篇》

《C++与STL剖析篇》 《数据结构篇》 《Python基础篇》《嵌入式砺剑录》

⭐️纵有狂风拔地起,我亦乘风破万里⭐️

Linux线程控制和封装Pthread库

前言:看本文章之前我们要先学习我们前置的知识,我们基于前置知识我们了解我们对应的线程,传送门:【Linux 篇】数字世界的任务执行者 —— Linux 线程概念与虚拟物理内存页表映射深度实战解析-CSDN博客

我们本文主要是认识我们的线程接口和我们封装我们的线程(我们的封装按照我们的库的形式进行封装)


目录/索引

目录

Linux线程控制和封装Pthread库

目录/索引

一、Linux线程控制

1.POSIX线程库

pthread_create:创建线程

ps -aL:查看我们的线程

线程的崩溃

phread_self: 获取线程的id信息。

线程的终止

pthread_exit: 终止自己的线程(强制)

pthread_cancel:终止指定的进程(请求)

pthread_join:线程的等待

pthread_detach:分离我们的线程。

二、线程ID级进程地址空间的空间布局

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

三、线程库的封装


一、Linux线程控制

1.POSIX线程库

与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以"pthread_"开头的。

要使用这些函数库,需要引入我们的头文件<pthread.h>

链接这个文件,这个文件属于第三方库,我们要在编译的时候要加入我们的”-lpthread“选项。

pthread_create:创建线程

我们依旧先看我们的官方文档。

man pthread_create
NAME
pthread_create – create a new thread

SYNOPSIS
#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);

我们能看到我们使用之前要包含我们对应的头文件,<pthread.h>

参数的介绍:

1.pthread_t* thread:输出型参数我们这里看到我们要传一个pthread_t 指针,那么这和pthread_t这个是什么呢?

我们来看一看吧:

我们的这个是一个无符号的长整形。

所以我们这里要先创建一个ptread_t的变量,这里我们称之为新线程的id。

2.const pthread_attr_t *attr:这个参数是我们的设置我们线程的属性,我们一般设置为NULL表示默认的属性。

3.void *(*start_routine) (void *):我们学过我们的c语言我们一定能看出来这是一个函数指针,我们后续要穿一个函数指针,让我们的创建的新线程去执行这个函数。我们的主线成继续的向下进行执行。注意我们的参数是我们的void* 如果我们想返回什么内容要强转成void* 。本质就是一个回调函数机制。

4.void *arg:我们第三个参数所传的参数,注意这个参数是我们的void*类型的,我们传一个字符串的时候我们要将它进行强制类型的转化噢。

返回值:

成功返回0,失败返回错误码。

错误检查:

  • 传统的一些函数是,成功返回 0,失败返回 – 1,并且对全局变量 errno 赋值以指示错误。
  • pthreads 函数出错时不会设置全局变量 errno(而大部分其他 POSIX 函数会这样做)。而是将错误代码通过返回值返回
  • pthreads 同样也提供了线程内的 errno 变量,以支持其它使用 errno 的代码。对于 pthreads 函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的 errno 变量的开销更小

那我们下面创建一个线程来看看结果。

注意我们写好的代码编译后要加入-l pthread。这是我们自己实现的第三方库。

test:pthread_test.cc
@g++ -o $@ $^ -l pthread
@echo "编译成功"

.PHONY:clean
clean:
@rm -f test
@echo "test 被删除"

这样我们就创建了一个新线程。

#include<iostream>
#include<pthread.h>
#include<cstdio>

void* Print(void*s)
{
printf("%s\\n",(char*)s);
return (void*)"print success";
//pthread_exit((void*)"print success");
}

int main()
{
//创建我们的线程make
pthread_t tid;
int ret = pthread_create(&tid,nullptr,Print,(void*)"我是一个新线程");
if(ret != 0)
{
perror("pthread_creat fail!!!");
exit(1);
}
printf("我是一个主线程\\n");

return 0;
}

我们可以sleep几秒查看我们的细节。

首先我们先认识一个命令这个是我们查看我们的轻量级进程的也就是我们的线程

ps -aL:查看我们的线程

我们的PID是我们当前进程的pid,我们的LWP(Light weight process轻量级进程)也就是我们的线程的id信息。

到这了我们有个疑问,我们的LWP是我们程序定义的时候的thread吗?

我们将我们的程序的id打印看看吧。

我们惊讶的看到我们的pid是不一样的,这正常吗?当然正常啊。怎么正常?我的LWP是我们的系统的概念,他是一个轻量级进程,不是我们的线程,我们的操作系统可以说没有我们的线程,我们的线程是用我们的轻量级进程进行实现的。我们的pthread——create是我们c语言库提供的,既然是我们c语言库提供的我们当然要搞一套我们自己线程的id了。所以我们的两个的id是不一样的。

打印出来的 tid 是通过 pthread 库中有函数 pthread_self 得到的,它返回一个 pthread_t 类型的变量,指代的是调用 pthread_self 函数的线程的 "ID"。

怎么理解这个 "ID" 呢?这个 "ID" 是 pthread 库给每个线程定义的进程内唯一标识,是 pthread 库维持的。

由于每个进程有自己独立的内存空间,故此 "ID" 的作用域是进程级而非系统级(内核不认识)。

其实 pthread 库也是通过内核提供的系统调用(例如 clone)来创建线程的,而内核会为每个线程创建系统全局唯一的 "ID" 来唯一标识这个线程。

lwp用户不关心,我们的pthread_creat是封装了我们的vfork() 和我们close()的这两个就是创建我们的轻量级进程的。

Linux线程的实现是用户实现出来的,我们称之为用户级线程。

这里我们看到我们的代码时先创建我们的主线程然后我们经过2s后创建我们的新线程,然后再过2s我们的两个线程都退出了。

纯净版的控制循环打印脚本。

while :; do ps -aL | grep -v ps | grep -v grep; sleep 1; done
#include<iostream>
#include<pthread.h>
#include<cstdio>
#include <unistd.h>

void* Print(void*s)
{
sleep(2);
printf("%s\\n",(char*)s);
return (void*)"print success";
//pthread_exit((void*)"print success");
}

int main()
{
//创建我们的线程make
pthread_t tid;
std::cout<<"线程的id信息"<<tid << std::endl;
sleep(2);
int ret = pthread_create(&tid,nullptr,Print,(void*)"我是一个新线程");
sleep(2);
if(ret != 0)
{
perror("pthread_creat fail!!!");
exit(1);
}
printf("我是一个主线程\\n");

return 0;
}

线程的崩溃

我们上一篇文章说过我们的进程崩溃了我们的线程也就崩溃了,我们的线程崩溃了我们的进程是不是也会崩溃呢?

我们试一试吧。        

我们这里直接的退出我们的主线程下面还没来的急打呢我的线程就退出了。所以我们的子线程异常退出我们的进程也会直接的退出的。

下面我们在介绍我们的一个函数,用来获取我们的线程id的。

phread_self: 获取线程的id信息。

man pthread_self

函数原型:

NAME
pthread_self – obtain ID of the calling thread

SYNOPSIS
#include <pthread.h>

pthread_t pthread_self(void);

返回值:

函数不会失败返回我们线程的id信息。

使用:

是当然这也可能会出现我们的信息打在一起的情况,我们后期学了互斥就好了。

LWP 是什么呢?LWP 得到的是真正的线程 ID。之前使用 pthread_self 得到的这个数实际上是一个地址,在虚拟地址空间上的一个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程 ID,线程栈,寄存器等属性。

在 ps -aL 得到的线程 ID,有一个线程 ID 和进程 ID 相同,这个线程就是主线程,主线程的栈在虚拟地址空间的栈上,而其他线程的栈在是在共享区(堆栈之间),因为 pthread 系列函数都是 pthread 库提供给我们的。而 pthread 库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。

当然我们线程创建了我们要怎么终止我们的线程呢?

线程的终止

当然我们也有对应的线程终止的函数调用。

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

1.从线程函数return 。这种方法对于主线程不适用,从main函数调用return相当于终止整个进程。

2.线程可以调用自己的pthread_exit库来终止自己。

3.一个线程可以调用自己的pthread_cancel来终止同一个进程中的另一个线程。

我们先看:

pthread_exit: 终止自己的线程(强制)

man pthread_exit

函数原型:

NAME
pthread_exit – terminate calling thread

SYNOPSIS
#include <pthread.h>

void pthread_exit(void *retval);

terminate:结束。结束一个线程。

参数解析

retval:不要指向我们的一个局部变量,因为我们这个线程被结束了的时候我们的局部变量都会被释放,所以我们带出去会访问野指针的,所以我们一般返回我们的字符串,当作我们的取消信息。

返回值:

我们线程退出了没有有谁能接受返回值,所以我们没有返回值

我们下面使用了进程的等待我们下面会讲解的。我只是先把我们的消息打印出来。方便观察。

#include<iostream>
#include<pthread.h>
#include<cstdio>
#include <unistd.h>

void* Print(void*s)
{
//sleep(2);
printf("%s\\n",(char*)s);
pthread_t id = pthread_self();
std::cout<<"新线程的id信息"<< id << std::endl;
//return (void*)"print success";
pthread_exit((void*)"exit success");
//return (void*)"print success";
}

int main()
{
//创建我们的线程make
pthread_t tid;
//sleep(2);
int ret = pthread_create(&tid,nullptr,Print,(void*)"我是一个新线程");
sleep(2);
if(ret != 0)
{
perror("pthread_creat fail!!!");
exit(1);
}
printf("我是一个主线程\\n");

pthread_t id = pthread_self();
std::cout<<"主线程的id信息"<<id << std::endl;
std::cout<<"主线程的tid信息"<<tid << std::endl;

//等待我们的新线程
void* retval;
ret = pthread_join(tid,&retval);
if(ret!=0)
{
perror("pthread_join fail!");
exit(2);
}
printf("%s\\n", (char*)retval);

return 0;
}

pthread_cancel:终止指定的进程(请求)

man pthread_cancel

函数原型:

NAME
pthread_cancel – send a cancellation request to a thread

SYNOPSIS
#include <pthread.h>

int pthread_cancel(pthread_t thread);

send a cancellation request to a thread:发送一个请求取消这个线程。注意我们只是请求并不是一定。

参数介绍:

thread:我们能看出来我们的thread是一个我们的一个id,我们传入我们的一个id我们对应的线程就会被取消。

返回值:成功返回我们的0,失败返回非0的错误码。

函数使用:

#include<iostream>
#include<pthread.h>
#include<cstdio>
#include <unistd.h>

void* Print(void*s)
{
sleep(2);
printf("%s\\n",(char*)s);
pthread_t id = pthread_self();
std::cout<<"新线程的id信息"<< id << std::endl;
//return (void*)"print success";
//pthread_exit((void*)"exit success");
return (void*)"print success";
}

int main()
{
//创建我们的线程make
pthread_t tid;
//sleep(2);
int ret = pthread_create(&tid,nullptr,Print,(void*)"我是一个新线程");
//sleep(2);
if(ret != 0)
{
perror("pthread_creat fail!!!");
exit(1);
}
printf("我是一个主线程\\n");

pthread_t id = pthread_self();
std::cout<<"主线程的id信息"<<id << std::endl;
std::cout<<"主线程的tid信息"<<tid << std::endl;

//线程的终止
ret = pthread_cancel(tid);
if(ret!=0)
{
perror("pthread_cancel fail!");
exit(3);
}
printf("线程:%ld已被终止\\n", tid);

return 0;
}

pthread_join:线程的等待

为什么我们需要等待我们的线程呢?

1.已退出的线程,其空间,没有被释放,仍在该进程的地址空间内。

2.创建新的线程不会复用刚退出的线程地址空间。

我们先查看文档介绍

man pthread_join

函数原型:

NAME
pthread_join – join with a terminated thread

SYNOPSIS
#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

这里我们有两个参数

1.thread:我们要等待哪个进程我需要该进程的tid

2.retval:我们的retval是一个二级指针,是为了接受我们的返回值。我们线程的返回值是一个void*所以我们要使用二级指针来进行接收。输出型参数,我们不关心设置为NULL。

返回值:成功返回0失败返回-1.

代码测试上面已经写过了。

调用该函数的线程将挂起等待,直到 id 为 thread 的线程终止。thread 线程以不同的方法终止,通过 pthread_join 得到的终止状态是不同的,总结如下:

  • 如果 thread 线程通过 return 返回,value_ptr 所指向的单元里存放的是 thread 线程函数的返回值。
  • 如果 thread 线程被别的线程调用 pthread_cancel 异常终掉,value_ptr 所指向的单元里存放的是常数 PTHREAD_CANCELED。
  • 如果 thread 线程是自己调用 pthread_exit 终止的,value_ptr 所指向的单元存放的是传给 pthread_exit 的参数。
  • 如果对 thread 线程的终止状态不感兴趣,可以传 NULL 给 value_ptr 参数。
  • 我们这里进行下面的几组测试:

    1.调用我们的pthread_exit和pthread_join的情况下,我们先调用我们的pthread_exit,后调用pthread_join 的情况:如下正常运行。

    1.调用我们的pthread_exit和pthread_join的情况下,我们先调用我们的pthread_join,后调用pthread_exit 的情况:如下正常运行

    3.调用我们的pthread_cancel和pthread_join的情况下,我们先调用我们的pthread_cancel,后调用pthread_join 的情况:如下正常运行

    我测试了很多组得到一个结论我们的pthread_cancel可能会出现段错误。下面解释

    注意点:

    我们的pthread_exit 和我们pthread_join 一起使用是没什么问题的,但是我们的ptread_join 和我们的pthread_cancel是有问题的,原因是我们先发送我们的信息我们该函数没有返回值,没有运行到返回所以我们接受不到我们的返回信息,那么在这里我们就不能打印我们的信息,因为我们的ret参数没有收到我们对应的返回值。

    如果我们的retval=我们的PTHREAD_CANCELED那就说明我们的线程已经被取消了我们没拿到对应的返回值。


    pthread_detach:分离我们的线程。

    • 默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成系统泄漏。
    • 如果不关心线程的返回值,join 是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

    我们查阅一下我们的官方文档:  

    man pthread_detach

    函数原型:

    NAME
    pthread_detach – detach a thread

    SYNOPSIS
    #include <pthread.h>

    int pthread_detach(pthread_t thread);

    参数的简介:

    thread:传入我要分离的进程id。

    分离状态,线程结束后我们直接进行释放,不需要我们在手动的进行join。

    线程的分离,主线程能分离新线程,主线程也能分离主线程。

    我们线程处于我们的分离状态不能在使用join,join会失败。

    返回值:

    成功返回0失败返回错误码。

    #include<iostream>
    #include<pthread.h>
    #include<cstdio>
    #include <unistd.h>

    void* Print(void*s)
    {
    sleep(5);
    printf("%s\\n",(char*)s);
    pthread_t id = pthread_self();
    std::cout<<"新线程的id信息"<< id << std::endl;
    //return (void*)"print success";
    //pthread_exit((void*)"exit success");
    return (void*)"print success";
    }

    int main()
    {
    //创建我们的线程make
    pthread_t tid;
    //sleep(2);
    int ret = pthread_create(&tid,nullptr,Print,(void*)"我是一个新线程");
    //sleep(2);
    if(ret != 0)
    {
    perror("pthread_creat fail!!!");
    exit(1);
    }
    printf("我是一个主线程\\n");

    pthread_t id = pthread_self();
    std::cout<<"主线程的id信息"<<id << std::endl;
    std::cout<<"主线程的tid信息"<<tid << std::endl;

    //sleep(5);

    //线程的终止
    ret = pthread_cancel(tid);
    if(ret!=0)
    {
    perror("pthread_cancel fail!");
    exit(3);
    }
    printf("线程:%ld已被终止\\n", tid);

    //线程的分离
    ret = pthread_detach(tid);
    if(ret !=0)
    {
    perror("pthread_detach fail!");
    exit(4);
    }
    printf("线程已被分离\\n");

    return 0;
    }

    二、线程ID级进程地址空间的空间布局

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

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

    三、线程库的封装

    #include <iostream>
    #include <pthread.h>
    #include <cstring>
    #include<functional>
    #include <cstdio>
    #include <unistd.h>

    #define TID_FILE 1

    using Func =std::function<void()>;
    namespace nThread {
    static int count = 1;
    class Thread {
    private:
    void Enablerunning()
    {
    _running=true;
    }
    void NoEnablerunning()
    {
    _running=false;
    }
    void NoEnabledetach()
    {
    _detach=false;
    }
    void Enabledetach()
    {
    _detach=true;
    }
    static void* Running_func(void* argv)
    {
    Thread* self = static_cast<Thread*> (argv);
    self->Enablerunning();
    pthread_setname_np(self->_tid,self->_name.c_str());
    if(self->_detach)
    self->NoEnabledetach();
    self->_func();
    return nullptr;
    }

    public:
    Thread(Func func)
    :_tid(TID_FILE), _running(false), _detach(true),_ret(nullptr), _func(func)
    {
    _name = "我是线程:" + std::to_string(count++);
    }

    //启动线程
    bool Start()
    {
    //创建线程
    if(_running)
    return false;

    int n = pthread_create(&_tid,NULL,Running_func,this);
    printf("线程的id是 %ld",_tid);

    if(n != 0)
    {
    perror("pthread fail!!!");
    return false;
    }
    //创建成功
    std::cout<< "线程创建成功" <<std::endl;

    return true;
    }

    //线程的等待
    void* Join()
    {
    if(_detach)
    return nullptr;

    int ret = pthread_join(_tid,&_ret);
    if(ret != 0)
    {
    perror("pthread_join fail!!!");
    exit(1);
    }

    printf("线程等待成功\\n");
    //NoEnablerunning();
    return _ret;
    }

    //线程的取消
    bool Cancel()
    {
    if(!_running)
    return false;
    if(pthread_cancel(_tid)!=0)
    {
    perror("pthread_cancel fail!!!");
    exit(1);
    }
    NoEnablerunning();
    std::cout<<"线程已被取消"<<std::endl;
    return true;
    }

    //线程的分离
    void Detach()
    {
    if(_detach)
    return;

    Enabledetach();
    if(!_running)
    return;

    if(pthread_detach(_tid)!=0)
    {
    perror("pthread_detach fail!!!");
    exit(1);
    }
    std::cout<<"线程成功进行分离"<<std::endl;
    }

    void Getname(char* buffer)
    {
    if(_running)
    pthread_getname_np(_tid,buffer,sizeof(buffer));

    else
    std::cout<<"线程已经结束无法获取name"<<std::endl;
    }

    ~Thread(){}
    private:
    pthread_t _tid;
    std::string _name;
    void* _ret;
    bool _running;
    bool _detach;
    Func _func;
    };
    }

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux 篇】数字世界的线程工坊 —— Linux 线程控制与自研线程库深度实战解析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址