【Linux】线程的概念

文章目录

  • 📖 前言
  • 1. 线程的引入
    • 1.1 执行流:
    • 1.2 线程的创建:
    • 1.3 线程的等待:
  • 2. 查看线程
    • 2.1 链接线程库:
    • 2.2 ps -aL:
  • 3. 页表的认识
    • 3.1 二级页表:
    • 3.2 页表的实际大小:
  • 4. 再看线程
    • 4.1 线程总结:
    • 4.2 线程的优点:
    • 4.3 线程的缺点:

📖 前言

从本章开始,我们进入Linux系统编程最后一节多线程的学习,本章我们先来简单的认识一下线程。


1. 线程的引入

在我们之前的Linux学习中,学习了进程的相关概念,操作系统内核中的task_struct描述进程,CPU在运行时,会根据时间片轮询调度进程,让每个进程得以推进。

在之前进程地址空间的学习中,我们知道,每个进程的PCB都可以看到一整个进程地址空间,我们以前学的进程是一个PCB对应一个进程地址空间。

而线程我们可以理解为轻量级进程,每一个进程都可以创建多个线程,并行执行不同的代码。

线程 : 进程 = n : 1
在这里插入图片描述
创建的这三个PCB有了属于它们自己的一小份代码和数据。那么我们把这里的其中一个task_ struct对应的占有这个的进程的一小份代码,一小份数据,使用它局部的一部分页表的,这样的执行流task_struct在Linux中叫做线程

  • 不再独立分配独立的地址空间。
  • 不再分配独立页表,而是所有PCB指向同一个地址空间,甚至将来访问同一张页表。

CPU看待进程和线程是一样的,调度的时候都是以task_struct为单位来调度的。

  • TCB(Thread Control Block)
  • PCB(Process Control Block)

Windows中:

  • 真线程的操作系统当中,pcb和tcb非常复杂。
  • 在真正的线程操作系统中,TCB (Thread Control Block)和PCB(Process Control Block)是分开实现的。

Linux中:

  • 进程和线程在概念上没有区分,只有一个叫做执行流!
  • 进程有优先级,线程也有优先级,都要切换,都要上下文保护, 也要找到对应的代码和数据。
  • 无非是,进程的代码和数据多一些,线程的代码和数据少一些,进程做的工作更多,线程少。

Linux的线程是用进程模拟的PCB模拟的,Linux下也有tcb只不过没有为线程单独设计,用的照样是task_struct

Linux没有提供纯纯的创建线程接口,因为底层没有用真线程,用的是进程作为载体去模拟线程。

进程具有独立性是,有自己的资源,地址空间,页表还有该进程加载到内存中的代码和数据。

以前创建进程是创建独立进程,PCB、地址空间和页表是私有的。

创建线程只创建PCB,CPU调度时,只看PCB。

小结:

  1. 在进程内部运行的执行流。
  2. 线程比进程粒度更细,调度成本更低。
  3. 线程是CPU调度的基本单位。

1.1 执行流:

进程和线程在执行流层面是不一样的。

在Linux中,执行流(Execution Flow)是指程序的执行过程中的控制流动。它描述了程序中指令的顺序执行路径,决定了程序的执行顺序。

  • 单执行流进程:单执行流进程是指在计算机系统中,每个进程只有一个执行线程,即同一时间只能执行一个指令或一个操作。
  • 多执行流进程:多执行流进程是指在计算机系统中,一个进程可以同时拥有多个执行线程,即能够同时执行多个指令或多个操作。

fork之后,父子是共享代码的可以通过if else判断,让父子进程执行不同的代码块不同的执行流,可以做到进行对特定资源的划分。

  • 进程:向系统申请资源的基本单位(系统分配)
  • 线程:系统调度的基本单位

进程(Process)和线程(Thread)在执行流层面上是不一样的:

  • 进程(Process):
    • 进程是操作系统中的一个独立执行单位,它具有独立的内存空间、程序代码和执行环境。
    • 每个进程都有自己的执行流,包括程序计数器(Program Counter)和栈,用于存储指令的地址和局部变量等信息。
    • 进程之间相互独立,并且可以通过进程间通信机制进行数据交换。
  • 线程(Thread):
    • 线程是进程内的一个执行单元,一个进程可以包含多个线程。
    • 与进程不同,线程共享同一个进程的地址空间和资源,在同一个进程中的线程之间可以直接访问共享的内存区域和变量,而无需使用进程间通信的机制。
    • 线程之间可以并发执行,共享进程的执行环境,包括打开的文件、信号处理函数、信号屏蔽字等。

