【Linux】Linux多线程

news2024/10/6 6:04:13

      • 怎么理解线程
      • 线程的优缺点
      • 线程的二级页表
      • 线程用途
      • 怎么管理线程
        • 线程创建
          • 获取创建后的ID方法
        • 线程等待
        • 线程终止
        • 分离线程
        • 线程ID及进程地址空间布局

怎么理解线程

通俗易懂的说,线程的运行是必须有进程给它打个基础。线程的运行是服用进程的代码及空间来进行运作的。

那么什么是线程?

线程是进程内部的一个分支,执行的粒度比进程更细,调用的成本更低。
此外,线程是cpu调度的基本单位,而进程是系统分配资源的基本实体。

  • 线程是进行内部的一个执行分支: 进程的代码被分为一个个部分有不同的线程来执行,线程提供了该部分的执行入口,操作系统会并发的调度各个进程
  • 线程执行粒度比进程更细: 每个线程一般只是执行进程代码中的一部分,且线程的创建一般是多个的,将进程瓜分的更细。
  • 线程的调度成本比进程更低:线程切换时,不需要修改地址空间和页表。

那么怎么理解进程和线程的区别

我先简单用文字描述以下,然后再看一下逻辑图

进程:实际上,进程包括进程控制块PCB(task_struct)、进程地址空间(虚拟内存)、以及页表和需要映射的物理内存。
当我们创建多个进程的时候, 这时它们都是独立的,它们都有属于自己的一套结构包括PCB。
在这里插入图片描述

线程:在上面的介绍中我们说到,线程是服用进程的那一套结构来完成的,那么线程自己是怎么实现服用和管理的呢?
因为线程可以有多个,多个线程的创建其实都是通过PCB(task_struct)的分支来完成的,分成多份来供线程使用。它们总体都是通过pthread_t来进程管理的

在这里插入图片描述

线程时CPU调度的基本单位
CPU只是机械的执行操作系统传入的命令,它区分不出进程和多线成,只是操作系统将task_struct以及相关的数据传入CPU,CPU就会根据task_struct执行相应的代码,无论进程内部只有一个执行流还是多个执行流,CPU都是以task_struct为单位进程调度的,因此称线程是CPU调度的基本单本
在这里插入图片描述

多线程下进程是系统分配资源的基本实体
进程是由一个或多个task_struct构成的执行流、进程地址空间、页表、代码和数据组成
在这里插入图片描述
只有在创建进程时,操作系统才会为其申请内存资源创建地址空间等结构和加载代码和数据,因此进程是系统分配资源的基本实体,因为操作系统会给进程分配系统资源,才会有内存空间用于task_struct的创建来实现多线程。

线程的优缺点

线程的优点

  • 多线程之间可以共享数据资源和数据,避免了进程间通信的开销和复杂性
  • 线程的创建和开销相对较小,相比进程来说更轻量级,因此被称为轻量级进程(LWP)。
  • 多线程可以实现任务的并行处理,提高系统的响应速度和吞吐量。

线程的缺点

线程的二级页表

注:二级页表用于32位计算机,64位计算机需采用三级页表,二级页表与三级页表的使用原理相同。

我们知道一个文件进行IO每次是以4KB来进行的,也就是八个扇区,然后对数据块进行整体的管理。
在我们的线程内部,我们每次存储也是按照4KB进行存储的。不管是物理内存还是磁盘,都是os统一的。

在这里插入图片描述
内存管理本质:将磁盘中的特定数据块(数据内容)存放到物理内存中的哪个页框(数据加载的空间)
将内存划分成一个个页框后,操作系统就可以
使用数组结构描述物理内存
然后使用该结构对物理内存继续管理。值得注意的是,根据局部性原理,从磁盘中加载一个数据块到内存中,实际上就是一个预加载操作,能够减少IO的次数,提高整机效率。

32位计算机物理内存大小为4GB,物理内存的基本单位是字节,因此表示物理内存的地址需要32位比特位,在使用二级页表时将这32位的地址划分为10+10+12三个部分:

