Linux线程(1)--线程的概念 | 线程控制

目录

前置知识

线程的概念

Linux中对线程的理解

 重新定义进程与线程

重谈地址空间

线程的优缺点

线程的优点

线程的缺点

线程异常

线程的用途

Linux线程 VS 进程

线程控制

创建线程

线程等待

线程终止

线程ID的深入理解


前置知识

我们知道一个进程有属于自己的PCB(task_struct),地址空间,页表;OS会为进程在物理内存中申请资源(空间),通过页表与地址空间产生映射关系。在执行进程时,会通过地址空间使用OS为进程申请的资源。

因此我们可以说:

地址空间时进程的资源窗口

在创建子进程时,也会为子进程创建属于它的PCB,地址空间,页表,与物理内存的资源。子进程会将父进程的部分属性拷贝下来。


线程的概念

在一些教材中对于线程是这样定义的:

线程:是进程内的一个执行分支。线程的执行粒度,要比进程要细。

 这里对线程的定义是通过线程的特点定义的,并不能很好的解释什么是线程。

这里可以这样解释:

Linux中对线程的理解


我们知道在之前我们创建的进程都是一个PCB(task_struct)的,这里我们再创建多个特殊的 "进程" :

这个 "进程" 有属于自己的PCB,但是与它的 "父进程",共享同一个地址空间,同一个页表,同一块物理内存。(即:将只属于一个PCB的栈,堆等资源,划分一部分给另一个新创建的PCB).  

由于父进程与新创建的“子进程”,他们共享一个地址空间,可能会导致他们可以使用同一个资源。

这里我们可以说这些"进程"的执行粒度要比,原来进程(一个task_struct)的执行粒度要小。

原因:原来进程由自己一个就可以执行完全代码,使用空间。而现在却需要多个"进程"去执行代码,使用空间。这些"进程"是原来进程的一个执行分支(执行流)。因此我们说现在这些 "进程"的执行粒度小于原来进程的。

这里为了和原来的进程进行区分,我们把新创建的"进程"叫做:线程。


Linux中实现线程的方案:

1.在Linux中,线程在进程"内部"执行,即:线程在进程的地址空间内运行。

  • 任何执行流要执行,都要有资源,在上面我们说过,地址空间是进程的资源窗口。这里线程采用了共用同一个地址空间(将地址空间分成若干份,分配给线程)。所以说新城在地址空间中运行。

2.在Linux中,线程的执行粒度要比进程要细。

  • 线程执行了进程的一部分代码,共享同一份资源。

注:不同的操作系统对线程的实现方案可能是不一样的,但实现原理都是一样的。


同时我们知道操作系统用PCB对进程描述与管理,会通过进程的PCB进行调度,但是对于多线程来说,一个进程中会有多个PCB,那么该如何调度的呢?

这里我们要知道在OS中是通过CPU进行调度的,对于CPU来说,它并没有进程与线程的概念,它只有调度执行流的概念,即:task_struct(只要有代码与数据让CPU去执行就可以了)。

 重新定义进程与线程

什么叫做线程?

  • 我们认为线程是操作系统调度的基本单位。

什么是进程?

  • 在内核角度:进程是操作系统分配资源的基本实体,执行流也是资源,所以线程是进程内部的执行流资源!

        对于以前的进程可以这样理解:操作系统是以进程为基本单位进行资源分配,在进程内部就有一个执行流。


Linux中对线程的描述与管理

 这里我们知道,在操作系统里一个进程可能有1个或多个线程,所以进程和线程的关系是1:N的。在操作系统用PCB(task_struct)来描述和管理进程,所以线程也可以采用同样的方式来进行管理,在有些操作系统中(Windows)用struct tcb(thread control block)来进行描述与管理。但是我们知道对于进程的管理就已经很复杂了,如果再进一步细分struct tcb会更加的复杂。所以在Linux中,并没有使用struct tcb来对线程进行描述与管理,由于线程是进程的一个执行流,在大体上并没有太大的区别,于是采用了复用进程的PCB(task_struct)来对线程进行描述与管理。

因此可以在进程中只有一个PCB时,把它当作进程的,多个PCB时,当作线程的。甚至可以不区分这些,直接把他们当作执行流去看待。

