基于多线程版本的定时器

news2024/11/26 0:56:48

定时器

1)咱们前面学习过的阻塞队列,相比于普通的队列线程安全,相比于普通的队列起到一个更好的阻塞效果

2)虽然使用阻塞队列,可以达到销峰填谷这样的一个效果,但是峰值中有大量的数据涌入到队列中,如果后续的服务器消费很慢的话,队列中的某些元素就会滞留很久,那么此时就可以使用定时器,让滞留太久的请求直接消费掉;

3)我们的定时器就是类似于一个闹钟,我们可以进行定时,在一定时间之后,我们就可以被唤醒并执行之前我们已经设定好的某一个任务

4)咱们的join可以指定超时时间,咱们的sleep也可以指定休眠时间,他们都是依靠当前系统中内部的定时器来进行实现的

5)系统的定时任务是依靠java.util.Timer包底下的,核心方法只有一个schedule;

咱们的TimerTask这个类是一个抽象类,这个抽象类的底层实现了Runnable接口

        Timer timer=new Timer();
        timer.schedule()里面有两个参数,一个是描述一个任务,一个参数是多长时间后执行这个任务
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("触发定时器");
            }
        },3000);
    

一个定时器里面是可以安排很多任务的,这些任务就会按照时间,谁先到了时间,就会先执行谁

1)描述任务:使用一个runnable来进行描述任务

2)组织任务:就需要有一个数据结构,把这里面的很多任务给放到一起
2.1)在定时器里面要有工作线程不断进行扫描是从一大堆任务中找到那个最先要到点的任务,使用优先队列,最好使用带有阻塞队列的数据结构,为了线程安全;(数组还需要遍历)

2.2)谁时间越靠前,我们就把这个任务放到最前面,快要到时间的时候,我们再把这个任务取出来,执行就可以了
2.3)此处我们最好使用带阻塞功能的优先队列,这有可能涉及到多线程操作,我们就可以保证线程安全了

3)定时器里面提供一个schedule方法,这个方法就是向我们的阻塞队列里面添加元素

4)我们还需要让Timer内部有一个工作线程,我们就需要让这个线程可以一直去扫描队首元素,看看队首元素是否到点了,如果到点,那么就执行这个任务,如果没有到点,我们就需要把这个队首元素塞回到队列里面,继续进行扫描

Timer内部都需要啥东西? 

1)管理很多的任务

2)执行时间到了的任务

啥叫管理任务?

管理任务,就是通过描述+组织的形式,描述是通过一个类来描述一个任务的属性,组织是通过一定的数据结构将这些任务放到一起

1)我们直接创建一个Task类(与其他代码没有耦合),这个类就表示了我们的定时器中的一个一个的具体任务,这个类描述了我们具体的要执行什么任务?多长时间执行?如何执行这个任务的工作内容?这个类的实例最后是阻塞队列中的一个一个的元素

建立一个Task类,直接传一个runnable来描述这个任务(任务内容在runnable里面),以及要执行的时间,后续再把这个任务放到阻塞队列中,从阻塞队列中拿出来的时候调用Task里面的run方法来执行里面的任务即可,时间要写一个毫秒级的时间戳,Task里面有run方法,在这里面直接执行runnable里面的方法;

2.我们创建一个Timer类,里面包含了阻塞队列(将我们的所有任务放到阻塞队列里面来进行保存),还有schedule方法,它的作用就是说根据传入的Runnable接口和指定时间创建一个Task类,并把这个Task类装到阻塞队列里面,相当于是注册一个任务

2.1)假设我们现在有多个任务过来了,10小时之后去做作业,11个小时之后去上课,10分钟之后去休息一会,当我们进行安排任务的时候,这些任务的顺序是无序的,但是当我们执行任务的时候,这就不是无序的了,我们的需求就是在我们能够安排的任务里面可以找到时间最小的任务,我们就需要创建一个小根堆,所以说在咱们的JAVA标准库里面就有一个专门的数据结构叫做PriorityQueue(派奥瑞忒),叫做优先级队列,不能使用顺序表或者链表

2.2)里面有一个构造方法,里面就有一个工作线程来进行循环扫描定时器里面的任务(扫描阻塞队列中的队首元素),对于构造方法来说,我们可以这么想,每当我们启动一个定时器的时候,这个定时器都会创建一个工作线程,来进行循环扫描阻塞队列里面的任务来进行执行