在这里插入图片描述
前10位地址作用于第一级页表(页目录),页目录中会存储前10位地址所组成的所有二进制地址的映射关系,映射到对应的第二级页表(页表项)。中间的10位地址作用于第二级页表(页表项),由于每个页表项是根据对应的页目录映射找到的,因此页表项中的10位地址实际建立的是前20位地址所组成的所有二进制地址的映射关系,映射到对应的页框。最后12位地址作用于页框中,当通过二级页表找到对应页框后,根据页框首地址偏移后12位地址所表示的大小找到对应数据的首地址。使用二级页表寻找对应数据的示意图如下:
在这里插入图片描述
实际上页表还有其他保存的选项,例如
在这里插入图片描述

线程用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率。
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。

怎么管理线程

线程头文件
#include <pthread,h>
在编译的时候,我门需要在末尾加上-lpthread

线程创建

创建线程的函数叫做pthread_create

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

参数说明:

  • thread:获取创建成功线程的ID,该参数是一个输出型参数。
  • attr:用于设置创建线程的属性,传入NULL表示默认属性。
  • start_routinue:该参数是一个函数地址,表示线程例程,即线程启动后要执行的函数。
  • arg:传入线程例程的参数,也就是要执行函数的参数。

返回值:线程创建成功返回0,失败返回错误码

主线程是什么
当一个程序启动后,就有一个进程被操作系统创建,与此同时一个线程也立刻被运行,这个线程就叫主线程。

  • 主线程是产生其他子线程的线程。
  • 通常主线程必须最后完成某些操作,比如各种关闭操作。

下面我们让主线程调用pthread_create函数创建一个新线程,此后新线程就会跑去执行自己的新例程,而主线程则继续执行后续代码。

在这里新线程和主线程的运行是没有规律的,没有谁前谁后。

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

using namespace std;

void* func(void* arg)
{
    char* msg = (char*)arg;
	while (1){
		printf("I am %s\n", msg);
		sleep(1);
	}
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, func, (void*)"thread 1");

    cout << tid << endl;
    sleep(10);//这里睡眠十秒钟的作用是不让主线程过早的退出
    //给分支线程足够的运行时间
    return 0;
}

在这里插入图片描述

每一个线程都是通过主线程分支得到的,当主线程异常退出的时候,其他的分支进程也会随之退出。
我们可以通过 ps -aL命令来查看进行的多线程和主线程

  • 带-L可以查看到每个进程内的多个轻量级进程。

下面我们可以多创建几个线程,来查看一下它们之间的关系

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

using namespace std;

void* func(void* arg)
{
    char* msg = (char*)arg;
	while (1){
		printf("I am %s,pid:%d,ppid:%d\n", msg,getpid(),getppid());
		sleep(1);
	}
}

int main()
{
    pthread_t tid[4];
    for(int i = 0;i < 4;i++)
    {
        pthread_create(tid + i, NULL, func, (void*)"thread 1");
        cout << tid[i] << endl;
        sleep(1);
    }
    sleep(100);
    return 0;
}

在这里插入图片描述
我们从图中可以看出,他们的 PID 和 PPID都是一样的,这说明它们属于同一个线程,但是他们的LWP是不相等的,从图中我们可以看到有五个线程,其中,第一个线程为主线程,当主线程异常退出时,剩余的进程也全都会随之退出。

LWP表示轻量级进程的ID,代表对线程ID,我们可以看出,每个线程的ID都是不一样的,实际操作系统调用的时候都是调用的LWP,而并非PID,只不过我们之前接触到的都是单线程进程,其PID和LWP是相等的,所以对于单线程进程来说,调度时采用PID和LWP是一样的。

获取创建后的ID方法
  • 获取创建成功的ID方法有两种
    • 一种为我们调用创建函数时,有一个输出型参数,将我们创建好的线程ID给返回了。
    • 另一种方法为调用线程这块的函数来进行返回pthread_self