本来串行执行的代码,现在在CPU上可以并发或者并行去执行,让代码在一个时间段或者一个时间点同时得以推进,这种解决方案就叫做线程。

再看进程:

  • 进程 = 内核数据结构 + 进程对应的代码和数据。
  • 进程 = 内核视角:承担分配系统资源的基本实体(进程的基座属性)

再说进程就是PCB就不准确了。包括地址空间,页表,包括构建的映射关系,包括在内存中申请的各种代码和数据对应的内存,包括对应的PCB合起来这一堆才叫进程。

进程的最大意义不是被执行而是:向系统申请资源的基本单位!

  • 内部只有一个执行流的进程 —— 单执行流进程
  • 内部有多个执行流的进程 —— 多执行流进程

以前学的都是单执行流,执行流PCB本身也属于进程内部的资源。
线程是调度的基本单位。

进程切换的成本非常的高,但是进程和线程在CPU中看到的是一样的。
进程切换,地址空间,页表,包括曾经的数据基本都要切换。

内部的执行流就可以称之为一个线程,也就是说一个进程内部可以有一个或者多个线程,CPU调度时, 看到的基本单位全部都叫做线程

1.2 线程的创建:

Linux中没有原生创建线程的接口,但是Linux有原生线程库,由应用级程序员帮我们开发出了一批接口, 叫做pthread_create

不是操作系统的接口,叫做原生线程库:

在这里插入图片描述

  • 第一个参数: 是一个输出型参数,在成功创建线程后,这个变量会被用来保留新线程的ID,供后面的操作使用。
  • 第二个参数: 用来设置线程属性的,可以传递一个nullptr指针,表示使用默认线程属性,也可以通过pthread_attr_t类型的变量来设置自定义的属性。
  • 第三个参数: 是一个函数指针,是指向线程运行函数的指针,函数的返回值和参数必须符合线程函数的要求。
  • 第四个参数: 就是第三个参数,函数指针指向的函数的参数。它是一个void类型的指针,可以传递任意类型的数据给线程函数。

注意:

  • 在现在所有主流的Linux版本中,都默认带这个库,是原生的,在操作系统中就存在的。
  • 不是所谓的系统调用接口,是库函数。

创建线程的时候,本质就是让线程执行进程代码的一部分,有一个进程里面有十几个函数,把某一个函数当做该线程的入口函数,让该线程去调度。

  • CPU看到的所有的task_struct都是一个进程。
  • CPU看到的所有的task_struct都是一个执行流(线程)

线程是属于某一个进程的,所以不需要创建新的mm_struct页表映射,但是创建的效率高于创建子进程。创建新线程后(创建新的PCB)只要将task_struct指向所属进程的mm_struct即可。

在进程中,我们谈父子线程,在线程中,我们谈主新线程。

1.3 线程的等待:

在这里插入图片描述
pthread_ join等待线程的理由:

  1. 释放线程资源,前提是线程退出了。
  2. 获取线程对应的退出码。

线程退出的时候,一般必须要进行join,如果不进行join

  • 就会造成类似于进程那样的内存泄漏的问题(没有僵尸线程这样的说法)
  • 线程对应的退出结果暂时不获取

返回值:

在这里插入图片描述
阻塞等待:

  • pthread_join函数在等待线程时会一直阻塞等待,直到被等待的线程结束并返回。
  • 如果被等待的线程尚未结束,pthread_join函数将一直阻塞当前线程,直到被等待的线程结束为止。

pthread_join第二个参数的理解:

  • 是一个输出型参数,获取新线程退出时的退出码。
  • 进程退出的三种情况:
    • 代码跑完,结果正确。
    • 代码跑完,结果不正确。
    • 异常。
  • 线程也是一样,执行流的退出情况也是上述三种情况。

pthread_join第二个参数为什么是二级指针:

  • 因为是一个输出型参数,要改变指针,就要传指针的地址。

