【Linux】从零开始认识多线程 --- 线程控制

news2024/11/14 3:44:21

在这里插入图片描述

在这个浮躁的时代
只有自律的人才能脱颖而出
-- 《觉醒年代》

从零开始认识多线程 --- 线程控制

  • 1 知识回顾
  • 2 线程控制
    • 2.1 线程创建
    • 2.2 线程等待
    • 2.3 线程终止
  • 3 测试运行
    • 3.1 小试牛刀 --- 创建线程
    • 3.2 探幽析微 --- 理解线程参数
    • 3.3 小有心得 --- 探索线程返回
    • 3.4 求索无厌 --- 实现多线程
    • 3.5 返璞归真 --- 线程终止与线程分离
  • 4 语言层的线程封装
  • Thanks♪(・ω・)ノ谢谢阅读!!!
  • 下一篇文章见!!!

1 知识回顾

上一篇文章中,我们通过对地址空间的再次学习来认识了线程:

  1. 物理空间不是连续的,是4kb的内存块(页框)组成的。
  2. 页表映射是通过虚拟地址来索引物理地址:
    • 虚拟地址共32位:前10位用来索引页目录中的元素(页表),中间10位用来索引页表中的对应的元素(页框),后12位用来索引页框中的每一个字节
  3. 虚拟地址本质是一种资源,可以进行分配!对一个进程的数据进行分配执行,就是多线程的本质!
  4. Linux中的线程是通过进程模拟的(并没有单独设计出一个单独的线程模块)
  5. 进程中可以有多个进程(之前学习的是进程的特殊情况),他们共用一个地址空间。进程从内核来看,是承担分配系统资源的基本实体!
  6. Linux中的执行流是线程 ,CPU看到的执行流 <= 进程

进程与线程需要注意:

  1. 线程的调度成本比进程低很多,是由于硬件原因:CPU中存在一个cache会储存热点数据(进程相关数据) ,要访问数据时,会先在cache中寻找,如果命中直接访问,反之进行置换。切换进程需要更换热点数据,切换线程不需要切换。
  2. 线程的健壮性很差!一个线程出错会导致整个线程退出,而不同进程是独立的互不影响!进程和线程各有特长!
  3. 线程的本质是代码块!只使用函数的对应代码,即拿页表的一部分来执行!!!
  4. 线程的使用场景多为计算密集型和IO密集型,可以充分使用CPU的并行能力!

同一个进程中的线程虽然共享一个地址空间,但是还是有独属于自己的一些东西:

  1. 一组寄存器:在硬件中储存上下文数据,保证线程可以动态并行运行!
  2. 栈空间:线程中可以处理自己的临时变量,临时变量储存在自己独立的栈区,可以独立完成任务。
  3. 线程ID
  4. errno信号屏蔽字
  5. 调度优先级

复习的差不多了,我们了解了线程的基本概念,接下来就要开始学习如何管理线程 — 线程控制。根据我们之前学习的进程控制,大概可以估计一下线程控制的基本接口:线程创建 , 线程等待 , 线程退出…

2 线程控制

2.1 线程创建

万事开头难,我们先来看线程怎么创建:

PTHREAD_CREATE(3)                                                   Linux Programmer's Manual                                                  PTHREAD_CREATE(3)

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);

       Compile and link with -pthread.

pthread_create是创建线程的接口,里面有4个参数:

  1. pthread_t *thread :输出型参数,线程ID。
  2. const pthread_attr_t *attr :线程属性(优先级,上下文…),默认传入nullptr
  3. void *(*start_routine) (void *) : 函数指针,线程需要执行的函数地址。
  4. void arg:想要传入到线程的信息,可以传入int,string地址或者传入一个类对象的地址。

再来看返回值:

RETURN VALUE
       On success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.

pthread系列的函数的返回值是都是一样的:成功返回0,反之返回错误码!

2.2 线程等待