pthread_self函数的函数原型如下:

pthread_t pthread_self(void);

我们用以下代码来尝试一下以下案例:

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

using namespace std;

void* func(void* arg)
{
    char* msg = (char*)arg;
	while (1){
		printf("I am %s,ID:%d,pid:%d,ppid:%d\n", msg,pthread_self(),getpid(),getppid());
		sleep(1);
	}
}

int main()
{
    pthread_t tid[4];
    char str[125];
    for(int i = 0;i < 4;i++)
    {
        sprintf(str,"thread: %d",i);
        pthread_create(tid + i, NULL, func, str);
        printf("我是主线程我的线程ID为:%d\n",tid[i]);
        sleep(1);
    }
    sleep(100);
    return 0;
}

在这里插入图片描述
注意: 用pthread_self函数获得的线程ID与内核的LWP的值是不相等的,pthread_self函数获得的是用户级原生线程库的线程ID,而LWP是内核的轻量级进程ID,它们之间是一对一的关系。

线程等待

当我们进行运行时,由于主线程和新建的线程的运行轨迹不一样,他们的生命周期也是不统一的,就像进程的父进程和子进程一样,如果新建的线程没有主线程的阻塞等待,那么他就会产生内存泄漏

等待线程的函叫做 pthread_join

pthread_join函数的函数原型如下:

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

参数说明:

  • thread:被等待线程的ID
  • retval:线程退出时的退出码信息

返回值说明:

  • 线程等待成功返回0,失败返回错误码。

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

  • 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
  • 如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
  • 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
  • 如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。

不获取返回值阻塞式等待运行

void* func(void* arg)
{
    char* msg = (char*)arg;
    int cnt = 6;
	while (cnt <= 6){
		printf("I am %s,ID:%d,pid:%d,ppid:%d\n", msg,pthread_self(),getpid(),getppid());
		sleep(1);
        cnt++;
	}
}

int main()
{
    pthread_t tid[4];
    char str[125];
    for(int i = 0;i < 4;i++)
    {
        sprintf(str,"thread: %d",i);
        pthread_create(tid + i, NULL, func, str);
        printf("我是主线程我的线程ID为:%d\n",tid[i]);
        sleep(1);
    }
    for(int i = 0;i < 4;i++)
    {
        pthread_join(tid[i],NULL);
        printf("tid[%d]:%d\n",i,tid[i]);
    }
    return 0;
}

在这里插入图片描述
如果要进行接受函数退出的返回值,这里,我们要注意它的接受类型

int pthread_join(pthread_t thread, void **retval);
#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

void* func(void* arg)
{
    char* msg = (char*)arg;
    int cnt = 6;
	while (cnt <= 6){
		printf("I am %s,ID:%d,pid:%d,ppid:%d\n", msg,pthread_self(),getpid(),getppid());
		sleep(1);
        cnt++;
	}

    return (void*)6666;
}

int main()
{
    pthread_t tid[4];
    char str[125];
    for(int i = 0;i < 4;i++)
    {
        sprintf(str,"thread: %d",i);
        pthread_create(tid + i, NULL, func, str);
        printf("我是主线程我的线程ID为:%d\n",tid[i]);
        sleep(1);
    }
    for(int i = 0;i < 4;i++)
    {
        void* ret = NULL;
        pthread_join(tid[i],&ret);
        printf("tid[%d]:%d,返回值为:%d\n",i,tid[i],(int*)ret);
    }
    return 0;
}

在这里插入图片描述
当我们进行阻塞式线程等待时,当我们其中的一个线程出现异常错误时,整个进程都会异常退出,这时我们的阻塞等待函数是起不到阻塞作用的。

我们通过下边的代码来演示看一下

void* func(void* arg)
{
    char* msg = (char*)arg;
    int cnt = 6;
	while (cnt <= 6){
		printf("I am %s,ID:%d,pid:%d,ppid:%d,计算%d\n", msg,pthread_self(),getpid(),getppid(),10/0);
		sleep(1);
        cnt++;
	}

    return (void*)6666;
}