主线程为何没有获取新线程退出时的信号?

  • 线程异常了的话,那么整个进程也就直接退出了。
  • 线程异常 == 进程异常
  • 所以也就是说,一个线程会影响其他线程的运行。
  • 线程的健壮性不如进程。

线程出异常了,不再是线程的问题了,而是进程的问题了。所以pthread_join不需要退出信号。

所以以后考虑线程终止,只考虑正常终止。


2. 查看线程

我们来创建两个线程,来分别查看一下进程和线程:

#include <iostream>
#include <string>
#include <unistd.h>
// #include <pthread.h>
#include <thread> // C++11的线程库using namespace std;void* callback1(void* args)
{string name = (char*)args;while (true){cout << name << ": " << ::getpid() << endl;sleep(1);}
}void* callback2(void* args)
{string name = (char*)args;while (true){cout << name << ": " << ::getpid() << endl;sleep(1);}
}int main()
{// std::thread t([](){//     while(true)//     {//         cout << "线程运行起来啦" << endl;//         sleep(1);//     }// });// 等待就可以了// t.join();pthread_t tid1;pthread_t tid2;pthread_create(&tid1, nullptr, callback1, (void*)"thread 1");pthread_create(&tid2, nullptr, callback2, (void*)"thread 2");while (true){cout << "我是主线程...: " << ::getpid() << endl;sleep(1);}pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);return 0;
}

2.1 链接线程库:

创建线程后,像之前那样编译源文件是不行的,因为要链接线程库:

在这里插入图片描述
查看链接的库:

在这里插入图片描述
链接动态库不明白的小伙伴看过来:👉动静态库👈

  • pthread库是和Linux强相关的库,原生线程库,在用户层实现的线程实现的一种线程实现接口。

2.2 ps -aL:

首先我们来查看一下进程:

在这里插入图片描述
只看到了一个进程,但是我们有三个执行流在跑,怎么只是看到了一个?

  • 这是因为,三个执行流是三个线程(线程1,线程2,主线程),同属于一个进程。
  • ps axj选项是查进程的所以只能查一个。

查看线程:

在这里插入图片描述

  • 在Linux中,LWP的缩写代表Lightweight Process,它意味着轻量级进程。
  • 如果LWPPID是相等的,那么就是主线程,俗称进程。
  • 三个执行流的PID是一样的,说明是在同一个进程内的三个执行流。

在这里插入图片描述

多个线程谁先运行也不确定,完全是调度器自己决定。
C++11里的多线程和操作系统底层的原生线程库是封装关系。


3. 页表的认识

字符常量不可被修改曾经是怎么加载到内存中的呢?

  • 字符常量不可被修改,修改的话,编译不会报错,但是运行时报错了。
  • 是因为当尝试着去修改时候,页表里有对应的条目,会限制进行读写。

在这里插入图片描述

如果不可被修改,那么曾经是怎样加载到内存里的呢?

  • 内存在任何时候都可以被读取的,只不过是能不能读取的问题。
  • 所以在语言上,经过虚拟地址到物理地址转化的时候,会有个读取权限,如果是正常数据是RW,如果是字符串是R(只读的)。
  • 所以在尝试写入时,直接在页表那一层拦截这个进程。
  • 那么MMU也叫做内存管理单元,这个硬件结合页表中读取的数据,就会发生异常。
  • 操作系统发现并识别这个异常,解释称信号,发送给目标进程,直接终止掉进程了。

语言层有些字符串是常量的,代码是只读属性是如何保证的,根本原因是因为在转化过程中拦截了。

从用户空间到内核空间的映射是由页表来完成的:

  • 页表分为用户级页表和内核级页表。
  • 页表结构都是一样的,所有进程用的都是一套内存管理机制。
  • UK来确认当前指向的内容是内核代码还是用户代码。
  • UK用来区分进程用的是内核级页表还是用户级页表。
  • 每一个虚拟地址都要对应一个物理地址。

页表有多大:

  • 假设页表只有一张,请问有多少条目?
    • 一共有2^32个条目。
  • 保守计算一个条目8Byte,那么整个页表有多大?
    • 2 ^ 32 * 8 Byte = 32 GB
  • 要是真的这样的话,内存早就被页表占满了。