学习进程的时候,如果进程创建出来了,但是不进行等待,就拿不到退出信息,还会造成僵尸进程,进而造成内存泄漏。同样线程也需要进行等待。由主线程来等待新线程

PTHREAD_JOIN(3)                                                     Linux Programmer's Manual                                                    PTHREAD_JOIN(3)

NAME
       pthread_join - join with a terminated thread

SYNOPSIS
       #include <pthread.h>

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

       Compile and link with -pthread.

这个函数里面有2个参数:

  1. pthread_t thread:需要进行等待的线程ID
  2. void **retval: 获取的返回信息

2.3 线程终止

牢记:main线程结束那么进程结束,所以一定要保证main线程最后退出。

  1. 最简单的线程终止是线程函数返回return
  2. 切记不要使用exit(),我们在进程控制中学习过exit()可以退出进程,但是要注意线程是在一个进程中讨论的,新线程如果使用了exit()那整个进程就退出了!exit()不可以用来终止线程
  3. 操作系统也给我们提供了线程终止的接口:
PTHREAD_CANCEL(3)                                                   Linux Programmer's Manual                                                  PTHREAD_CANCEL(3)

NAME
       pthread_cancel - send a cancellation request to a thread

SYNOPSIS
       #include <pthread.h>

       int pthread_cancel(pthread_t thread);

       Compile and link with -pthread.

通过这个参数,可以看出来这是个很简单的接口,终止对应tid的线程。只要线程存在,并且知道tid , 就可以终止线程(可以自己终止自己)。线程终止的返回值是一个整数!

3 测试运行

3.1 小试牛刀 — 创建线程

我们进行一个简单的测试,来使用这两个接口:
注意,使用线程库的接口需要动态链接g++ -o $@ $^ -std=c++11 -lpthread

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <string>

// 测试 1
void *ThreadRun(void *args)
{
    std::cout << "name: " << *(std::string*)args << " is running"<< std::endl;
    sleep(1);
    std::string* ret = new std::string(*(std::string*)args + "finish...") ;

    return (void*)ret;
}

int main()
{
    // 创建一个新线程
    // int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
    pthread_t tid;
    std::string name = "thread - 1";
    pthread_create(&tid, nullptr, ThreadRun, &name);

    //进程等待
    //int pthread_join(pthread_t thread, void **retval);
    std::string *ret = nullptr;
    pthread_join(tid, (void**)&ret);
    std::cout << *(std::string*)ret << std::endl;

    return 0;
}

编译运行一下,我们可以看到:
在这里插入图片描述
新线程完成了任务!

问题 1 : main线程和new线程谁先运行? 不确定,和进程的调度方式一致,由具体情况来定。

问题 2 : 我们期望谁先退出?肯定是main线程,所以就有join来进行等待,阻塞等待线程退出。如果不进行join,就会造成类似僵尸进程的情况(内存泄漏)!

问题 3 :tid是什么样子的,我们可不可以看一看?当然可以:

std::string ToHex(int x)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", x);
    return buffer;
}

这样就可以打印出来tid的十六进制:
在这里插入图片描述
这数字好像和lwp不一致啊
在这里插入图片描述
为什么tid这么大?其实tid是一个虚拟地址!!!

3.2 探幽析微 — 理解线程参数

问题 4 : 全面看待线程函数传参。上面我们的程序传入了name变量的地址,让线程获取了对应的名字。如果想要传入多个变量或方法,可以传入类对象的地址:

class ThreadData
{
public:
    std::string name;
    int num;
};
vvoid *ThreadRun(void *args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    std::cout << "name: " << td->name << " is running" << std::endl;
    std::cout << "num: " << td->num << std::endl; 
    sleep(1);
    std::string *ret = new std::string(*(std::string *)args + "finish...");

    return (void *)ret;
}

std::string ToHex(int x)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", x);
    return buffer;
}