3.我们有一个工作线程,来循环进行扫描阻塞队列中的任务,循环取出队首元素中的任务来进行和当前时间进行对比,看看时间是否已经到了,看看是否要执行这个任务,我们要进行循环扫描,如果时间到了,就执行,时间不到,就把这个元素带回去

import javafx.scene.layout.Priority;
import java.util.concurrent.PriorityBlockingQueue;
class HelloWorld {
   static Object object1=new Object();
    static class Task implements Comparable<Task> {
        public long time;//任务具体什么时候执行
        Runnable runnable;//表示任务具体要干啥
        public Task(Runnable runnable, long after) {
            this.runnable = runnable;
            this.time = after + System.currentTimeMillis();
        }
        public void run() {
            runnable.run();
        }
        public int compareTo(Task o) {
            return (int) (this.time - o.time);
        }
    }
//我们进行创建一个扫描线程,这个扫描线程的主要作用就是说扫描阻塞队列,看看这个任务是不是到时间就可以进行执行
    static class Worker extends Thread {
        PriorityBlockingQueue<Task> queue = null;
        public Worker(PriorityBlockingQueue<Task> queue) {
            this.queue = queue;
        }
        public void run() {
            while (true) {//这里我们是需要进行循环扫描的,因为我们在不停的向定时器里面添加任务,队首可能在不断发生变化
                try {
//去除阻塞队列里面的元素看看执行时间是否到了
                    Task task = queue.take();
                    long currenttime = System.currentTimeMillis();
                    if (task.time > currenttime) {
//这就表示当前队列的队首元素的时间还没有到,我们暂时先不执行,前面的take操作会把队首元素删除掉,这种情况是不可以进行删除的,我们需要重新插回到队列里面
                        queue.put(task);
                        synchronized (object1){
                            object1.wait(task.time-currenttime);
                        }
                    } else{
                        task.run();
                    }
                } catch(InterruptedException e){
                    e.printStackTrace();
//如果说我们这个循环被异常退出了,此时我们break就可以结束循环
                    break;
                }
            }
        }
    }
    static class Timer {
        PriorityBlockingQueue<Task> queue=new PriorityBlockingQueue<>();
        public Timer()
        {
            Worker worker=new Worker(queue);
            worker.start();
        }
        void schedule(Runnable runnable,long after)
        {
            Task task =new Task(runnable,after);
            queue.put(task);
            synchronized (object1)
            {
                object1.notify();
            }
        }
    }
    public static void main(String[] args) {
        Timer time = new Timer();
        time.schedule(new Runnable() {
            public void run() {
                System.out.println("任务正在执行");
            }
        }, 200);
    }
}

1)此时我们所进行选取的队列一定要进行注意线程安全问题,我们可能会在多个线程中增加任务这样子的操作,同时我们还有一个专门的线程来进行取出任务执行,此时的队列就需要进行注意线程安全问题,下面的这个队列带有有优先级又具有阻塞效果

BlockingQueue<Task> queue=new PriorityBlockingQueue<>()

2)在这里面其实我们的take()操作其实是不如peek()操作高效的,因为我们每一次take()操作都会取出队首元素,并且把整个堆继续向下调整成小堆,这里面的时间复杂度就是O(logN)

3)咱们目前是把这个元素放到优先级队列里面,而我们的优先级队列的内部是一个堆,是堆的话我们就需要进行调整,而调整就需要知道元素之间的大小关系,而我们随便写一个类,他们的大小关系是不明确的,像咱们之前写的普通的一个Task类,他的比较规则并不是默认就存在的,这个需要我们手动指定按照时间来进行比较大小;

4)当使用PriorityBlockingQueue对队列的元素进行排列的时候,就会把所有的元素向Comparable这个方向来进行转换,转换之后再调用compareTo方法来进行比较,此时发现无法转化成功,转化不成功,就会出现类型转化异常

Thread.MyTask can not be cast to JAVA.lang.Comparable

1)其实这个代码加锁解决一个很严重的问题,那就是忙等问题,例如现在队列中有一个元素,他的执行时间是8:30;