3.1 二级页表:

操作系统通常使用多级页表(Multilevel Page Table)以实现虚拟内存管理:

  • 32位系统中用的是两级页表。

在这里插入图片描述

  • CPU根据指令内部的地址,进行寻址再访问物理内存的时候,CPU里出来的地址是虚拟地址。
  • 虚拟地址在被转化的过程中,不是直接转化的,而是被划分成了10+10+12

文件系统和物理内存进行IO的时候,IO的基本单位默认是4KB

  • 物理内存通常被划分成大小相等的页框(Page Frame)。
  • 页框是物理内存中的最小单位,用于存储数据和指令。
  • 每个页框的大小由系统设计决定,常见的大小包括 4KB、8KB、16KB 等。

4GB物理内存为例,每个页框4KB,那么一共有,4GB / 4KB = 1024 * 1024 = 2^20 个页框。

操作系统要将页框管理起来:

  • 一定是先描述,再组织。
  • struct page的结构体中描述页框。
  • struct page mem[1024 * 1024]中管理。

虚拟地址编译,也划分好了4KB:

  • 数据加载到内存,实际上是程序按照4KB为单位可以整体加载。
  • 当然也可以把程序的一部分以4KB为单位加载到内存当中。

页表中的page起始地址,只记录了某个page,不关心页内细节:

  • 是否命中是以页为单位的
  • 在用虚拟地址找一级页表和二级页表的时候
  • 其中先找的是page,说明在计算机中找内存是以页为单位找的
  • 找到后根据最后12位,找到在页内的偏移量是什么位置

物理内存一般4GB,一个页框是4KB,那么内存一共被划分成了2^20个页框。

虚拟地址后12位:

  • 虚拟地址的后12位,一共有2^12次方个地址。
  • 而一个页框是4KB = 2^12B,所以虚拟地址后12位将一整个页框所有地址全部覆盖了。

页表中的Page帧地址是用于标识物理内存中每个Page框的编号的。

页表只需要映射到page就不需要映射了,拿虚拟地址后12位做偏移量的:

  • 之前讲的映射是有问题的,我们将虚拟地址到物理地址转化是按照字节为单位映射的。
  • 其实只需要找到page这一目就不要再映射了。
  • 最后再拿虚拟地址后12位找偏移量就好了。

用虚拟地址找page,再根据虚拟地址找页内偏移量来找到的。

page命中:

  • 有没有命中,即要访间的空间是否在物理内存里面。
  • 如果没有命中,那么进程就暂时不被调度了。
  • MMU会报错,会触发缺页中断的东西。

所以CPU就找到了对应的数据,然后就读取里面的数据,此时这里的数据就会被CPU再次拿到,CPU做计算等操作,如果还有寻址指令,那就再回过头,再重复刚刚的过程。

这样做的优点:

  • 进程虚拟地址管理和内存管理,通过页表 + page进行解耦
  • 分页机制 + 按需创建页表 = 节省空间
  • 此时页表就被分离了,就可以实现按需创建

3.2 页表的实际大小:

  • 假设一个条目有20Byte,页表最大也就:20B * (2 ^ 32 / 2 ^ 12) = 20B * 2 ^ 20 = 20B * 1M = 20MB

表映射是通过MMU(内存管理单元)来实现的,软(表)硬件(MMU)结合的方式。


4. 再看线程

4.1 线程总结:

  • 使用计算机的时候,所有的行为都会成为进程,人和计算机交互的时候,全都是以进程为载体完成所有的任务的。
  • 进程是承担分配资源的基本实体。
  • 以前讲的进程是:内核数据结构 + 进程的代码和数据。
  • 内核数据结构,包括把代码和数据加载到内存里,本质是申请内存空间。是在做资源准备,真正去执行的是内部的线程。
  • 线程是在进程的地址空间内去运行的,地址空间是进程看待它自己资源的一个统一的视角,进程看待内存等资源是以统一地址空间的方式去看待的。
  • 线程的执行力度比进程更细,调度成本更低,执行的是进程的一部分,访问的是进程的一部分资源,使用的是进程一部分的数据。
  • 调度成本更低,因为在线程切换时,不需要切换页表地址空间,还有CPU中不可显示的寄存器值,只需要将线程需要切换的上下文数据切换就可以,其他的切换成本就很低了。
  • 线程是CPU调度的基本单位。