int main()
{
    // 创建一个新线程
    // int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
    pthread_t tid;
    // std::string name = "thread - 1";
    ThreadData td;
    td.name = "thread - 1";
    td.num = 100;
    pthread_create(&tid, nullptr, ThreadRun, &td);

    // 查看tid
    sleep(1);
    std::cout << "tid: " << ToHex(tid) << std::endl;

    // 进程等待
    // int pthread_join(pthread_t thread, void **retval);
    std::string *ret = nullptr;
    pthread_join(tid, (void **)&ret);
    std::cout << *(std::string *)ret << std::endl;

    return 0;
}

这样就可以传入多个变量:
在这里插入图片描述
所以这个void*的变量是可以传入任何地址的,一定要想到可以传入类对象。但是刚写的有些问题,我们上面的写法是在主线程的栈区创建变量,让新线程读取主线程的栈,不太合适(破坏了一定独立性)!如果多个变量都传入了这个变量,那么修改一个就会造成所以的线程中的数据都发生改变!!!这可不行!推荐写:

	ThreadData* td = new ThreadData();
    td->name = "thread - 1";
    td->num = "100";
    pthread_create(&tid, nullptr, ThreadRun, td);

这是在堆区进行开辟空间,然后将该空间交给新线程来管理!就不会出现这样的问题了!以后我们都使用这种方式来传递参数!!!

3.3 小有心得 — 探索线程返回

问题 5 :线程的返回值输出型参数void** retval,他需要我们传递一个void*变量,然后返回值就交给了void*变量!这个过程就是对一个指针进行改变其指向的内容的操作。

下面是一个让新线程进行加法工作的程序

void *ThreadRun(void *args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    std::cout << "name: " << td->name << " is running" << std::endl;
    std::cout << "num: " << td->num << std::endl; 
    sleep(1);
    delete td;
    //返回值
    std::string *ret = new std::string(*(std::string *)args + "finish...");
    return (void *)ret;
}

这就将void*变量返回给&(void* ret)变量,让ret指向对应的堆区。这就类似int a放入int * 中就可以改变a的值

问题 5 :如何全面的看待线程的返回。我们知道如果一个线程出现问题,整个进程就会退出。所以线程的返回只有正常的返回,没有异常的返回,出现异常整个进程会直接退出,根本没有返回错误信息的机会!和传入参数音参数一样,我们也可以返回一个类对象来传递多个变量。

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <string>

// 测试 1
class ThreadData
{
public:
    std::string name;
    int num1;
    int num2;
};

class ThreadResult
{
public:
    std::string name;
    int num1;
    int num2;
    int ans;
};

void *ThreadRun(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    std::cout << "name: " << td->name << " is running" << std::endl;
    std::cout << "num1: " << td->num1 << " num2: " << td->num2 << std::endl;
    sleep(1);

    ThreadResult *ret = new ThreadResult();
    ret->name = td->name;
    ret->num1 = td->num1;
    ret->num2 = td->num2;
    ret->ans = td->num2 + td->num1;

    delete td;
    return (void *)ret;
}

std::string ToHex(int x)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", x);
    return buffer;
}

int main()
{
    // 创建一个新线程
    // int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
    pthread_t tid;
    // std::string name = "thread - 1";
    ThreadData *td = new ThreadData();
    td->name = "thread - 1";
    td->num1 = 100;
    td->num2 = 88;

    pthread_create(&tid, nullptr, ThreadRun, td);

    // 查看tid
    sleep(1);
    std::cout << "tid: " << ToHex(tid) << std::endl;

    // 进程等待
    // int pthread_join(pthread_t thread, void **retval);
    ThreadResult *ret = nullptr;
    pthread_join(tid, (void **)&ret);
    
    std::cout << ret->num1 << " + " << ret->num2 << " = " << ret->ans << std::endl;

    return 0;
}

来看返回值:
在这里插入图片描述
我们成功获取了新线程中设置的返回值!非常nice!

3.4 求索无厌 — 实现多线程

问题 6 :上面只是创建了单独的一个线程,那如何创建多线程呢?
可以通过维护一个vector数组来对tid进行统一管理