由于Linux是采取复用进程的结构体去管理线程的,所以Linux没有真正意义上的线程,而是用"进程的内核数据结构 "模拟线程的。

这里以CPU的角度去看:

线程<=执行流<=进程

所以在Linux中执行流,也叫做轻量级进程。


重谈地址空间

这里我们知道进程是OS分配资源的基本单位,线程是调度的基本单位。在进程里多线程会共享同一块地址空间,它们会把 地址空间里的资源划分给每一个线程,那么是如何划分的呢?这里我们就不得不再谈地址空间了。

 

虚拟地址是如何转换到物理地址???(以32位虚拟地址为例)

在Linux中,将32位的虚拟地址,分为了三部分:32=10+10+12。同时页表并不是直接记录在一张表里的。因为:地址空间一共有4GB个,一个按照10字节算,页表最大为40GB,明显太大,不能存储下来,更别说其他的了。页表是这样的:分为两级:第一级页表有1024个,存放第二级页表地址;第二级页表:有1024个,存放了物理内存中页框的起始地址。

在Linux中这样转换的:

这里我们知道一个变量的地址是变量的起始地址,那么从虚拟地址转化成物理地址,我们只能找到一个物理地址,但是如果是int,那么它是用4个字节存储的,那么他是如何读取的呢?

这里我们知道每一个变量都有一个类型,在读取数据是,就识别出了它的类型,在它找到物理地址时,会加上类型大小对应的偏移量进行读取。


在了解地址空间后,我们知道线程的资源分配,全部都是由地址空间来的,而所有的代码于数据都是,地址空间通过页表的映射在物理内存中找到的。

所以线程分配资源的本质就是分配地址空间。

线程的优缺点

线程比进程要更轻量化:

创建和释放更加轻量化

  • 创建线程只需要创建一个PCB,而进程不但要创建PCB,还要创建地址空间,页表,将地址空间通过页表于物理地址进行映射等。

切换更加轻量化

  •  在线程切换时,只需要改变CPU寄存器中对应线程的的上下文内容于数据,但是效率主要来自于一个存储常用数据cache,切换线程时并不会改变该cache,而进程会将其清空,从头慢慢开始。

我们知道进程有时间片,线程也有时间片。

在OS中并不会为创建的新线程重新赋予时间片,而是瓜分同一进程的时间片(时间片也是资源,会分配给每个线程)。

那OS是如何区分是进程切换还是线程进行切换的呢?

task_struct是可以标识,这里在进程中的线程是有主次之分的,在进程刚开始的时候的task_struct时主线程,其他创建的是副线程。在主线程的task_struct里记录着进程的时间片于给线程的时间片,在其他副线程中记录着线程的时间片,当线程的时间片结束,主线程中记录进程的时间片会减去线程的时间,这是如果为0,则进行进程的切换,否则进行线程的切换

线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现(不是线程越多越合适,一般有多少个CPU就创建多少个线程)
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

线程的缺点

性能损失

  • 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型 线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
  • 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了 不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
  • 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
  • 编写与调试一个多线程程序比单线程程序困难得多

线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该 进程内的所有线程也就随即退出

线程的用途

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

Linux线程 VS 进程

进程和线程
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
  • 线程ID
  • 一组寄存器 (线程的上下文)
  • errno
  • 信号屏蔽字
  • 调度优先级