int main()
{
    pthread_t tid[4];
    char str[125];
    for(int i = 0;i < 4;i++)
    {
        sprintf(str,"thread: %d",i);
        pthread_create(tid + i, NULL, func, str);
        printf("我是主线程我的线程ID为:%d\n",tid[i]);
        
    }
    for(int i = 0;i < 4;i++)
    {
        void* ret = NULL;
        pthread_join(tid[i],&ret);
        printf("tid[%d]:%d,返回值为:%d\n",i,tid[i],ret);
    }
    return 0;
}

在这里插入图片描述
这时,整个进程都会随之退出

线程终止

线程终止的出现主要有这几种作用:

  • 资源释放:当线程不再需要某些资源时,可以在线程结束时终止线程,以释放这些资源。
  • 错误处理:线程中发生错误或异常时,可以选择终止线程以防止错误的扩散或损坏其他资源。
  • 完成任务:线程执行完任务后,可以选择终止线程以避免不必要的执行。
  • 优雅退出:在某些情况下,终止线程可能是一种优雅的退出方式。

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

  • 从线程函数return返回
  • 线程可以自己调用pthread_exit函数终止自己
  • 一个线程可以调用pthread_cancel函数终止同一进程中的另一个线程

pthread_exit函数

pthread_exit函数的功能就是终止线程,pthread_exit函数的函数原型如下:

void pthread_exit(void *retval);

参数说明:

  • retval:线程退出时的退出码信息
void* func(void* arg)
{
    char* msg = (char*)arg;
    int cnt = 6;
	while (cnt <= 6){
		printf("I am %s,ID:%d,pid:%d,ppid:%d\n", msg,pthread_self(),getpid(),getppid());
		sleep(1);
        cnt++;
	}

    pthread_exit((void*)4444);

    return (void*)6666;
}

int main()
{
    pthread_t tid[4];
    char str[125];
    for(int i = 0;i < 4;i++)
    {
        sprintf(str,"thread: %d",i);
        pthread_create(tid + i, NULL, func, str);
        printf("我是主线程我的线程ID为:%d\n",tid[i]);
        sleep(1);
        
    }
    for(int i = 0;i < 4;i++)
    {
        void* ret = NULL;
        pthread_join(tid[i],&ret);
        printf("tid[%d]:%d,返回值为:%d\n",i,tid[i],ret);
        sleep(1);
    }
    int c = 5;
    while(c--)
    {
        printf("我是主线程,我还在运行\n");
    }
    return 0;
}

在这里插入图片描述

pthread_cancel函数

线程是可以被取消的,我们可以使用pthread_cancel函数取消一个线程,也可以通过次函数取消主线程,或者取消本线程

pthread_cancel函数原型如下:

int pthread_cancel(pthread_t thread);

参数说明:

  • thread:被取消线程的ID

返回值说明

  • 线程取消成功返回0,失败返回错误码
void *threadRun(void *args)
{

    const char *name = static_cast<const char *>(args);
    int cnt = 5;
    while (cnt)
    {
        cout << name << "   is running  " << cnt-- << "  obtain self id:" << pthread_self() << endl;
        sleep(1);
    }
    pthread_cancel(pthread_self());
    sleep(10);
    pthread_exit((void*)111);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void *)"thread -1");

    void* ret = nullptr;
    pthread_join(tid,&ret);
    cout << " new thread exit : " << (int64_t)ret << "   quit thread: " << tid << endl;
    return 0;
}

在这里插入图片描述
在终止程序之前,将线程退出。(取消线程的方式也可以放到主线程中)

分离线程
  • 默认情况下,我们一般都是阻塞式等待线程退出,然后主线程回收他,如果不使用pthread_join函数,就会造成无法释放资源,造成内存泄漏。
  • 但是如果我们不担心返回值,我们可以直接将线程进行分离,后序当线程进行退出的时候,就会自动退出并且释放。
  • 线程分离后,不代表的他和进程没有关系了,它们同样都是复用关系,它的异常出错终止也是影响整个进程的,只不过他只是不需要手动pthread_join了
  • joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