void *ThreadRun(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        std::cout << name << "is running ..." << std::endl;
        sleep(1);
    }
    return (void *)0;
}

std::string ToHex(int x)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", x);
    return buffer;
}

const int num = 10;

int main()
{
    std::vector<pthread_t> tids;
    for (int i = 0; i < num; i++)
    {
        // 1. 线程ID
        pthread_t tid;
        // 2. 线程名字
        char* name = new char[128];
        snprintf(name, 128, "thread - %d", i + 1);
        pthread_create(&tid, nullptr, ThreadRun, name);
        //保存所有线程的ID
        tids.push_back(tid) ;
    }
    //join
    sleep(100);


    return 0;
}

在这里插入图片描述
这样就创建出了10个新线程,但是我们看这些新线程的的名字好像不太对:
在这里插入图片描述
怎么不是1 - 10???完全是乱的!因为线程谁先被调度运行不确定!而我们传入的名字是在主线程的栈区域,可能在新线程还没有调度,name就已经在主线程中被覆盖了!解决办法很简单,我们创建在堆区就可以了

for (int i = 0; i < num; i++)
    {
        // 1. 线程ID
        pthread_t tid;
        // 2. 线程名字
        //在堆区进行创建。防止被重写覆盖
        char* name = new char[128];
        snprintf(name, 128, "thread - %d", i + 1);
        pthread_create(&tid, nullptr, ThreadRun, name);
        pids.push_back(tid) ;
    }

在这里插入图片描述
这样就整齐多了!

接下来就要进行等待:
我们已经通过vector容器来维护了创建所有线程的tid,所以只需要对所有的tid进行join就好了!

void *ThreadRun(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        std::cout << name << "is running ..." << std::endl;
        sleep(3);
        break;
    }
    return nullptr;
}

std::string ToHex(int x)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", x);
    return buffer;
}

const int num = 10;

int main()
{
    std::vector<pthread_t> tids;
    for (int i = 0; i < num; i++)
    {
        // 1. 线程ID
        pthread_t tid;
        // 2. 线程名字
        char* name = new char[128];
        snprintf(name, 128, "thread - %d", i + 1);
        pthread_create(&tid, nullptr, ThreadRun, name);
        //保存所有线程的ID
        tids.push_back(tid) ;
    }
    //join
    for (auto tid : tids)
    {
        pthread_join(tid , nullptr);
        std::cout << ToHex(tid) << " quit..." << std::endl;
    }
} 

来看运行效果:
在这里插入图片描述
非常好!!!

我们也可以通过返回值来获取线程的名字:

    for (auto tid : tids)
    {
        void* name = nullptr;
        pthread_join(tid , &name);
        std::cout << (const char*)name<< " quit..." << std::endl;
        delete (const char*)name;
    }

非常优雅!
在这里插入图片描述

3.5 返璞归真 — 线程终止与线程分离

问题 7 :线程终止的返回值
我们来看看通过线程终止接口终止的线程返回值是什么样的:

void *ThreadRun(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        std::cout << name << "is running ..." << std::endl;
        sleep(3);
        //break;
    }
    return args;
}

std::string ToHex(int x)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", x);
    return buffer;
}

const int num = 10;

int main()
{
    std::vector<pthread_t> tids;
    for (int i = 0; i < num; i++)
    {
        // 1. 线程ID
        pthread_t tid;
        // 2. 线程名字
        char* name = new char[128];
        snprintf(name, 128, "thread - %d", i + 1);
        pthread_create(&tid, nullptr, ThreadRun, name);
        //保存所有线程的ID
        tids.push_back(tid) ;
    }
    //join
    sleep(3);
    
    for (auto tid : tids)
    {
        pthread_cancel(tid);
        std::cout <<  " cancel: " << ToHex(tid) << std::endl;
        void* ret= nullptr;
        pthread_join(tid , &ret);
        std::cout << (long long int)ret << " quit..." << std::endl;

    }

    return 0;
}