4.2 线程的优点:

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

4.3 线程的缺点:

性能损失、健壮性降低、缺乏访问控制、编程难度提高。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

WPF——Control与Template理解

文章目录 一、前言二、控件三、模板3.1 DataTemplate3.2 ControlTemplate3.3 ContentPresenter 四、结语 一、前言 最近又翻看了下刘铁猛的《深入浅出WPF》&#xff0c;发现对模板章节中的部分内容有了更深的体会&#xff0c;所以写篇文扯扯。 文章标题是Control与Template&a…

Linux vim的常见基本操作

目录 vim是一款多模式的编辑器 命令模式下&#xff1a; 用小写英文字母「h」、「j」、「k」、「l」&#xff0c;分别控制光标左、下、上、右移一格 gg&#xff1a;定位到代码第一行 nshiftg 定位到任意一行/最后一行 「 $ 」&#xff1a;移动到光标所在行的结尾 「 ^ 」&…

python基础开发篇3——线上环境部署Django项目

文章目录 一、基本了解二、打包本地项目三、服务器环境准备四、安装web服务4.1 使用uwsgi代理4.2 使用nginx代理&#xff08;推荐&#xff09; 五、部署daphne 一、基本了解 部署思路&#xff1a; Nginx服务接收浏览器的动态请求&#xff0c;再通过uwsgi模块将请求转发给uwsgi服…

利用html+css+js实现回到顶部小功能

本章教程&#xff0c;主要是实现一个网站中比较常见的小功能&#xff0c;这个功能就是回到顶部。 功能描述&#xff1a;当浏览器右侧的滚动条&#xff0c;滑动到某个位置的时候&#xff0c;显示回到顶部图标&#xff0c;回到顶部之后&#xff0c;图标作隐藏处理&#xff0c;本文…

c++数据类型

基本数据类型简介 位、字节和内存寻址 最小的内存单位是二进制数字&#xff08;也称为位&#xff09;&#xff0c;它可以保存 0 或 1 的值。你在现代计算机体系结构中&#xff0c;每个位都没有自己唯一的内存地址。这是因为内存地址的数量有限&#xff0c;并且很少需要逐位访…

目标检测笔记(十四): 使用YOLOv8完成对图像的目标检测任务(从数据准备到训练测试部署的完整流程)

文章目录 一、目标检测介绍二、YOLOv8介绍三、源码获取四、环境搭建4.1 环境检测 五、数据集准备六、 模型训练6.1 方式一6.2 方式二6.3 针对其他任务 七、模型验证八、模型测试九、模型转换9.1 转onnx9.1.1 方式一 9.2 转tensorRT9.2.1 trtexec9.2.2 代码转换9.2.3 推理代码 一…

《JDK17新特性和代码案例演示》

《JDK17新特性和代码案例演示》 &#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全…

酷开系统音乐频道,用音乐治愈你!

音乐作为娱乐生活中的一部分&#xff0c;它可以起到调节心情让身体放松的作用&#xff0c;同时还可以舒缓压力&#xff0c;给大脑一个休息的时间。有句话说得好&#xff1a;“耳机是人类的避难所&#xff0c;音乐是心脏的救命丸”。音乐是一种疗愈身心的存在&#xff0c;耳机线…

最新版WPS 2023 加载Zotero方法

安装wps2019vba.exe&#xff0c;获取链接&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1eeoc6Tmwyzxh3n1MFQTVeA 提取码&#xff1a;6431 –来自百度网盘超级会员V8的分享 打开WPS的工具的加载项 添加文件路径&#xff0c;我的在&#xff1a; C:\Users\Administrat…

Vue3+Ts中使用Jquery

1、安装jquery&#xff1a;npm i jquery --save 2、在vue.config.js文件中添加如下代码&#xff1a; const { defineConfig } require(vue/cli-service) const webpack require(webpack)module.exports defineConfig({configureWebpack: {plugins: [// 配置jQuerynew webp…

【C++ 学习 ㉑】- 详解 map 和 set(上)