进程的多个线程共享 同一地址空间,因此Text SegmentData Segment都是共享的,如果定义一个函数,在各线程 中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
  • 文件描述符表 (即:一个线程打开一个文件,所有线程都打开了)
  • 每种信号的处理方式(SIG_ IGNSIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

线程控制

创建线程

在Linux中复用了进程的PCB来对线程进行描述于管理,因此在Linux中没有明确的线程的概念,只有轻量级进程的概念。因此在Linux中不会直接提供线程的系统调用,只会给我们提供轻量级的系统调用!

但由于我们用户需要线程的接口去创建线程。

因此一些大佬们专门创建了一个应用层的pthread线程库(对轻量级进程接口进行封装),为用户提供直接创建线程的接口。(pthread线程库是一个第三方库,几乎所以的Linux平台,都默认自带这个库)

  • 功能:创建一个新的线程
参数
  • thread:输出型参数,创建线程成功返回线程ID。

  • attr:设置线程的属性,attr为nullptr表示使用默认属性
  • start_routine:是个函数指针(返回值为void*  参数也为void*),线程启动后要执行的函数里面的内容
  • arg:传给线程启动函数的参数,为start_routine指向函数的参数。不需要参数为nullptr

返回值:成功返回0;失败返回错误码

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

using namespace std;

void* thread(void* args)
{
    while(true)
    {
        cout<<"new thread:"<<endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,thread,nullptr);
    while(true)
    {
        cout<<"main thread"<<endl;
        sleep(1);
    }
    return 0;
}

编译:

g++ -o thread thread.cc -std=c++11

这里我们发现,在编译时报错。

这里是因为pthread_create是第三方库的接口,不是系统调用。g++在编译时,只会链接自己的C/C++库,这里因为Linux已经将pthread库加载到了指定路径下,因此我们只需要告诉g++链接那个库就可以了。

我们要这样编译:

g++ -o thread thread.cc -l pthread -std=c++11

这里我们还可以通过指令查看执行流:
 

ps -aL

  • LWP(Light Weight Process)就是轻量级进程的ID(用来标识轻量级进程),可以看到两个轻量级进程的PID相等,所以他们属于同一个进程。这里有一个线程的LWP和PID相等,这里我们把这个线程叫做主线程,另一个就是新创建的线程。
  • 之前说的OS调度一个进程,可以认为单进程单执行流,它调度的基本单位看的是pid也可以是LWP,因为它两相等。现在我们认为线程调度时OS看的是LWP。
  • LWP和PID的关系?   PID是对进程标识,相同进程具有相同的PID。LWP标识轻量级进程,它们的标识不同。OS真正调用时使用的时LWP

注:在同一进程里所有线程的PID都相等。

验证线程共享全局变量:

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

using namespace std;



int val=100;

void* thread(void* args)
{
    while(true)
    {
         printf("new thread:  getpid:%d  val=%d    &val=%p\n",getpid(),val,&val);
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,thread,nullptr);
    while(true)
    {
        printf("main thread:  getpid:%d  val=%d    &val=%p\n",getpid(),val,&val);
        val++;
        sleep(1);
    }
    return 0;
}

 验证一个线程出异常,整个进程崩溃:

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

using namespace std;



int val=100;

void* thread(void* args)
{
    while(true)
    {
        
        sleep(5);
        int a=1;
        a/=0;
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,thread,nullptr);
    while(true)
    {
        printf("main thread:  getpid:%d  val=%d    &val=%p\n",getpid(),val,&val);
        val++;
        sleep(1);
    }
    return 0;
}


获取线程ID

常见的获取线程ID的方式有两种:

  • 通过创建线程pthread_create函数的第一个输出型参数获得。
  • 通过调用pthread_self函数获得。

这里pthread_self函数那个线程调用,就获取那个线程的ID,类似于调用getpid()函数。

代码:主线程调用pthread_create函数,通过输出型参数获取并打印;在创建的新线程中调用pthread_self函数获得线程ID。

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

using namespace std;

void* thread(void* args)
{
    while(true)
    {
        printf("new thread:           new thread tid=%p\n",pthread_self());//将线程ID以16进制打印出来
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,thread,nullptr);
    while(true)
    {
        printf("main thread:   create new thread tid=%p\n",tid);//将线程ID以16进制打印出来
        sleep(1);
    }
    return 0;
}

用这里我们用pthread_self函数获取的线程ID于内核的LWP的值不同,pthread_self函数获得的是用户级原生线程库的线程ID,而LWP是内核的轻量级进程ID,他们之间是1:1。

线程等待

  • 这里创建完线程,我们并不知道是那个线程先进行。但是我们可以确定的是主线程最后退出,因为新线程是在主线程中创建的,主线程要对创建的新线程进行管理,所以最后退出。

  • 因此,一个线程被创建出来,这个线程如进程一般,需要主线程进行等待,如果主线先退出会出现类似于进程中的僵尸进程的情况,造成内存泄漏。(这里线程退出,类似于进程的退出,它的空间并没有被完全的释放。)

功能:

  • 指定主线程等待那个线程

参数:

  • thread:等待线程ID(用户级别的)
  • retval:接收线程退出时的返回值,若不使用可以为nullptr。

返回值:

  • 成功,返回0;失败返回错误码。
#include<iostream>
#include<unistd.h>
#include<pthread.h>

using namespace std;

void* thread(void* args)
{
    int cnt=5;
    while(cnt--)
    {
        cout<<"new thread: "<<cnt<<endl;
        sleep(1);
    }
    return (void*)101;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,thread,nullptr);
    void* retval;
    pthread_join(tid,&retval);主线程等待新线程退出;通过retval获取线程的返回值
    cout<<"main thread quit... retval:"<<(long long int)retval<<endl;
    return 0;
}