在这里插入图片描述
可以看的,被phread_cancel()终止的线程的返回值是 -1!这个 -1其实是宏定义#define PTHREAD_CANCELED ((void *) -1)。线程终止的方式有三种:

  1. 线程函数 return
  2. pthread_cancel 新线程退出结果为-1
  3. pthread_exit

问题 8 :可不可以不通过join线程,让他执行完就退出呢,当然可以!
这里需要线程分离接口:

PTHREAD_DETACH(3)                                                   Linux Programmer's Manual                                                  PTHREAD_DETACH(3)

NAME
       pthread_detach - detach a thread

SYNOPSIS
       #include <pthread.h>

       int pthread_detach(pthread_t thread);

       Compile and link with -pthread.

通过这个接口,分离出去的线程依然属于进程内部,但不需要被等待了。举个例子,之前再讲线程与进程的关系时,我们把不同的线程比作家庭成员,做好自己分内的事情,既可以让家庭幸福,即进程成功运行。而进程分离就好比你长大了,自己搬出去住,不受父母管了,但是依旧属于这个家庭。这种状态就是线程分离。

当然,如果想要将自己分离出去,就要知道自己的tid,这里需要接口:

PTHREAD_SELF(3)                                                     Linux Programmer's Manual                                                    PTHREAD_SELF(3)

NAME
       pthread_self - obtain ID of the calling thread

SYNOPSIS
       #include <pthread.h>

       pthread_t pthread_self(void);

       Compile and link with -pthread.

这个接口会返回调用它的线程的ID。如同getpid()

void *ThreadRun(void *args)
{
    // 线程分离
    pthread_detach(pthread_self());
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        std::cout << name << "is running ..." << std::endl;
        sleep(3);
        break;
    }
    return args;
}

std::string ToHex(int x)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", x);
    return buffer;
}

const int num = 10;

int main()
{
    std::vector<pthread_t> tids;
    for (int i = 0; i < num; i++)
    {
        // 1. 线程ID
        pthread_t tid;
        // 2. 线程名字
        char *name = new char[128];
        snprintf(name, 128, "thread - %d", i + 1);
        pthread_create(&tid, nullptr, ThreadRun, name);
        // 保存所有线程的ID
        tids.push_back(tid);
    }

    sleep(3);

    for (auto tid : tids)
    {
        pthread_cancel(tid);
        std::cout << " cancel: " << ToHex(tid) << std::endl;
        void *ret = nullptr;
        int n = pthread_join(tid, &ret);
        std::cout << (long long int)ret << " quit... , n: " << n << std::endl;
    }

    return 0;
}

可以看到,如果我们等待一个已经分离出去的线程,会得到22号错误信息!所以不能 join 一个分离的线程!
在这里插入图片描述
所以主线程就可以不管新线程,可以继续做自己的事情,不用阻塞在join!

但是注意:线程分离了,依然是同一个进程!一个线程出异常,会导致整个进程退出!

上面是自己分离自己。也可以通过主线程分离新进程:

    for (auto tid : tids)
    {
        pthread_detach(tid);//主线程分离新线程
    }

4 语言层的线程封装

上面讲的是Linux系统提供给我们的系统调用,帮助我们可以进行线程控制,也叫做原生线程库。我们熟悉了底层的原生线程库,就会方便很多。
我们来看C++11中的线程

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <vector>
#include <string>
#include <thread>

void threadrun( int num)
{
    while (num)
    {
        std::cout << " num: " << num << std::endl;
    }
}

// C++中线程库
int main()
{
    std::thread mythread(threadrun, 10);

    while (true)
    {
        std::cout << "main thread..." << std::endl;
        sleep(1);
    }
    mythread.join();
    return 0;
}

注意,虽然是使用的语言层的线程库,但是依旧要连接thread动态库,因为语言层线程库的本质是对原生线程库接口的封装!!!无论是java还是python都要与原生线程库产生联系!

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1927658.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

金九银十,软件测试面试题大全(自动化篇+含答案)