一个线程如果被分离,就无法在被join,如果ioin就会函数报错
分离线程的函数叫pthread_detach

pthread_detach函数的函数原型如下:

int pthread_detach(pthread_t thread);

参数说明:

  • thread:被分离线程的ID。
    返回值说明:
  • 线程分离成功返回0,失败返回错误码。
void *threadRun(void *args)
{
    pthread_detach(pthread_self());
    const char *name = static_cast<const char *>(args);
    int cnt = 6;
    while (cnt)
    {
        cout << name << "   is running  " << cnt-- << "  obtain self id:" << pthread_self() << endl;
        sleep(1);
    }

    pthread_exit((void *)111);
}

int main()
{
    pthread_t tid[4];
    char str[128];
    for (int i = 0; i < 4; i++)
    {
        sprintf(str,"thread:%d",i);
        pthread_create(tid + i, nullptr, threadRun, str);
        sleep(1);
    }
    sleep(10);
    printf("全部回收\n");
    return 0;
}

在这里插入图片描述

线程ID及进程地址空间布局
  • pthread_create函数产生的线程ID,存放在第一个参数指向的地址中,该线程的ID和内核中的LWP不是一回事。
  • pthread_create函数第一个参数指向一个虚拟内存单元,线程库的后续操作就是根据该线程ID来操作线程的。

pthread_t到底是什么类型呢?

首先,Linux不提供真正的线程,只提供LWP,也就意味的操作系统只需要对内核执行流LWP进行管理,而供用户使用的线程接口等其他数据,应该由线程库自己来管理,因此管理线程时的“先描述,再组织”就应该在线程库里进行。

通过ldd命令可以看到,我们采用的线程库实际上是一个动态库。

在这里插入图片描述

进程运行时动态库被加载到内存,然后通过页表映射到进程地址空间中的共享区,此时该进程内的所有线程都是能看到这个动态库的。
在这里插入图片描述

我们说每个线程都有自己私有的栈,其中主线程采用的栈是进程地址空间中原生的栈,而其余线程采用的栈就是在共享区中开辟的。除此之外,每个线程都有自己的struct pthread,当中包含了对应线程的各种属性;每个线程还有自己的线程局部存储,当中包含了对应线程被切换时的上下文数据。
每一个新线程在共享区都有这样一块区域对其进行描述,因此我们要找到一个用户级线程只需要找到该线程内存块的起始地址,然后就可以获取到该线程的各种信息。

在这里插入图片描述
上面我们所用的各种线程函数,本质都是在库内部对线程属性进行的各种操作,最后将要执行的代码交给对应的内核级LWP去执行就行了,也就是说线程数据的管理本质是在共享区的。

pthread_t到底是什么类型取决于实现,但是对于Linux目前实现的NPTL线程库来说,线程ID本质就是进程地址空间共享区上的一个虚拟地址,同一个进程中所有的虚拟地址都是不同的,因此可以用它来唯一区分每一个线程。

例如,我们也可以尝试按地址的形式对获取到的线程ID进行打印。

void* func(void* args)
{
    while (1)
    {
        printf("new thread tid:%p\n",pthread_self());
        sleep(1);
    }
    
    
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,func,NULL);
    while(1)
    {
        printf("main thread tid:%p\n",pthread_self());
        sleep(1);
    }

    void* ret;
    pthread_join(tid,&ret);
    return 0;
}

在这里插入图片描述
证明一人一块空间,都有自己独立的线程栈和线程局部存储部分。

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

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

相关文章

[架构之路-223]:数据管理能力成熟度评估模型DCMM简介

目录 一、背景 二、评估依据 三、评估内容 四、主要适用对象 五、能力等级 六、不同层次的文件&#xff1a; 一、背景 信息技术与经济社会的交汇融合引发了数据爆发式增长。数据蕴含着重要的价值&#xff0c;已成为国家基础性战略资源&#xff0c;正日益对全球生产、流通…