目录 一、C STL 关联式容器 二、pair 类模板 三、set 3.1 - set 的基本介绍 3.2 - set 的成员函数 3.1.1 - 构造函数 3.1.2 - 迭代器 3.1.3 - 修改操作 3.1.4 - 其他操作 四、map 4.1 - map 的基本介绍 4.2 - map 的成员函数 4.2.1 - 迭代器 4.2.2 - operator[] …

解决出现的java: 无法访问org.springframework.boot.SpringApplication问题~

错误描述如下所示&#xff1a; 错误原因&#xff1a;版本号匹配不一致导致的&#xff0c;61.0对应jdk17&#xff0c;52.0对应jdk8。 而我本地的java为java8&#xff0c;因此需要降低版本&#xff0c;即可解决该问题 <groupId>org.springframework.boot</groupId>…

EndNote21 | 安装及库的创建

EndNote21 | 安装及库的创建 一、EndNote21安装二、EndNote21库的创建 一、EndNote21安装 软件安装界面&#xff0c;双击“EndNote 21.exe”程序&#xff1b; 图1 安装软件界面点击next&#xff0c;选择30天试用&#xff0c;点击next&#xff1b; 图2 安装过程点击next&…

深入理解Linux网络笔记(一):内核是如何接收网络包的

本文为《深入理解Linux网络》学习笔记&#xff0c;使用的Linux源码版本是3.10&#xff0c;网卡驱动是Intel的igb网卡驱动 Linux源码在线阅读&#xff1a;https://elixir.bootlin.com/linux/v3.10/source 1、内核是如何接收网络包的 1&#xff09;、Linux网络收包总览 在TCP/I…

腾讯mini项目-【指标监控服务重构】2023-08-20

今日已办 PPT制作 答辩流程 概述&#xff1a;对项目背景、架构进行介绍&#xff08;体现我们分组的区别和需求&#xff09;人员&#xff1a;小组成员进行简短的自我介绍和在项目中的定位&#xff0c;分工进展&#xff1a;对项目进展介绍&#xff0c;其中a、b两组的区别和工作…

Azure + React + ASP.NET Core 项目笔记一:项目环境搭建(二)

有意义的标题 pnpm 安装umi4 脚手架搭建打包语句变更Visual Studio调试Azure 设置变更发布 pnpm 安装 参考官网&#xff0c;或者直接使用npm安装 npm install -g pnpmumi4 脚手架搭建 我这里用的umi4&#xff0c;官网已附上 这里需要把clientapp清空&#xff0c;之后 cd Cl…

YUVToRGB(CUDA Conversion)库的学习

目录 前言1. YUVToRGB1.1 Supported Feature1.2 Performance1.2.1 Performance Table1.2.2 How to Benchmark1.2.3 How to Verify the Accuracy 1.3 User Integration1.4 Summary 2. YUVToRGB案例2.1 环境配置2.2 run案例 3. YUVToRGB浅析4. 补充知识4.1 YUV不同格式区别4.2 Lu…

代码随想录算法训练营Day60 | 84. 柱状图中最大的矩形

文章目录 84. 柱状图中最大的矩形首尾加 0双指针 84. 柱状图中最大的矩形 题目链接 | 解题思路 本题和接雨水的题目相互呼应&#xff0c;但是难度略有提升&#xff0c;同样是一道非常棒的题&#xff01; 在接雨水中&#xff0c;需要找到每一列的左侧最大值和右侧最大值&…

Mybatis基础知识(一)

Mybatis基础知识(一) Mybatis基础知识 Mybatis基础知识(一)一、MyBatis特性二、和其它持久化层技术对比三、MyBatis的简单使用1、创建maven工程2、创建pojo对象3、创建MyBatis的核心配置文件①properties②typeAliases③environments:④mappers:引入映射文件 4、创建mapper接口…

fabic如何将绘图原点移到画布中心

情况说明&#xff1a; fabic默认绘图原点为left&#xff1a;0&#xff0c;top&#xff1a;0 后端给我的内容是按照x&#xff0c;y返回的&#xff0c;需要将坐标系移到fabic画布的中心位置&#xff0c;找了下网上合适的砖&#xff0c;想一句命令直接设置&#xff0c;结果没有。…