注:主线程等待的时候,默认是阻塞等待。

线程终止

1.return返回

在线程中使用return代表当前进程退出,但是在main函数中使用,代表整个进程退出,也就是说只要主线程退出,那么整个资源就会被释放,而其他线程会因为没有资源,自然而然的也退出了。

2.pthread_exit函数

功能:终止调用该函数的线程。

参数:retval:线程退出时的退出码信息。

说明:

  • 该函数无返回值,跟进程一样,线程结束时无法返回它的调用者(自身)。
  • pthread_exit或者return返回指针所指向的内存单元必须在全部变量或是用malloc分配的,不能在线程函数的只能上分配,因为当其他线程得到这个返回指针时,指针指向的空间已经被释放了。
  • exit函数的作用时终止整个进程,任何一个线程调用exit函数就代表着整个进程终止。
#include<iostream>
#include<unistd.h>
#include<pthread.h>

using namespace std;

void* thread(void* args)
{
    int cnt=5;
    while(cnt--)
    {
        cout<<"new thread: "<<cnt<<endl;
        pthread_exit((void*)101);//终止线程。这里的参数可以理解为线程的退出码,可以被pthread_join的第二个参数接收
        sleep(1);
    }
    return (void*)101;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,thread,nullptr);
    void* retval;
    pthread_join(tid,&retval);主线程等待新线程退出;通过retval获取线程的返回值
    cout<<"main thread quit... retval:"<<(long long int)retval<<endl;
    return 0;
}

3.pthread_cancel

功能:取消指定线程,类似于kill。被取消的线程的错误码(返回值)为-1。

参数:取消线程的ID。

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

using namespace std;

void* thread(void* args)
{
    int cnt=5;
    while(cnt--)
    {
        cout<<"new thread: "<<cnt<<endl;
       
        sleep(1);
    }
    return (void*)101;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,thread,nullptr);
    sleep(1);//保证线程已创建成功

    pthread_cancel(tid);//取消线程

    void* retval;
    pthread_join(tid,&retval);主线程等待新线程退出;通过retval获取线程的返回值
    cout<<"main thread quit... retval:"<<(long long int)retval<<endl;
    return 0;
}

线程ID的深入理解

  • pthread_create函数会产生一个线程,这里和进程一样,也需要一个属性来标识线程,这里我们叫做线程的ID,存放在第一个参数指向的地址中,这里线程的ID于内核中的LWP不是一回事的。
  • 内核中LWP属于进程调度的范畴,因为线程是轻量级进程,是操作系统调度器的最小单位。
  • Linux不提供真正的线程,只提供轻量级进程,也就意味着操作系统只需要对内核执行流LWP进行管理,而供用户使用的线程接口等其他数据,应该由线程库自己管理。因此管理线程时的“先描述,再组织”就应该在线程库里进行。
  • 这里线程库是一个动态库,加载到地址空间的共享区中
  • 每个线程都有自己私有的栈,其中主线程采用的栈是进程地址空间中原生的栈,而其余线程采用的栈就是在共享区中开辟的。除此之外,每个线程都有自己的struct pthread,当中包含了对应线程的各种属性;每个线程还有自己的线程局部存储,当中包含了对应线程被切换时的上下文数据。
  • 每一个新线程在共享区都有这样一块区域对其进行描述,因此我们要找到一个用户级线程只需要找到该线程内存块的起始地址,然后就可以获取到该线程的各种信息。

  • 上面我们所用的各种线程函数,本质都是在库内部对线程属性进行的各种操作,最后将要执行的代码交给对应的内核级LWP去执行就行了,也就是说线程数据的管理本质是在共享区的。
  • pthread_t到底是什么类型取决于实现,但是对于Linux目前实现的NPTL线程库来说,线程ID本质就是进程地址空间共享区上的一个虚拟地址,同一个进程中所有的虚拟地址都是不同的,因此可以用它来唯一区分每一个线程。
  • 所谓线程ID可以理解为每个新线程在库当中的内存位置的起始地址,线程控制块的起始地址。
