Linux(线程控制)

Linux(线程控制)

  • [1 线程的相关函数](#1 线程的相关函数)
  • [2 线程的创建](#2 线程的创建)
    • [2.1 代码认识pthread_create函数](#2.1 代码认识pthread_create函数)
    • [2.2 线程tid](#2.2 线程tid)
    • [2.3 线程等待](#2.3 线程等待)
    • [2.4 pthread_create的第4个参数和新线程返回值问题](#2.4 pthread_create的第4个参数和新线程返回值问题)
    • [2.5 多线程代码](#2.5 多线程代码)
    • [2.6 栈也是共享的,但是一般我们不会这么干](#2.6 栈也是共享的,但是一般我们不会这么干)
  • [3 线程退出](#3 线程退出)
    • [3.1 return](#3.1 return)
    • [3.2 exit](#3.2 exit)
    • [3.3 pthread_exit](#3.3 pthread_exit)
    • [3.4 pthread_cancel(删掉一个线程)](#3.4 pthread_cancel(删掉一个线程))
  • [4 主线程执行自己的事情](#4 主线程执行自己的事情)
  • [5 线程进行程序替换](#5 线程进行程序替换)

1 线程的相关函数

(1)man pthread_create:

pthread_create它不是一个系统调用,它是一个库函数,在Linux内核中,没有线程的概念!只有LWP(轻量级进程),线程是用LWP模拟实现的,所以Linux系统不会给我们提供线程接口,只会提供创建轻量级进程的接口

这个创建轻量级进程的接口就是vfork:

(2)、上面这两个函数都是封装的clone这个系统调用,但是这个系统调用很难被使用

(3)、虽然Linux没有线程这个概念,但是作为我们使用者来说我们一直接触的都是线程,如果我们现在想在Linux下创建线程岂不是还要学习LWP这些概念吗?Linux的开发者为了方便用户的使用就新增了一层软件层,将LWP封装,我们将线程真正的提取出来是在用户层,而并非内核层,这种概念叫做用户级线程

(4)、因为windows真的有线程这种概念,所以windows下是真的有创建线程的系统调用

2 线程的创建

2.1 代码认识pthread_create函数

(1)在Linux中线程的库我们叫做pthread库(用户级),Linux系统自带!(原生线程库),以后我们创建线程都用pthread_create这个库函数

第一个参数:线程id

第二个参数:线程属性(这里我们不需要,统一设置为nullptr)

第三个参数:函数指针(线程执行)

第四个参数:我们给线程起的名字,这个参数就是我们传递给第三个函数指针的参数(回调函数),这个参数的类型为void*,意思是我们可以传递任意类型的参数

返回值:创建失败的原因

(2)创建线程的demo样例

1、mythread.cc

2、makefile

ps -al | grep mythread(另一个终端查看该进程)

(3)C++也支持创建线程:

2.2 线程tid

如果我们将线程的tid打印出来:

(1)子线程打印自己的 tid(pthread_self)

哪一个线程调用该函数就返回该线程的tid

(2)新线程和main线程谁先运行,不确定,调度由OS自主决定,其次线程会瓜分进程的资源,时间片也会被瓜分

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

std::string ToHex(pthread_t tid)
{
    char buff[64];
    snprintf(buff, sizeof(buff), "0x%lx", tid);
    return buff;
}

//被重入
void *routine(void *args) // args就是pthread_create里的最后一个参数(线程名字)
{
    std::string name = static_cast<const char *>(args);
    while (1)
    {
        std::cout << "我是新线程,我的名字是:" << name << "  tid:" << ToHex(pthread_self()) << std::endl;
        sleep(1);
    }
    return 0;
}
int main()
{
    pthread_t tid1;
    pthread_create(&tid1, nullptr, routine, (void *)"thread-1");

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

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

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

    std::cout << "new thread is " << ToHex(tid1) << std::endl;
    std::cout << "new thread is " << ToHex(tid2) << std::endl;
    std::cout << "new thread is " << ToHex(tid3) << std::endl;
    std::cout << "new thread is " << ToHex(tid4) << std::endl;
    std::cout << "new thread is " << ToHex(tid5) << std::endl;

    while (1)
    {
        std::cout << "我是主线程" << std::endl;
        sleep(1);
    }
}

上面这一份代码就是我们创建多线程的代码示例,但是我们这里让所有线程执行同一个函数,只有参数的区别,打印的结果大家可以下来试一下(每一个线程的tid都不同),这符合我们的预期,但是我们之前学过线程重入的概念,这里不是发生重入了吗?

(3)在不加保护的情况下,显示器资源就是共享资源

(4)进程内的函数,线程共享,全局变量也是被所有线程共享

(5)如果一个线程崩溃,就会引起所有线程的崩溃,即进程崩溃

2.3 线程等待

(1)线程我们虽然不知道谁先运行,但我们必须保证主线程最后退出,并且线程也需要被等待和回收的,原因:1、可能会导致与僵尸进程类似的原因 2、为了知道该线程的执行结果

(1)参数一:指明哪一个新线程

(2)参数二:输出型参数 ,如果我们不关心退出结果就设为nullptr

(3)返回值:成功返回0,失败返回非0

(4)等待线程是要等待线程退出,那要是线程不退出?所以这个函数默认为阻塞等待

(2)上面这份代码中,子线程在运行,主线程在阻塞式等待

2.4 pthread_create的第4个参数和新线程返回值问题

(1)上面我们已经说了第四个参数是void*类型,意味着我们可以传递任何类型的参数,包括自定义类型,这意味着什么?我们可以在传参的同时让它帮我们完成一些任务,或者我们可以传递n个参数(封装为结构体或者类)

代码样例:

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

class ThreadData
{
public:
    ThreadData(const std::string name, int a, int b) : _name(name), _a(a), _b(b)
    {
    }
    int Excute()
    {
        return _a + _b;
    }
    std::string Name()
    {
        return _name;
    }

private:
    std ::string _name;
    int _a;
    int _b;
};

void *routine(void *args) // args就是pthread_create里的最后一个参数(线程名字)
{
    // std::string name = static_cast<const char *>(args);
    ThreadData *td = static_cast<ThreadData *>(args);

    while (true)
    {
        std::cout << "我是新线程,我的名字是:" << td->Name() << std::endl;
        std::cout << "任务执行结果:" << td->Excute() << std::endl;
        sleep(1);
        break;
    }
    return 0;
}

int main()
{
    pthread_t tid;
    ThreadData *td = new ThreadData("thread-1", 1, 2);
    pthread_create(&tid, nullptr, routine, (void *)td);

    int n = pthread_join(tid, nullptr);
    if (n != 0)
        std::cout << "pthread_join error" << std::endl;
    else
        std::cout << "pthread_join success" << std::endl;

    while (1)
    {
        std::cout << "我是主线程" << std::endl;
        sleep(1);
    }
}

执行结果:

(2)线程执行结束

1、线程入口函数return,表示线程退出(long long是因为void*是8字节的,转为int会有精度损失)

那我们现在思考一个问题,为什么thread_join的第二个返回值类型为void**,原因是当新线程退出时如果我们使用void类型,就返回不了(void它是没有大小的,OS根本不知道开辟多少空间给它),但是void*它是有大小的(32位为4字节,64位为8字节),为了能够成功传参这里采用的void**类型

2、新线程的返回值可以返回变量,数字,甚至是对象!(理论上堆空间也是共享的)

上面这个代码表示堆空间理论也是共享的,谁拿到堆空间的入口地址,谁就能访问堆空间!

3、总结:通过前面的学习,实际上进程地址空间除了栈,其他区其实都是共享的

2.5 多线程代码

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

class ThreadData
{
public:
    ThreadData() {}
    void Init(const std::string &name, int a, int b)
    {
        _name = name;
        _a = a;
        _b = b;
    }
    void Excute()
    {
        _result = _a + _b;
    }
    int Result() { return _result; }
    std::string Name() { return _name; }
    void SetId(pthread_t tid) { _tid = tid; }
    pthread_t Id() { return _tid; }
    ~ThreadData() {}

private:
    std::string _name;
    int _a;
    int _b;
    int _result;
    pthread_t _tid;
};

void *routine(void *args) // args就是pthread_create里的最后一个参数(线程名字)
{
    // std::string name = static_cast<const char *>(args);
    ThreadData *td = static_cast<ThreadData *>(args);
    while (true)
    {
        std::cout << "我是新线程,我的名字是:" << td->Name() << std::endl;
        td->Excute(); // 让新线程去执行任务
        sleep(1);
        break;
    }
    return td;
}

#define NUM 10

int main()
{
    ThreadData td[NUM];
    // 1、准备处理我们要加工的数据
    for (int i = 0; i < NUM; i++)
    {
        char id[64];
        snprintf(id, sizeof(id), "thread-%d", i);
        td[i].Init(id, i * 10, i * 20);
    }

    // 2、创建多线程
    for (int i = 0; i < NUM; i++)
    {
        pthread_t id;
        pthread_create(&id, nullptr, routine, &td[i]);
        td[i].SetId(id);
    }

    // 3、等待多个线程
    for (int i = 0; i < NUM; i++)
    {
        pthread_join(td[i].Id(), nullptr);
        //这里我们可以获取退出信息,上面我们已经演示过了
    }

    // 4、汇总处理结果
    for (int i = 0; i < NUM; i++)
    {
        printf("td[%d],result:[%d],id:[%ld]\n", i, td[i].Result(), td[i].Id());
    }

    while (1)
    {
        std::cout << "我是主线程" << std::endl;
        sleep(1);
    }
}

2.6 栈也是共享的,但是一般我们不会这么干

总结:所以线程会共享进程的所有进程地址空间内的所有区

3 线程退出

进程退出的方法

3.1 return

上面演示过了

3.2 exit

3.3 pthread_exit

demo样例:

3.4 pthread_cancel(删掉一个线程)

----- 取消一个线程,一定是该线程已经启动!

demo样例:

这里我们可以看到取消后拿到的返回值是-1,#define PTHREAD_CANCELED((void*) -1),所以取消线程也必须得join

4 主线程执行自己的事情

(1)上面我们使用join等待了线程的退出,但是这是阻塞式的等待,线程这里并没有非阻塞式的等待

---- 所以我们直接不等待新线程即可(将线程设置为分离状态)

(2)线程被等待的状态

1、joined:线程需要被join(默认)

2、detach:线程分离(主线程不需要等待新线程),类似于古时候的分家

3、注意:在多执行流情况下,主执行流一定是最后退的

(3)下面这个代码就是detach的demo,我们可以发现join的返回值为22,即出错返回

detach既可以在主线程中,又可以在新线程中

5 线程进行程序替换

线程其实是无法进行程序替换的,但是我们可以让新线程进行fork创建子进程

相关推荐
雯宝1 小时前
mnt_init 函数
linux
律宏阔7 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔7 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴7 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千8 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei378 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei378 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei378 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW8 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW8 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux