Linux线程控制

POSIX线程库

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

创建线程

cpp 复制代码
功能:创建⼀个新的线程
原型:
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变量的开销更小
cpp 复制代码
#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>

void * routine(void * args)
{
    std::string name=static_cast<const char*>(args);
    int cnt=10;
    while(cnt--)
    {
        std::cout<<"线程名字:"<<name<<std::endl;
        sleep(1);
    }
    return (void*)10;
}
int main()
{

pthread_t tid;
pthread_create(&tid,nullptr,routine,(void*)"thread-1");

int cnt=5;
while(cnt--)
{
    std::cout<<"main线程名字:"<<std::endl;
    sleep(1);
}
void *ret=nullptr;
pthread_join(tid,&ret);
std::cout<<"新线程结束,退出码:"<<(long long)ret<<std::endl;
}

1.main函数结束,代表主线程结束,一般代表进程结束

2.新线程对应的入口函数运行结束,代表当前线程运行结束

3.给线程传递的参数和返回值,可以是任意类型

有一个任务Task,Result计算任务结果,通过参数交给线程处理,再以退出码返回得到结果

cpp 复制代码
#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>


class Task
{
    public:
    Task(int a,int b):_a(a),_b(b){}
    ~Task(){}
    int Exectue()
    {
        return _a+_b;
    }
    private:
    int _a;
    int _b;
};

class Result
{
    public:
    Result(int result):_result(result){}
    ~Result(){}
    int GetResult(){return _result;}
    private:
    int _result;
};

void * routine(void * args)
{
  Task *t =static_cast<Task*>(args);
  sleep(1);
  Result *res=new Result(t->Exectue());
  sleep(1);
  return (void *)res;
}
int main()
{

pthread_t tid;
Task *t=new Task(10,20);
pthread_create(&tid,nullptr,routine,t);

Result *ret=nullptr;
pthread_join(tid,(void**)&ret);
std::cout<<"新线程结束,运行结果:"<<ret->GetResult()<<std::endl;
}

线程ID

pthread_self

cpp 复制代码
#include <pthread.h>
// 获取线程ID
pthread_t pthread_self(void);

打印出来的 tid 是通过 pthread 库中有函数 pthread_self 得到的,它返回一个 pthread_t 类型的

变量,指代的是调用pthread_self 函数的线程的 "ID"。

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

维持的。

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

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

cpp 复制代码
$ ps -aL | head -1 && ps -aL | grep mythread
PID
LWP TTY
TIME CMD
2711838 2711838 pts/235 00:00:00 mythread
2711838 2711839 pts/235 00:00:00 mythread
-L 选项:打印线程信息

LWP 是什么呢?LWP 得到的是真正的线程ID。之前使用 pthread_self 得到的这个数实际上是⼀

个地址,在虚拟地址空间上的⼀个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程ID,线程栈,寄存器等属性。

在 ps -aL 得到的线程ID,有⼀个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在虚拟

地址空间的栈上,而其他线程的栈在是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的。而pthread库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。

长话短说:线程ID不同于进程ID,进程ID是操作系统给进程分配的统一标识方便进程管理,而线程ID是线程内部方便管理线程而统一标识的,操作系统不认线程ID,同一进程的不同线程PID相同,而LWD(TID)不同,对PID操作相当于对所有TID操作,对某个TID操作一般不影响同一个进程下的其他线程

线程终止

不能使用exit()来结束线程,exit是操作系统对进程的结束命令,调用后会杀手全部同一进程下的全部线程,所有得用专门的接口

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

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

  2. 线程可以调用pthread_ exit终止自己。

  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

pthread_exit

cpp 复制代码
功能:线程终⽌
原型:
void pthread_exit(void *value_ptr);
参数:
value_ptr:value_ptr不要指向⼀个局部变量。
返回值:
⽆返回值,跟进程⼀样,线程结束的时候⽆法返回到它的调⽤者(⾃⾝)

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

pthread_cancel函数

cpp 复制代码
功能:取消⼀个执⾏中的线程
原型:
int pthread_cancel(pthread_t thread);
参数:
thread:线程ID
返回值:成功返回0;失败返回错误码

线程等待

为什么需要线程等待?

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

pthread_join

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

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过

pthread_join得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。

  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常

数PTHREAD_ CANCELED。

  1. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。

  2. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

线程分离

pthread_join

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join 操作,否则

无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。分离的线程在未退出的情况下,依旧在进程的地址空间中,进程的所有资源,被分离的进程依旧可以访问,可以操作。

cpp 复制代码
int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

cpp 复制代码
pthread_detach(pthread_self());

joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

cpp 复制代码
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>