“ 今天我给大家介绍一些python自动化测试中常见的面试题&#xff0c;涵盖了Python基础、测试框架、测试工具、测试方法等方面的内容&#xff0c;希望能够帮助你提升自己的水平和信心。” 项目相关 1.什么项目适合做自动化测试&#xff1f; 答&#xff1a;一般来说&#xff…

鸿蒙模拟器(HarmonyOS Emulator)Beta申请审核流程

文 | Promise Sun 一.背景&#xff1a; 鸿蒙项目开发需要使用模拟器进行开发测试&#xff0c;但目前想在DevEco Studio开发工具中使用模拟器就必须到华为官网进行报名申请&#xff0c;参加“鸿蒙模拟器&#xff08;HarmonyOS Emulator&#xff09;Beta活动申请”。 申请审核通…

在互联网供应链系统可能是永远不会过时的系统

一、前言 在互联网在到人工智能&#xff0c;从基本的门户网站&#xff0c;社交网站&#xff0c;到移动互联网&#xff0c;视频网站&#xff0c;再到现在比较火爆短视频直播和人工智能AI&#xff0c;大模型。互联网的迭代&#xff0c;出现了无数的系统。但是有些系统一直久经不…

知识图谱和 LLM:利用Neo4j驾驭大型语言模型(探索真实用例)

这是关于 Neo4j 的 NaLLM 项目的一篇博客文章。这个项目是为了探索、开发和展示这些 LLM 与 Neo4j 结合的实际用途。 2023 年,ChatGPT 等大型语言模型 (LLM) 因其理解和生成类似人类的文本的能力而风靡全球。它们能够适应不同的对话环境、回答各种主题的问题,甚至模拟创意写…

Java中的流类型详解

Java中的流类型详解 1、按照流的方向分类1.1 输入流&#xff08;InputStream&#xff09;1.2 输出流&#xff08;OutputStream&#xff09; 2、按照实现功能分类2.1 节点流&#xff08;Node Stream 或 Basic Stream&#xff09;2.2 处理流&#xff08;Wrapper Stream 或 Proces…

Java语言程序设计——篇四(2)

类和对象 方法设计定义和使用方法访问方法和修改方法方法的调用方法参数的传递✨方法重载✨构造方法(构造器)&#x1f6a9;this关键字this关键字主要用于以下两种情况&#xff1a; 编程练习静态变量和静态方法静态变量静态方法&#x1f308;解释&#xff1a;main方法的访问权限…

玩客云刷入海纳思系统

玩客云(晶晨S805)刷机 | 海纳思系统 (ecoo.top) https://www.ecoo.top/update/soft_init/amlproject/USB_Burning_Tool_v2.1.3.exe https://node4.histb.com:9088/update/system/s805/hinas_s805_eMMC.burn.img.zip

【排序算法】—— 归并排序

归并排序时间复杂度O(NlongN)&#xff0c;空间复杂度O(N)&#xff0c;是一种稳定的排序&#xff0c;其次可以用来做外排序算法&#xff0c;即对磁盘(文件)上的数据进行排序。 目录 一、有序数组排序 二、排序思路 三、递归实现 四、非递归实现 一、有序数组排序 要理解归…

[算法] 优选算法(五):二分查找(上)

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…

Linux虚拟机扩展磁盘空间

文章目录 在VM上进行扩展新的磁盘空间进入虚拟机将扩展的磁盘空间分配给对应的分区 VM 下的Linux虚拟机提示磁盘空间不足&#xff0c;需要对其进行磁盘扩容&#xff0c;主要有以下两步&#xff1a; 在VM上进行扩展新的磁盘空间 先关闭虚拟机在VM的虚拟机设置处进行硬盘扩展 …

STM32自己从零开始实操:PCB全过程