会声会影和pr哪个好用? 2023年最新功能介绍解析

随着抖音、快手、B站等视频平台的普及&#xff0c;每个人都能成为视频创作者&#xff0c;视频剪辑软件成为自媒体创作的必备工具。一些新入门视频剪辑的小伙伴可能会疑惑&#xff0c;会声会影和PR软件哪个好呢&#xff1f;今天我将从核心功能、稳定性和性价比三个方面&#xff…

从中序遍历和后序遍历构建二叉树

题目描述 106. 从中序与后序遍历序列构造二叉树 中等 1.1K 相关企业 给定两个整数数组 inorder 和 postorder &#xff0c;其中 inorder 是二叉树的中序遍历&#xff0c; postorder 是同一棵树的后序遍历&#xff0c;请你构造并返回这颗 二叉树 。 示例 1: 输入&#xff1…

腾讯Mini项目课程前置学习笔记(第一轮)

Mini项目课程前置学习笔记&#xff08;第一轮&#xff09; 时间&#xff1a;5.20 ~ 5.23 项目基本介绍 项目 B. 指标监控服务重构 指标的收集与展示是后台系统监控中非常重要的一环&#xff0c;很可惜大而全的全链路监控方案并不适合我们&#xff0c;于是就有了本 mini 项目。…

cesium在vue中引入报错

npm install cesium1.95.0 找了很久各种办法都用了 最后就是降到这个版本。 在main.js中这样引入就可以运行起来了 import * as Cesium from cesium/Build/Cesium/Cesium.js; import cesium/Build/Cesium/Widgets/widgets.css; Vue.prototype.cesiumCesium 没出来是因为这个…

根据表名称快速查询表所有字段是否包含特定数据筛选

当前需要清理某个表中所有字段&#xff0c;检查是否有包含DEl字符的脏数据&#xff0c;如果字段比较少的直接根据字段查询即可&#xff0c;但是如果有几十个字段的话&#xff0c;逐个检查会很慢。 当前使用的方式是&#xff1a; 1&#xff1a;根据表名称获取当前表所有的字段 …

《信息系统项目管理师教程(第4版)》第1章至第5章 信息化发展、信息技术发展、信息系统治理、信息系统管理、信息系统工程 常见考点、知识点、思维导图、xmind

第一章至第五章多以选择题形式考察&#xff0c;分值在20分左右。已将考点、知识点整理成思维导图&#xff0c;可免费下载。以下是思维导图的部分截图&#xff1a; 第一章 信息化发展 第二章 信息技术发展 第三章 信息系统治理 第四章 信息系统管理 第五章 信息系统工程

代码随想录算法训练营第四十二天 | 动态规划 part 4 | 01背包问题(二维、一维滚动数组)、416. 分割等和子集

目录 01背包问题 二维代码 01背包问题&#xff08;一维滚动数组&#xff09;代码 416. 分割等和子集思路代码 01背包问题 二维 背包问题汇总&#xff1a; 二维数组dp——01背包五部曲 dp[i][j]表示从下标为[0-i]的物品里面任意取&#xff0c;放进容量为j的背包&#xff0c;价值…

Redis入门 (店铺营业状态设置) --苍穹外卖day4

目录 redis简介 redis下载与安装 redis服务启动与停止​编辑 redis数据类型 五种常用数据类型 各个类型特点 redis常用命令 字符串 哈希 列表 集合 有序集合 通用指令 ​在Java中操作Redis 导入坐标 编写配置类​ 通过RedisTem~对象操作 字符串 ​哈希 列…

什么是CDN

CDN是什么&#xff1f; 假设有一个IP为100.1.1.1的服务器网站&#xff0c;在没有CDN的情况下&#xff0c;PC先访问该服务器的DNS服务器&#xff0c;经过域名解析后再去访问服务器&#xff0c;如下图&#xff1a; 如果部署了CDN&#xff0c;这时候就有一个能识别CDN节点的DNS服…