2)当前时间8:01,时间还没到,取出队首元素又放回去,又看了一眼时间,显然这个循环不会执行当前的任务,代码中又会把这个任务放回到阻塞队列里面了,这个时候这个循环会转得非常快,1s能转几万次,就会白白的浪费CPU,虽然CPU在干活,但是CPU干的事情没有任何意义,只是进行循环取队列元素,进行比较,然后直接放回去了,如果说当前我们的队列中没有任何元素,那还好,我们的这个线程只是在这里面阻塞了,没有问题,我们怕的就是说任务队列里面的任务不为空,还有队首元素时间和现实时间差很大,一直循环取元素放元素,这样的操作就没有任何意义,还一直吃CPU资源,即没有实质性的工作产出,也没有休息,像这种操作,是非常浪费CPU资源的;

3)8:01发现时间还没到,去除队首元素又放回去,还有一个小时呢,在循环中频繁的取出元素,放回去,这不是频繁占用CPU资源吗?

解决方案:假设当前队首元素是在8:30来进行执行的

1)如果在8:30之前没有其他Task元素进入队列,就让他一直阻塞等待到执行时间,然后被唤醒,不需要notify,时间到了自动被唤醒,计算当前时间和目标时间的时间差,就让他等待这么长的时间就可以了;

2)如果有Task元素新入队的话(例如插入的时间为8:05),就唤醒wait操作此时出的队首元素就是8:05

既然我们说了是指定一个等待时间,那么为什么不去使用sleep,而是去使用wait呢?

1)因为咱们的sleep是不可以被中途唤醒的,但是咱们的wait是可以被中途唤醒的

2)因为我们在等待的过程中,可能会插入新的任务,新的任务是很有可能出现在所有任务的最前面的,而我们的sleep操作使唤不醒的,所以我们无法支持最新的任务操作吧,所以我们需要在最新的schedule操作中,我们每一次进行新增任务的时候,都需要进行唤醒操作

3)我们进行唤醒一下扫描线程,让我们的线程重新检查一下队首的任务看看时间到了是否要执行,有可能我们进行新插入进去的任务比原来队首元素的任务还要靠前呢,如果没到,还是要重新计算计算一下新的等待时间

我们使用wait可以指定一个时间作为参数,可以通过当前时刻和首个任务之间的执行间隔来进行计算,wait的唤醒有哪些条件呢?

1)指定时间进行唤醒,比如说现在有一个7:00的任务,但是现在是6:30,我们就可以通过wait来进行指定时间,时间到了,我们就开始执行这个任务

2)我们可以直接通过notify方法来唤醒wait操作,比如说现在有一个7:00的任务,但是现在此时才6:30,我们是通过wait来进行指定半个小时,但是说如果现在有一个6:50的任务插入了进来,我们就要唤醒wait操作,让我们的线程就重新进行扫描;

我们使用wait就是解决新插入的任务比原有的任务还要往前

 线程等待wait()和通知notify(),主要用于多线程之间的通信协作,而且这两个方法都是属于Object类,说明任何对象都可以调用这两个方法。 当在一个实例对象上调用了wait()方法之后,当前线程就会在这个对象上等待,直到另外的线程调用了该对象的notify()方法,处于等待状态的线程才得以继续进行, 这样,多线程之间就可以用这两个方法进行通信协作了

定时器总结:

1)先进行描述一个任务,Runnable+time

2)使用优先级队列来进行组织若干个任务,通过PriorityBlockingQueue

3)通过schedule方法来注册任务到我们的阻塞队列里面

4)创建一个扫描线程,让这个线程可以不停的来进行获取队首元素,并且判定时间是否到达

5)况且说可以让MyTask类支持比较,并且能够解决这里面的忙等问题

线程池的引入:

1)进程一般来说是比较重量的频繁创建和销毁进程,开销是很大的,这个时候的解决方案就是:线程池或者线程

2)咱们的线程虽然比进程要轻量很多,但是如果说创建和销毁的频率进一步增加,那么此时我们会发现开销还是很大的,解决方案就是:线程池或者是协程

3)所以说我们可以把线程提前创建好放到一个池子里面,后面我们再次想要用到线程直接就从池子里面取出来,就不需要在从系统这边申请了;咱们的线程使用完毕之后,也不是直接还给系统,而是说放回到池子里面,以备下次继续使用,这就会比频繁创建线程和销毁线程速度要更快了

为什么线程放到池子里面,比从系统中这边申请来得更快呢?