一、PCB总体分布 以下只能让大家看到各个模块大致分布在板子的哪一块&#xff0c;只能说每个人画都有自己的理由&#xff1a; 电源&#xff1a;从外部接入电源&#xff0c;5V接到中间&#xff0c;向上变成4V供给无线&#xff0c;向下变成3V供给下面的接口&#xff08;也刻意放…

Java---SpringBoot详解二

勤奋勤劳铸梦成&#xff0c; 晨曦微露起长征。 汗水浇灌花似锦&#xff0c; 寒窗苦读岁月明。 千锤百炼心如铁&#xff0c; 万里征途志不倾。 持之以恒终有日&#xff0c; 功成名就笑谈中。 目录 一&#xff0c;统一响应结果 二&#xff0c;三层架构 三&#xff0c;分层解耦 四…

力扣第九题

回文数 提示&#xff1a; 给你一个整数 x &#xff0c;如果 x 是一个回文整数&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 回文数是指正序&#xff08;从左向右&#xff09;和倒序&#xff08;从右向左&#xff09;读都是一样的整数。 代码展示&#…

请你谈谈:AnnotatedBeanDefinitionReader 显式地注册一个Bean到Spring容器,以及注册并解析配置类

为了深入探讨Spring框架中的beanDefinition对象&#xff0c;我们不可避免地要提及BeanFactoryPostProcessor这一核心类&#xff0c;它作为Spring的bean工厂后置处理器发挥着关键作用。接下来&#xff0c;我们将详细讨论BeanFactoryPostProcessor的执行时机&#xff0c;这是一个…

顶顶通呼叫中心中间件-添加自定义变量到CDR方法(mod_cti基于FreeSWITCH)

顶顶通呼叫中心中间件-添加自定义变量到CDR方法(mod_cti基于FreeSWITCH) 1、自定义变量添加到cti.json 例&#xff1a;需要添加的变量为“bridge_uepoch" 2、添加进数据库 在数据库中找到表"cdr"在cdr表中也添加数据&#xff0c;数据名为新变量名&#xff1a…

基于Java的科大讯飞大模型API调用实现

写在前面&#xff1a;因为现在自己实习的公司新拓展的一个业务是结合AI的低代码平台&#xff0c;我负责后端的开发&#xff0c;之前一直都是直接使用gpt或者文心一言等ui界面来直接使用大模型&#xff0c;从来没有自己调接口过&#xff0c;所以本文记录一下自己第一次使用大模型…

P2p网络性能测度及监测系统模型

P2p网络性能测度及监测系统模型 网络IP性能参数 IP包传输时延时延变化误差率丢失率虚假率吞吐量可用性连接性测度单向延迟测度单向分组丢失测度往返延迟测度 OSI中的位置-> 网络层 用途 面相业务的网络分布式计算网络游戏IP软件电话流媒体分发多媒体通信 业务质量 通过…

从零开始做题:什么奇奇怪怪的东西

题目 解题 mrf拓展名&#xff0c;macro recorder打开&#xff0c;鼠标键盘的记录 然后解压flag.zip即可&#xff0c;发现有一个挂载的文件&#xff0c;直接打开后 显示所有的隐藏文件 一个一个打开 然后进行拼接运行吧估计。 首先打开txt文件直接久就给出了代码&#xff1…

maven项目容器化运行之2-maven中使用docker插件调用远程docker构建服务并在1Panel中运行

一.背景 公司主机管理小组的同事期望我们开发的maven项目能够在1Panel管理的docker容器部署。上一篇写了先开放1Panel中docker镜像构建能力maven项目容器化运行之1-基于1Panel软件将docker镜像构建能力分享给局域网-CSDN博客。这一篇就是演示maven工程的镜像构建、容器运行、运…

学习大数据DAY16 PLSQL基础语法5

目录 异常 自定义异常的格式 raise_application_error 处理异常 预定义异常 SQLcode和SQLerrm 非预定义异常 作业 触发器 触发器基本概念 DML触发器 DML触发器使用 instead of 触发器 管理触发器 作业2 函数、过程和包 函数 过程 参数 1. in 参数 2.out 参…