经过打包后运行app.exe文件之后问题解决

项目文件经过python代码如何打包方法打包之后文件目录是下面这样的。 按照下面的路径运行app.exe文件报错&#xff1a; RuntimeError: Unable to open E:\face_detection\dist\app\face_recognition_models\models\shape_predictor_68_face_landmarks.dat [35816] Failed to…

AI在线工具分享

1、ChatGPT ChatGPT是一种由OpenAI训练的大型语言模型。它的原理是基于Transformer架构&#xff0c;通过预训练大量文本数据来学习如何生成人类可读的文本&#xff0c;然后通过接受输入并生成输出来实现对话。 ChatGPT的用途非常广泛&#xff0c;可以用于自然语言处理&#xf…

Seata入门系列【1】安装seata 1.7.1+nacos 2.1.1

1 介绍 Seata 是一款开源的分布式事务解决方案&#xff0c;致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式&#xff0c;为用户打造一站式的分布式解决方案。 Github: https://github.com/seata/seata 官方文档&#xff1a;h…

短期光伏发电量短期预测(Python代码,先对异常值处理,再基于XGBoost模型预测)

一.代码流程&#xff08;运行效果&#xff1a;短期光伏发电量短期预测&#xff08;Python代码&#xff0c;先对异常值处理&#xff0c;再基于XGBoost模型预测&#xff09;_哔哩哔哩_bilibili 模型流程&#xff1a; 导入所需的库&#xff0c;包括NumPy、Pandas、Matplotlib、Sea…

TS编译选项——TS文件编译后消除注释

在tsconfig.json文件中配置removeComments属性 {"compilerOptions": {// outDir 用于指定编译后文件所在目录"outDir": "./dist", // 将编译后文件放在dis目录下// 是否文件编译后移除注释"removeComments": true} } 左边是编写的t…

通过解读yolov5_gpu_optimization学习如何使用onnx_surgon

onnx实战一: 解析yolov5 gpu的onnx优化案例: 这是一个英伟达的仓库, 这个仓库的做法就是通过用gs对onnx进行修改减少算子然后最后使用TensorRT插件实现算子&#xff0c; 左边是优化过的, 右边是原版的。 通过这个案例理解原版的onnx的导出流程然后我们看英伟达是怎么拿gs来优化…

伦敦银如何选择最优的交易方法

经常有投资者会问&#xff0c;伦敦银投资中如何选择最好的方法呢&#xff1f;我们进行伦敦银投资&#xff0c;目的就是找到一个能够盈利的交易方法&#xff0c;它能够使我们大部分交易都是盈利&#xff0c;少部分交易亏损&#xff0c;但是可以将亏损控制在一定的范围之内&#…

Windows10关闭此电脑“桌面”“图片”“视频”“3D对象”“文档”等显示,只显示“设备与驱动器”

如何关闭下图"文件夹"等7个子文件夹&#xff0c;只显示“设备和驱动器”&#xff1f; 关闭步骤&#xff1a; 打开cmd,输入regedit打开注册表编辑器打开注册表编辑器后&#xff0c;定位到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\My…

会声会影和剪映哪个好,2023年全新功能对比详细解析

随着网络视频的蓬勃发展&#xff0c;越来越多的人开始涉足视频剪辑领域&#xff0c;毕竟技多不压身嘛。在众多剪辑软件中&#xff0c;剪映和会声会影是备受新手青睐的两种。那么&#xff0c;会声会影和剪映哪个好呢&#xff1f;在它们之间&#xff0c;哪一个更适合初学者呢接&a…

软件测试之Web安全测试详解

前言 随着互联网时代的蓬勃发展&#xff0c;基于Web环境下的应用系统、应用软件也得到了越来越广泛的使用。 目前&#xff0c;很多企业的业务发展都依赖于互联网&#xff0c;比如&#xff0c;网上银行、网络购物、网络游戏等。但&#xff0c;由于很多恶意攻击者想通过截获他人…