#include<iostream>
#include<unistd.h>
#include<pthread.h>

using namespace std;

void* thread(void* args)
{
    int cnt=5;
    while(cnt--)
    {
        printf("new thread  tid:%p\n",pthread_self());
        sleep(1);
    }
    return (void*)101;
}

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python学习24

前言&#xff1a;相信看到这篇文章的小伙伴都或多或少有一些编程基础&#xff0c;懂得一些linux的基本命令了吧&#xff0c;本篇文章将带领大家服务器如何部署一个使用django框架开发的一个网站进行云服务器端的部署。 文章使用到的的工具 Python&#xff1a;一种编程语言&…

macOS开启HiDPI外接2K显示器(解决字体发虚问题)

1.前言&#xff1a; 购置了一台2K显示器&#xff0c;但通过HDMI直接连接时的显示效果让人难以接受&#xff0c;因此我们需要启用苹果系统的HiDPI模式&#xff0c;以实现更完美的显示效果。 那么&#xff0c;为什么要启用HiDPI模式呢&#xff1f;2K显示器的分辨率为2560*1440&…

数学建模【线性规划】

一、线性规划简介 线性规划通俗讲就是“有限的资源中获取最大的收益”&#xff08;优化类问题&#xff09;。而且所有的变量关系式都是线性的&#xff0c;不存在x、指数函数、对数函数、反比例函数、三角函数等。此模型要优化的就是在一组线性约束条件下&#xff0c;求线性目标…

7.1 Qt 中输入行与按钮

目录 前言&#xff1a; 技能&#xff1a; 内容&#xff1a; 参考&#xff1a; 前言&#xff1a; line edit 与pushbotton的一点联动 当输入行有内容时&#xff0c;按钮才能使用&#xff0c;并能读出输入行的内容 技能&#xff1a; pushButton->setEnabled(false) 按钮不…

17.3.2.9 像素处理与内存处理之比较

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 通过第17.3.2.1节到第17.3.2.8节&#xff0c;相信读者对通过锁定内存来处理图像有了一定认识。与第17.3.1节相比较&#xff0c;可以…

【递归】【后续遍历】【迭代】【队列】Leetcode 101 对称二叉树

【递归】【后续遍历】Leetcode 101 对称二叉树 解法一&#xff1a; 递归&#xff1a;后序遍历 左右中解法二&#xff1a; 迭代法&#xff0c;用了单端队列 ---------------&#x1f388;&#x1f388;对称二叉树 题目链接&#x1f388;&#x1f388;------------------- 解法一…

项目开发日志(登录界面):2. LoginTitle组件