在我们的操作系统里面一共分成了两种状态:

用户态VS内核态

一部分逻辑是应用程序里面执行的,一部分是在操作系统内核里面控制硬件来执行的

1)咱们自己写的代码,就是在最上面的应用程序这一层来进行运行的,咱们在这里面的代码就被称为用户态运行的代码;

2)但是我们有些代码,需要我们进行调用操作系统的API,进一步的逻辑就会在内核里面执行,比如说当我们调用System.out.println()的时候,本质上要经过write()系统调用,进入到内核里面,在内核里面执行一堆逻辑,控制显示器输出一个字符串,咱们的上面的那一个简单代码,一部分是咱们的应用程序里面执行的,一部分逻辑就是要在操作系统内核里面控制硬件来去完成的,在内核里面运行的代码,我们就称之为内核态运行的代码;

3)我们在进行创建线程的时候,本身就需要内核的支持因为我们创建线程的本质就是在内核中创建一个PCB,加到双向链表里面,我们调用的Thread.start()本身就是如此,也是要在内核态来运行的;

4)我们把创建好的线程放到池子里面,因为池子就是用户态来进行实现的,把这个放到池子里面,从池子里面取,这个过程是不需要涉及到内核态,我们只需要单纯纯粹的用用户态代码就可以完成;

内核态的操作:当我们进行调用start()方法的时候,调用内核态代码,就会在操作系统内核里面进行创建出PCB,效率比较低

用户态的操作:当我们用到线程的时候,就从池子里面取,不用线程,就放回到池子里面;

5)所以说,一般我们认为纯用户态的操作,效率要比经过内核态处理的操作,效率要更高;

6)我们线程池里面的线程,是一直保存在线程池里面,不会被内核回收;

7)我们平常认为内核效率低,并不是真的低,而是代码进入了内核态,就不可控了,内核啥时候可以把活干完,把结果给你,这些都是不可控的;

8)而咱们的线程池中的线程会一直保存在线程池里面,不会被内存回收,会一直等待程序调用;

内核态或者用户态的工作过程就是类似于银行办事:

1)滑稽老铁,自己带着身份证来到大厅的复印机这里来进行复印,这是纯用户态的事情,自己很着急的就进行忙完了,这个操作完全是由自己完成的,整体的过程是可控的

2)我们的滑稽老铁,把身份证交给银行的柜台,让柜员帮助他来进行复印,这个过程就是类似于交给了内核态来进行完成一些工作,这个操作不是自己完成的,整体是不可控的

因为咱们也不知道咱们的柜员到底是去做什么样的工作去了

3)可能说是给你复印去了,因为咱们的操作系统内核也是很忙的,也有可能顺手去做一些其他的事情,比如说,数一下钱,清点一下票据,上个厕所,给女神发一个消息,确实它可以把这个东西给你复印完拿回来,但是你没有办法知道它是否是立即就去复印了,去办你自己想要干的事情

4)不知道这个操作系统内核再复印的过程中又做了哪些其他的事情,你没有办法对这个柜员的行为作出任何的控制的,你不能控制它约束他立刻就去给你进行复印,一旦他从柜台这里消失了,整个过程就处于一个不可控的状态,所以这个柜员可能在一分钟之内就给你复印回来了,也有可能5min回来,也有可能10min回来,还有可能一个小时回来;

1)我们认为内核态效率低,并不是说真的低,而是代码进入到了内核态,就不可控了,内核有可能会捎带着去完成一些其他的事情;

2)我们不知道内核什么时候把活干完,把结果给你,有的时候快,有的时候慢

用户态的代码咱们是可控的,但是内核态的代码都是内核实现好的,谁知道他是怎么跑的,不知道他是否除了我们自己想做的工作还是否具有其他的任务,就是十分的不稳定,谁知道什么时候把活干好

class Student implements Comparable<Student>{
    public int age;
    public String name;
    public Student(int age, String name) {
        this.age = age;
        this.name = name;
    }
    @Override
    public int compareTo(Student o) {
        return o.age-this.age;
    }
    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}
class Solution{
    public static void main(String[] args) {
          Student student1=new Student(19,"李佳鑫");
          Student student2=new Student(20,"李树全");
          Student student3=new Student(21,"李佳伟");
          Student[] studentDemo=new Student[]{student1,student2,student3};
          Comparable<Student>[] students=(Comparable<Student>[])studentDemo;
          Arrays.sort(students);//底层会调用compareTo方法
        System.out.println(Arrays.toString(students));
    }
}

 