void *routine(void *args)
{
    int cnt = 5;
    while (cnt--)
    {
        std::cout << "新线程" << std::endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{

    pthread_t tid;
    pthread_create(&tid, nullptr, routine, (void *)"pthead-1");

    // 主分新
    pthread_cancel(tid);
    std::cout << "新线程被取消" << std::endl;
    int cnt = 5;
    while (cnt--)
    {
        std::cout << "main线程" << std::endl;
        sleep(1);
    }
    int n = pthread_join(tid, nullptr);
    if (n != 0)
    {
        std::cout << "pthread_join error:" << strerror(n) << std::endl;
    }
    else
    {
        std::cout << "pthread_join success" << strerror(n) << std::endl;
    }
}

被分离的线程不能被线程等待,会失败

cpp 复制代码
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>


void *routine(void *args)
{
    int cnt = 5;
    while (cnt--)
    {
        std::cout << "新线程" << std::endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{

    pthread_t tid;
    pthread_create(&tid, nullptr, routine, (void *)"pthead-1");

    // 主分新
    pthread_detach(tid);
    std::cout << "新线程被分离" << std::endl;
    int cnt = 5;
    while (cnt--)
    {
        std::cout << "main线程" << std::endl;
        sleep(1);
    }
    int n = pthread_join(tid, nullptr);
    if (n != 0)
    {
        std::cout << "pthread_join error:" << strerror(n) << std::endl;
    }
    else
    {
        std::cout << "pthread_join success" << strerror(n) << std::endl;
    }
}

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

Linux本身没有线程,它是用轻量级进程模拟出来的,OS提供的接口,不会直接提供线程接口,在用户层,封装轻量级进程,形成原生线程库:

可执行程序依赖对应的库,两个都是ELF可执行程序,我们可执行程序加载,形成进程,动态链接和动态地址重定向,要将动态库加载到内存还要映射到当前进程的地址空间中

当我们运行可执行文件,首先进程创建一个test------struct的结构体,创建地址空间,链接库,将pthread加载到内存并映射到地址空间,我自己写的代码会用到pthead中的变量方法,采用库的起始地址+偏移量就能访问库的内容,线程的概念是在库里维护的,在库内部,就一定会存在多个被创建好的线程,库也要管理线程,线程也有自己独立的栈

先描述,再组织,我们没创建一个线程,就会在库内创建一个描述线程的结构体,开头就是描述线程的TCB,struct pthread存有各种属性,线程局部存储可以存放各个线程单独的变量,独立访问,再次创建线程,会在上一个线程的实例化后面紧挨着再次创建对应的TCB,而pthread_create()有一个很大的返回值,那不是LWP,而是线程在库当中对应管理块的虚拟地址,线程控制块种有一个void* ret;当这个控制块对应的代码执行完会把返回值写到对应的ret内,线程运行结束资源并没有被释放,所以线程需要pthread_join,线程结束只是函数结束完了,对应的管理块并没有被释放,pthread_join种传入的tid就是上一个执行完的线程的tid,通过该函数再把上一个线程的运行结果带出开,然后释放该控制块的资源,并解决内存泄漏

所以,我在自己的代码里面调用pthread_create,是在动态库内部帮我们创建动态库描述的控制块,管理块的开头叫TCB,里面包含了描述线程的相关信息,而TCB里包含了void *ret字段,而当前线程运行结束的时候,会把自己的退出状态写到自己的控制块ret里,然后主线程又和新线程共享地址空间,主线程也就能通过起始地址+偏移量找到对应线程的控制块,主线程通过pthread_join拿到对应的退出状态拷贝回来,进程结束LWP自动释放,但是库里的东西没被释放,LWP查不到内存泄漏

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

pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类

型的线程ID,本质就是一个进程地址空间上的一个地址。

用户线程VSLWP

调用pthread_create,在库中创建管理块,要在内核创建轻量级进程(调用系统调用clone),既要创建出对应的控制块也要创建出对应线程,第一个参数是传入多线程函数执行的入口地址,第二个传入线程私有的独栈结构地址,这样内核数据和用户数据就联动起来了,将来执行的就是传入的这部分方法和结构,将来调函数栈帧和临时变量在线程栈里执行,CPU调度时临时数据在用户空间保存

创建多线程

cpp 复制代码
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include<vector>

void *routine(void *args)
{  
    std::string name = static_cast<const char*>(args);
    delete (char*)args;
    int cnt=5;
    while(cnt--)
    {
        std::cout<<"new线程名字"<<name<<std::endl;
        sleep(1);
      
        
    }
  return nullptr;
}
const int num=10;
int main()
{
std::vector<pthread_t>tids;
for(int i=0;i<num;i++)
{
    pthread_t tid;
    char* id=new char[64];
    snprintf(id,64,"thread-%d",i);
    int n=pthread_create(&tid,nullptr,routine,id);
    if(n==0)
    {
        tids.emplace_back(tid);
    }
    else
    continue;
    sleep(1);
}


  for(int i=0;i<num;i++)
  {
    //一个一个等
   int n= pthread_join(tids[i],nullptr);

    if(n==0)
    {
        std::cout<<"等待新线程成功"<<std::endl;
    }
  }
  return 0;
}
相关推荐
搬码临时工4 分钟前
动态域名服务ddns怎么设置?如何使用路由器动态域名解析让外网访问内网?
服务器·网络·tcp/ip·智能路由器·访问公司内网
Java程序员-小白12 分钟前
使用java -jar命令指定VM参数-D运行jar包报错问题
java·开发语言·jar
算命吗?你算什么东西13 分钟前
腾讯云-人脸核身+人脸识别教程
服务器·云计算·腾讯云·环境搭建·人脸·人脸核身
yangshuo128135 分钟前
风车OVF镜像:解放AI开发限制的Ubuntu精简系统
linux·人工智能·ubuntu
文牧之41 分钟前
AutoVACUUM (PostgreSQL) 与 DBMS_STATS.GATHER_DATABASE_STATS_JOB_PROC (Oracle) 对比
运维·数据库·postgresql·oracle
flypig哗啦啦43 分钟前
ubuntu服务器版启动卡在start job is running for wait for...to be Configured
linux·运维·ubuntu
_Itachi__1 小时前
LeetCode 热题 100 114. 二叉树展开为链表
linux·leetcode·链表
ClearViper31 小时前
Java的多线程笔记
java·开发语言·笔记
敷啊敷衍1 小时前
深入探索 C++ 中的 string 类:从基础到实践
开发语言·数据结构·c++
不之道1 小时前
通过 wsl 安装 docker
运维·docker·容器