LoginTitle组件 样式 说明 属性 属性名含义类型是否必填默认值welcomeTitle欢迎标语String是无mainTitle标题String是无 样式 mainColor -> 主题颜色 代码 <template><div class"logintitle-container"><p class"subtitle">{{ welc…

模拟算法.

1.什么是模拟 在信息奥赛中,有一类问题是模拟一个游戏的对弈过程或者模拟一项任务的操作过程.比如乒乓球在比赛中模拟统计记分最终判断输赢的过程等等,这些问题通常很难通过建立数学模型用特定的算法来解决因为它没有一种固定的解法,需要深刻理解出题者对过程的解释一般只能采…

备战蓝桥杯---图论之建图基础

话不多说&#xff0c;直接看题&#xff1a; 首先&#xff0c;这个不是按照字典序的顺序&#xff0c;而是以只要1先做&#xff0c;在满足后让2先做。。。。 就是让数字小的放前面做拓扑排序。 我们可以先做1&#xff0c;看看它的前驱。 举个例子&#xff1a; 我们肯定要把1放…

⭐北邮复试刷题429. N 叉树的层序遍历(按层入队出队BFS)

429. N 叉树的层序遍历 给定一个 N 叉树&#xff0c;返回其节点值的层序遍历。&#xff08;即从左到右&#xff0c;逐层遍历&#xff09;。 树的序列化输入是用层序遍历&#xff0c;每组子节点都由 null 值分隔&#xff08;参见示例&#xff09;。 示例 1&#xff1a;输入&a…

VMware Workstation创建虚拟机

一、VMware Workstation下载安装 1、安装教程 VMware Workstation下载安装&#xff08;含密钥&#xff09; 二、VMware Workstation 创建虚拟机 1、新建虚拟机&#xff0c;点击“创建新的虚拟机” 2、选择自定义&#xff08;高级&#xff09;&#xff0c;点击“下一步” 3…

docker (六)-进阶篇-数据持久化最佳实践MySQL部署

容器的数据挂载通常指的是将宿主机&#xff08;虚拟机或物理机&#xff09;上的目录或文件挂载到容器内部 MySQL单节点安装 详情参考docker官网文档 1 创建对应的数据目录、日志目录、配置文件目录(参考二进制安装&#xff0c;需自己建立数据存储目录) mkdir -p /data/mysq…

Ps:污点修复画笔工具

污点修复画笔工具 Spot Healing Brush Tool专门用于快速清除图像中的小瑕疵、污点、尘埃或其他不想要的小元素。 它通过分析被修复区域周围的内容&#xff0c;无需手动取样&#xff0c;自动选择最佳的修复区域来覆盖和融合这些不完美之处&#xff0c;从而实现无痕修复的效果。 …

使用PaddleNLP UIE模型提取上市公司PDF公告关键信息

项目地址&#xff1a;使用PaddleNLP UIE模型抽取PDF版上市公司公告 - 飞桨AI Studio星河社区 (baidu.com) 背景介绍 本项目将演示如何通过PDFPlumber库和PaddleNLP UIE模型&#xff0c;抽取公告中的相关信息。本次任务的PDF内容是破产清算的相关公告&#xff0c;目标是获取受理…

解锁Spring Boot中的设计模式—02.解释器模式:探索【解释器模式】的奥秘与应用实践!

解释器模式 1.简介 解释器模式&#xff08;Interpreter Pattern&#xff09;是一种行为设计模式&#xff0c;它用于定义语言的文法&#xff0c;并且解释语言中的表达式。在Java中&#xff0c;解释器模式可以用于构建解释器以解析特定的语言或表达式&#xff0c;如数学表达式、…

OpenAI发布Sora模型,可根据文字生成逼真AI视频

早在2022年11月30日&#xff0c;OpenAI第一次发布人工智能聊天机器人ChatGPT&#xff0c;随后在全世界掀起了人工智能狂潮&#xff0c;颠覆了一个又一个行业。在过去的一年多的时间里&#xff0c;chatGPT的强大功能改变了越来越多人的工作和生活方式&#xff0c;成为了世界上用…

Open CASCADE学习|Geom_BSplineSurface转TopoDS_Face

B样条曲面&#xff08;B-Spline Surface&#xff09;是一种数学上用于描述三维形状的工具&#xff0c;它是B样条曲线在二维空间上的扩展。B样条曲面在计算机图形学、计算机辅助设计&#xff08;CAD&#xff09;、动画和许多其他领域都有广泛的应用。 B样条曲面由一组控制点和一…

使用八叉树模拟水和烟雾 Simulating Water and Smoke with an Octree Data Structure 论文阅读笔记

原文&#xff1a; Losasso, Frank, Frdric Gibou, and Ron Fedkiw. “Simulating water and smoke with an octree data structure.” Acm siggraph 2004 papers. 2004. 457-462. 引言 这篇文章扩展了 [Popinet 2003] 的工作&#xff0c;拓展到表面自由流&#xff0c;并且使…

数据输入时,数据的类型不匹配

一、问题背景 数据输入时&#xff0c;数据类型不匹配。此时输入失败&#xff0c;变量的值还是原来的值。 说明&#xff1a; 变量如果不做初始化&#xff0c;它的值是不确定的。 良好的编程习惯&#xff1a;变量在定义时&#xff0c;进行初始化&#xff0c;eg&#xff1…

- 项目落地 - 《选择项目工具的方法论》

本文属于专栏《构建工业级QPS百万级服务》 提纲&#xff1a; 选择大概率能完成业务目标的工具选择最适合的工具制作最适合的工具 本文所说的项目工具&#xff0c;泛指业务软件开发&#xff0c;所依赖的第三方提供的成熟的资源。包括但不限于开发语言、编辑工具、编译工具、三方…