1)在我们的优先级队列里面,如果实现了Comparator接口,那么会默认使用这个比较器调用里面的compareTo方法

2)在我们的优先级队列里面,如果说实现了Comparable接口,那么默认会将这种类型转化成Comparable类型

 

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

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

相关文章

教程:Flutter 和 Rust混合编程,使用flutter_rust_bridge自动生成ffi代码

实践环境&#xff1a;Arch Linuxflutter_rust_bridge官方文档Flutter环境配置教程 | Rust环境配置教程记录使用flutter_rust_bridge遇到的一些坑。假设已经我们配置了Fluuter与Rust环境现在直接使用flutter_rust_bridge模板创建自己的项目运行&#xff1a;git clone https://gi…

W13Scan 扫描器挖掘漏洞实践

一、背景 这段时间总想捣鼓扫描器&#xff0c;发现自己的一些想法很多前辈已经做了东西&#xff0c;让我有点小沮丧同时也有点小兴奋&#xff0c;说明思路是对的&#xff0c;我准备站在巨人的肩膀去二次开发&#xff0c;加入一些自己的想法&#xff0c;从freebuf中看到W13Scan…

进程调度模块

目录 1.进程介绍 2.进程调度 2.1.进程状态 2.2.进程调度函数 ---schedule 2.3.进程切换函数 ---switch_to&#xff08;&#xff09; 1.进程介绍 在进程模块里面&#xff0c;我们知道了进程就是一个task_struct的结构体&#xff0c;里面含有进程的各种信息。进程存放在进程…

AppScan被动手动探索扫描

系列文章 AppScan介绍和安装 AppScan 扫描web应用程序 第三节-AppScan被动手动探索扫描 被动式扫描&#xff1a;浏览器代理到AppScan&#xff0c;然后进行手工操作&#xff0c;探索产生出的流量给AppScan进行扫描。 他的优点是&#xff1a;扫描足够精准&#xff0c;覆盖率更…

注册中心和负载均衡(黑马SpringCloud笔记)

注册中心和负载均衡 目录注册中心和负载均衡一、服务远程调用1. RestTemplate2. 服务调用关系3. 远程调用的问题二、注册中心1. Eureka注册中心1.1 搭建Eureka注册中心1.2 服务注册1.3 服务拉取1.4 小结2. nacos注册中心2.1Nacos搭建2.2 服务注册2.3 服务拉取2.4 服务分级存储模…

虹科新闻 | 虹科与丹麦Eupry正式建立合作伙伴关系

近期&#xff0c;虹科与丹麦Eupry正式建立合作伙伴关系。未来&#xff0c;虹科与Eupry将共同关注最具创新性和稳定性的解决方案&#xff0c;为客户提供温度记录仪、温湿度记录仪、Mapping温度分布验证服务、以及基于云的温湿度自动监测系统。 虹科非常高兴欢迎并宣布我们的新合…

【Linux】基础:进程信号

【Linux】基础&#xff1a;进程信号 摘要&#xff1a;本文将会从生活实际出发&#xff0c;由此掌握进程信号的学习过程&#xff0c;分别为信号的产生、信号的传输、信号的保存和信号的处理&#xff0c;最后再补充学习信号后方便理解的其他概念。 文章目录【Linux】基础&#xf…

echarts柱状图值为0时不显示以及柱状图百分比展示

echarts柱状图值为0时不显示以及柱状图百分比展示 1.效果展示 2.代码 <template><div id"container"><div id"main"></div></div> </template> <script>import * as echarts from echarts import * as lodash…

(JVM)浅堆深堆与内存泄露

​浅堆深堆与内存泄露 1. 浅堆&#xff08;Shallow Heap&#xff09; 浅堆是指一个对象所消耗的内存。在 32 位系统中&#xff0c;一个对象引用会占据 4 个字节&#xff0c;一个 int 类型会占据 4 个字节&#xff0c;long 型变量会占据 8 个字节&#xff0c;每个对象头需要占用…

01.【Vue】Vue2基础操作

一、Vue Vue (读音 /vjuː/&#xff0c;类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是&#xff0c;Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层&#xff0c;不仅易于上手&#xff0c;还便于与第三方库或既有项目整合。另一方面&…

十五天学会Autodesk Inventor,看完这一系列就够了(七),工程图纸

众所周知&#xff0c;Autocad是一款用于二维绘图、详细绘制、设计文档和基本三维设计&#xff0c;现已经成为国际上广为流行的绘图工具。Autodesk Inventor软件也是美国AutoDesk公司推出的三维可视化实体模拟软件。因为很多人都熟悉Autocad&#xff0c;所以再学习Inventor&…

自动化测试 | 这些常用测试平台,你们公司在用的是哪些呢?

本文节选自霍格沃兹测试学院内部教材 测试管理平台是贯穿测试整个生命周期的工具集合&#xff0c;它主要解决的是测试过程中团队协作的问题。在整个测试过程中&#xff0c;需要对测试用例、Bug、代码、持续集成等等进行管理。下面分别从这四个方面介绍现在比较流行的管理平台。…

Spring入门-SpringAOP详解

文章目录SpringAOP详解1&#xff0c;AOP简介1.1 什么是AOP?1.2 AOP作用1.3 AOP核心概念2&#xff0c;AOP入门案例2.1 需求分析2.2 思路分析2.3 环境准备2.4 AOP实现步骤步骤1:添加依赖步骤2:定义接口与实现类步骤3:定义通知类和通知步骤4:定义切入点步骤5:制作切面步骤6:将通知…

Anaconda+VSCode配置tensorflow

主要参考https://blog.csdn.net/qq_42754919/article/details/106121979vscode的安装以及Anaconda的安装网上有很多教程&#xff0c;大家可以自行百度就行。在安装Anaconda的时候忘记勾选自动添加path&#xff0c;需要手动添加环境变量path下面介绍tensorflow安装教程:1.打开An…

getRequestDispatcher()转发和sendRedirect()重定向介绍与比较

文章目录1. request.getRequestDispatcher()1.1请求转发和请求包含的区别1.2request域2.response.sendRedirect()3.请求转发与重定向的区别比较测试1. request.getRequestDispatcher() getRequestDispatcher()包含两个重要方法&#xff0c;分别是请求转发和请求包含。一个请求…

系分 - 案例分析 - 系统设计

个人总结&#xff0c;仅供参考&#xff0c;欢迎加好友一起讨论 文章目录系分 - 案例分析 - 系统设计结构化设计SD内聚偶然内聚逻辑内聚时间&#xff08;瞬时&#xff09;内聚过程内聚通信内聚顺序内聚功能内聚耦合内容耦合公共耦合外部耦合控制耦合标记耦合数据耦合非直接耦合补…

DTO 与 PO的相互转换

目录 常见Bean映射框架 Dozer Orika MapStruct ModelMapper JMapper 测试模型 转化器 OrikaConverter DozerConverter MapperStructConvert JMapperConvert ModelMapperConverter 测试 平均时间 吞吐量 SingleShotTime 采集时间 DTO&#xff08;Data Transfer …

Android项目Gadle统一依赖管理

一.Gradle管理依赖版本 在中大型Android项目中&#xff0c;都会有多个Module进行协同配合。这些module中可能会依赖同一个库的不同版本&#xff0c;这将导致一些问题&#xff0c;要么是代码冲突&#xff0c;要么是APK包体积增大&#xff0c;亦或是项目构建的时间变长&#xff…

在Revit里如何将普通墙与曲面墙的内壁连接

在Revit里如何将普通墙与曲面墙的内壁连接&#xff1f;创建异形建筑时&#xff0c;为了达到如图1所示的效果&#xff0c;该如何操作&#xff1b; 我们可以使用体量建模的方式来创建该类建筑&#xff0c;要点在于如将幕墙与曲面墙的内壁连接。具体方法如下&#xff1a; 一、创建…

ASP.NET Core+Element+SQL Server开发校园图书管理系统(一)

随着技术的进步&#xff0c;跨平台开发已经成为了标配&#xff0c;在此大背景下&#xff0c;ASP.NET Core也应运而生。本文主要基于ASP.NET CoreElementSql Server开发一个校园图书管理系统为例&#xff0c;简述基于MVC三层架构开发的常见知识点&#xff0c;仅供学习分享使用&a…