Java8新特性--CompletableFuture

并发与并行

Java 5并发库主要关注于异步任务的处理,它采用了这样一种模式,producer线程创建任务并且利用阻塞队列将其传递给任务的consumer。这种模型在Java 7和8中进一步发展,并且开始支持另外一种风格的任务执行,那就是将任务的数据集分解为子集,每个子集都可以由独立且同质的子任务来负责处理。

这种风格的基础库也就是fork/join框架,它允许程序员规定数据集该如何进行分割,并且支持将子任务提交到默认的标准线程池中,也就是“通用的”ForkJoinPool。(在本文中,非全限定的类和接口名指的都是java.util.concurrent包中的类型。)在Java 8中,fork/join并行功能借助并行流的机制变得更加具有可用性。但是,不是所有的问题都适合这种风格的并行处理:所处理的元素必须是独立的,数据集要足够大,并且在并行加速方面,每个元素的处理成本要足够高,这样才能补偿建立fork/join框架所消耗的成本。

同时,Java 8在并行流方面的革新得到了广泛的关注,这导致大家忽略了并发库中一项新增的重要功能,那就是CompletableFuture<T>类。本文将会探讨CompletableFuture类,有一些系统会依赖于不同类型的异步执行任务,本文将会阐述该类为什么会对这种类型的系统如此重要,并介绍了它是如何补充fork/join风格的并行机制和并行流的。

页面渲染器

我们的起始点将是“Java Concurrency in Practice”(JCiP)一书中的样例,这个样例非常经典地阐述了Java 5中的并发工具类。在JCiP的第6.3节中,Brian Goetz探讨了如何开发一个Web页面的渲染器,对于每个页面来说,它的任务就是渲染文本,并下载和渲染图片。图片的下载会耗费较长的时间,在这段时间内CPU无事可做,只能等待。所以,在渲染页面时,一个很明显的策略就是首先初始化所有图片的下载,然后利用它们完成之前的这段时间渲染页面文本,最后渲染下载的图片。

在JCiP中,第一版本的页面渲染器使用了Future的理念,这个接口暴露了一些方法,允许客户端监控任务的执行进度,这里的任务是在一个不同的进程中执行的。在程序清单1中,Callable代表了下载页面中所有图片的任务,它被提交到了Executor中,然后返回一个Future对象,通过它就能询问下载任务的状态。当主线程渲染完页面的文本后,会调用Future.get方法,这个方法会一直阻塞直到所有下载的结果均可用为止,在本例中这个结果是以List<ImageData>的形式来表示的。这种方式一个很明显的缺点在于下载任务的粗粒度性,在所有的图片下载完成之前,我们一张图片也不能渲染。接下来,我们看一下如何缓解这个问题。

public void renderPage(CharSequence source) {
List<ImageInfo> info = scanForImageInfo(source);
//创建Callable,它代表了下载所有的图片
final Callable<List<ImageData>> task = () ->info.stream().map(ImageInfo::downloadImage).collect(Collectors.toList());// 将下载任务提交到executorFuture<List<ImageData>> images = executor.submit(task);// renderText(source);
try {// 获得所有下载的图片(在所有图片可用之前会一直阻塞)final List<ImageData> imageDatas = images.get();// 渲染图片imageDatas.forEach(this::renderImage);
} catch (InterruptedException e) {// 重新维护线程的中断状态Thread.currentThread().interrupt();// 我们不需要结果,所以取消任务images.cancel(true);
} catch (ExecutionException e) {throw launderThrowable(e.getCause()); }
}

程序清单1:使用Future等待所有的图片下载完成

为了让这个样例及其后面的变种易于管理,这里有一个前提条件:我们假设类型ImageInfo(简单来讲,就是一个URL)和ImageData(图片的二进制数据)以及方法scanForImageInfo、downloadImage、renderText、renderImage、launderThrowableImageInfo.downloadImage都已经存在了。实例变量executor是通过ExecutorService类型声明的并进行了恰当的初始化。在本文中,我将JCiP中最初的样例利用Java 8 lambda表达式和流进行了现代化。

在这段代码中,必须要等待所有下载都完成的原因在于,它使用Future接口来代表下载任务,作为异步执行的任务模型,它有很大的局限性。Future允许客户端询问任务执行的结果,如果必要的话,将会产生阻塞,另外还可以询问任务的状态,判断它已经完成还是被取消了。但是,Future本身并不能提供回调方法,假设能够这样做的话,当每个图片下载完成的时候,就能通知页面的渲染线程了。

程序清单2改善了之前样例中粒度较粗的问题,它将页面下载的任务提交到了CompletionService类中,这个类的polltake方法会产生对应的Future实例,这些实例是按照任务完成的顺序排列的,而不是像程序清单1那样任务是按照提交的顺序处理的。在ExecutorCompletionService接口的平台实现中,为了实现该功能,每项任务都会包装在一个FutureTask中,FutureTaskFuture的一个实现,它允许提供完成时的回调。Future的回调行为是在ExecutorCompletionService中创建的,完成的任务会封装到一个队列中,供客户端询问时使用。

 public void renderPage(CharSequence source) { List<ImageInfo> info = scanForImageInfo(source); CompletionService<ImageData> completionService = new ExecutorCompletionService<>(executor); // 将每个下载任务提交到completion service中info.forEach(imageInfo -> completionService.submit(imageInfo::downloadImage)); renderText(source); // 当每个RunnableFuture可用时(并且我们也准备处理它的时候),// 将它们检索出来 for (int t = 0; t < info.size(); t++) { Future<ImageData> imageFuture = completionService.take(); renderImage(imageFuture.get()); } }

程序清单2:借助CompletionService,当图片可用时立即将其渲染出来(为了保持简洁性,省略掉了中断和错误处理的代码)

CompletableFuture简介

程序清单2代表了Java 5所能达到的水准,不过2014年之后,在Java中,编写异步系统的表现性得到了巨大的提升,这是通过引入CompletableFuture (CF)类实现的。这个类是Future的实现,它能够将回调放到与任务不同的线程中执行,也能将回调作为继续执行的同步函数,在与任务相同的线程中执行。它避免了传统回调最大的问题,那就是能够将控制流分离到不同的事件处理器中,而这是通过允许CF实例与回调方法进行组合形成新的CF来实现的。

作为样例,可以参考thenAccept方法,它接受一个 Consumer(用户提供的且没有返回值的函数)并返回一个新的CF。这个新CF所能达到的效果就是在最初CF完成时所得到的结果上,运用Consumer。与很多其他的CF方法类似,thenAccept有两个变种形式,在第一个中,Consumer会由通用fork/join池中的某一个线程来执行;在第二个中,它会由Executor中的某一个线程来负责执行,而Executor是我们在调用时提供的。这形成了三种重载形式:同步运行、在ForkJoinPool中异步运行以及在调用时所提供的线程池中异步运行,CompletableFuture中有近60个方法,上述的这三种重载形式占了绝大多数。

如下是thenAccept的一个样例,借助它重新实现了页面渲染器的功能:

public void renderPage(CharSequence source) { List<ImageInfo> info = scanForImageInfo(source); info.forEach(imageInfo -> CompletableFuture .supplyAsync(imageInfo::downloadImage) .thenAccept(this::renderImage)); renderText(source); }

程序清单3:使用CompletableFuture来实现页面渲染功能

尽管程序清单3比前面的形式更加简洁,但是我们需要练习一下才能更好地阅读它。工厂方法supplyAsync返回一个新的CF,它会在通用的 ForkJoinPool中运行指定的Supplier,完成时,Supplier的结果将会作为CF的结果。方法thenAccept会返回一个新的CF,它将会执行指定的Consumer,在本例中也就是渲染给定的图片,即supplyAsync方法所产生的CF的结果。

需要澄清的是,thenAccept并不是将CF与函数组合起来的唯一方式。将CF与函数组合起来可以接受如下的参数:

  • 应用于CF操作结果的函数。此时,可以采用的方法包括:
    • thenCompose:针对返回值为CompletableFuture的函数;
    • thenApply:针对返回值为其他类型的函数;
    • thenAccept:针对返回值为void的函数;
  • Runnable。通过thenRun方法,可以接受Runnable参数;
  • 函数在处理的过程中,可能正常结束也可能异常退出。CF能够通过方法来分别组合这两种情况:
    • handle,针对接受一个值和一个Throwable,并有返回值的函数;
    • whenComplete,针对接受一个值和一个Throwable,并返回void的函数。

扩展页面渲染器

扩展该样例能够阐述CompletableFuture的其他特性。比如,当图片下载超时或失败时,我们想使用一个图标作为可见的指示器。CF暴露了一个名为get(long, TimeUnit)的方法,如果 CF在指定的时间内没有完成的话,将会抛出TimeoutException异常。我们可以使用它来定义一个函数,这个函数会将 ImageInfo转换为ImageData (程序清单4)。

Function<ImageInfo, ImageData> infoToData = imageInfo -> { CompletableFuture<ImageData> imageDataFuture = CompletableFuture.supplyAsync(imageInfo::downloadImage, executor); try { return imageDataFuture.get(5, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); imageDataFuture.cancel(true); return ImageData.createIcon(e); } catch (ExecutionException e) { throw launderThrowable(e.getCause()); } catch (TimeoutException e) { return ImageData.createIcon(e); } 
}

程序清单4:使用CompletableFuture.get来实现超时

现在,页面可以通过连续调用infoToData来进行渲染。其中每个调用都会同步返回一个下载的图片,所以要并行下载的话,需要为它们各自创建一个新的异步任务。要实现这一功能,合适的工厂方法是CompletableFuture.runAsync(),它与supplyAsync类似,但是接受的参数是Runnable而不是Supplier

public void renderPage(CharSequence source) throws InterruptedException { List<ImageInfo> info = scanForImageInfo(source); info.forEach(imageInfo -> CompletableFuture.runAsync(() -> renderImage(infoToData.apply(imageInfo)), executor)); 
}

现在,我们考虑进一步的需求,当所有的请求完成或超时后,在页面上显示一个指示器,如果对应的所有CompletableFuture都从join方法中返回,就能表示出现了这种场景。静态方法allOf就是为这种需求而提供的,它能够创建一个返回值为空的CompletableFuture,当其所有的组件均完成时,它也会达到完成状态。(join方法通常用来返回某个CF的结果,为了查看allOf方法所组合起来的所有CF的结果,必须要对其进行单独地查询。)

public void renderPage(CharSequence source) { List<ImageInfo> info = scanForImageInfo(source); CompletableFuture[] cfs = info.stream() .map(ii -> CompletableFuture.runAsync( () -> renderImage(mapper.apply(ii)), executor)) .toArray(CompletableFuture[]::new); CompletableFuture.allOf(cfs).join(); renderImage(ImageData.createDoneIcon()); }

联合多个CompletableFuture

另外一组方法允许将多个CF联合在一起。我们已经看见过静态方法allOf,当其所有的组件均完成时,它就会处于完成状态,与之对应的方法也就是anyOf,返回值同样是void,当其任意一个组件完成时,它就会完成。除了这两个方法以外,这个组中其他的方法都是实例方法,它们能够将receiver按照某种方式与另外一个CF联合在一起,然后将结果传递到给定的函数中。

为了展现它们是如何运行的,我们扩展一下JCiP中的另一个例子,这是一个旅行预订的门户,我们将互相关联的订购过程记录在TripPlan对象中,它包含了总价以及所使用服务供应商的列表:

 interface TripPlan { List<ServiceSupplier> getSuppliers(); int getPrice(); TripPlan combine(TripPlan); }

ServiceSupplier(比如说某个航线或酒店)能够创建一个TripPlan:(当然,在现实中,ServiceSupplier.createPlan 将会接受参数,来反映对应的目的地、旅行等级等信息。)

interface ServiceSupplier { TripPlan createPlan(); String getAlliance();       // 稍后使用 
}

为了选择最佳的旅行计划,需要查询每个服务供应商为我们的旅行所给定的规划,然后使用Comparator来对比每个规划结果,这个Comparator反映了我们的选择标准(在本例中,只是简单的选择价格最低者):

TripPlan selectBestTripPlan(List<ServiceSupplier> serviceList) { List<CompletableFuture<TripPlan>> tripPlanFutures = serviceList.stream() .map(svc -> CompletableFuture.supplyAsync(svc::createPlan, executor)) .collect(toList()); return tripPlanFutures.stream() .min(Comparator.comparing(cf -> cf.join().getPrice())) .get().join(); 
}

请注意中间的collect操作,在流处理里面,由于中间操作的延迟性(laziness of intermediate operation),它就变得非常必要了。如果没有它的话,流的终止操作(terminal operation)将会是min,它如果要执行的话,首先需要针对tripPlanFutures的每个元素执行join操作。如上述的代码所示,我们并没有这样做,终止操作是collect,它会将map操作所形成的CF值累积起来,这个过程中没有阻塞,因此允许底层的任务并发执行。

如果获取航线和酒店最佳旅行计划的任务是独立的,那么我们会希望它们能够同时初始化,就像前文所述的图片下载一样。要将两个CF按照这种方式联合在一起,我们需要使用CompletableFuture.thenCombine方法,它会并行地执行receiver以及所提供的CF,然后将它们的结果使用给定的函数组合起来(在这里,假设变量 airlineshotels和(稍后使用的)cars都是以List<TravelService> 类型进行声明的,并且已经进行了恰当的初始化):

CompletableFuture .supplyAsync(() -> selectBestTripPlan(airlines)) .thenCombine( CompletableFuture.supplyAsync(() -> selectBestTripPlan(hotels)), TripPlan::combine); 

对这个样例进行扩展,我们将会学到更多的内容。假设每个服务供应商都属于某一个旅行联盟(travel alliance),通过String类型的属性alliance来表示。在独立订购完航线和酒店后,我们将会确定它们是否属于同一个联盟,如果是的话,那么只有属于同一联盟的租车服务,才在我们的考虑范围之内:

  private TripPlan addCarHire(TripPlan p) { List<String> alliances = p.getSuppliers().stream() .map(ServiceSupplier::getAlliance) .distinct() .collect(toList()); if (alliances.size() == 1) { return p.combine(selectBestTripPlan(cars, alliances.get(0))); } else { return p.combine(selectBestTripPlan(cars)); } }

selectBestTripPlan方法新的重载形式将会接受一个String类型作为偏爱的联盟,如果这个值存在的话,会使用它来过滤流中的服务:

  private TripPlan selectBestTripPlan( List<ServiceSupplier> serviceList, String favoredAlliance) { List<CompletableFuture<TripPlan>> tripPlanFutures = serviceList.stream() .filter(ts -> favoredAlliance == null || ts.getAlliance().equals(favoredAlliance)) .map(svc -> CompletableFuture.supplyAsync(svc::createPlan, executor)) .collect(toList()); ... }

在本例中,选择租车服务的CF要依赖于航班和酒店预订任务组合所形成的CF。只有航班和酒店都预订之后,它才能完成。实现这种关联关系的方法就是thenCompose

CompletableFuture.supplyAsync(() -> selectBestTripPlan(airlines)) .thenCombine( CompletableFuture.supplyAsync(() -> selectBestTripPlan(hotels)), TripPlan::combine) .thenCompose(p -> CompletableFuture.supplyAsync(() -> addCarHire(p)));

预订航班和酒店联合形成的CF会执行,并且它的结果,也就是联合后的TripPlan,将会作为thenCompose函数参数的输入。结果形成的CF非常简洁地封装了不同异步服务之间的依赖关系。这段代码如此简洁的原因在于,尽管thenCompose联合了两个CF,但是它所返回的并不是我们预期的CompletableFuture<CompletableFuture<TripPlan>>,而是CompletableFuture<TripPlan>。所以,不管在创建CF的时候使用了多少层级的组合,它并不是嵌套的,而是扁平的,要获取它的结果只需要一步操作。这是monad“绑定(bind)”操作(这个名称来源于Haskell)的特性,CF就是这种monad,并且阐明了monad一些非常积极的特征:比如,在本例中,我们能够按照函数式的形式进行编写,如果没有这项功能的话,就需要在各个回调中非常繁琐地显式编写任务定义。

thenCombine方法只是将两个CF联合起来的方法之一,其他的方法包括:

  • thenAcceptBoth:与thenCombine类似,但是它接受一个返回值为void的函数;
  • runAfterBoth:接受一个 Runnable,在两个CF都完成后执行;
  • applyToEither:接受一个一元函数(unary function),会将首先完成的CF的结果提供给它;
  • acceptEither:与applyToEither类似,接受一个一元函数,但是结果为void;
  • runAfterEither:接受一个Runnable,在其中一个CF完成后就执行。

结论

我们不可能在一篇短文中,完整地阐述像CompletableFuture这样的API,但是我希望这里的样例能够让你对它所能实现的并发编程形式有一个直观印象。将CompletableFuture与其他CompletableFuture组合,以及与其他函数组合,能够为多项任务构建类似流水线的方案,这样能够控制同步和异步执行以及它们之间的依赖。你想更加详细了解的内容可能会包括异常处理、选择和配置executor的实际经验以及设计异步API所面临的挑战。

我希望已经解释清楚了Java 8所提供的两种异步编程风格之间的联系。在使用fork/join并行机制(包括并行流)的场景中,能够非常高效的将工作内容进行跨核心分发。但是,它的适用条件却非常有限:数据集很大并且能够高效地分割,对某个数据元素的操作与其他元素是(相对)独立的,这些操作的成本应该是比较高昂的,并且应该是CPU密集型的。如果这些条件无法满足的话,尤其是如果你的任务会花费很多时间阻塞在I/O或网络请求上的话,那么 CompletableFuture是更好的替代方案。作为Java程序员,我们非常幸运地有这样一个平台库,它将这些补充的方式集成在了一起。

转载于:https://www.cnblogs.com/kexianting/p/8692437.html

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

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

相关文章

用 MAUI 在Windows 和 Linux 绘制 PPT 图表

我在做一个图表工具软件&#xff0c;这个软件使用 MAUI 开发。我的需求是图表的内容需要和 PPT 的图表对接&#xff0c;需要用到 OpenXML 解析 PPT 内容&#xff0c;读取到 PPT 图表元素的内容&#xff0c;接着使用 MAUI 渲染层绘制图表元素。图表工具软件需要在 Windows 平台和…

聊聊接口性能优化的11个小技巧

前言 接口性能优化对于从事后端开发的同学来说&#xff0c;肯定再熟悉不过了&#xff0c;因为它是一个跟开发语言无关的公共问题。 该问题说简单也简单&#xff0c;说复杂也复杂。 有时候&#xff0c;只需加个索引就能解决问题。 有时候&#xff0c;需要做代码重构。 有时…

Java中ArrayList,LinkedList,Vector三者的异同点及其使用场景和ArrayList的一些常用方法

相同点&#xff1a;三者存储的都是有序&#xff0c;可重复的数据。 异&#xff1a; ①&#xff1a;ArrayList底层存储类型是Object数组&#xff0c;而LinkedList底层是双向链表 ②&#xff1a;ArrayList和Vector调用创建空参构造器创建对象时&#xff0c;默认的size是10&…

第二百四十六节,Bootstrap弹出框和警告框插件

Bootstrap弹出框和警告框插件 学习要点&#xff1a; 1.弹出框 2.警告框 本节课我们主要学习一下 Bootstrap 中的弹出框和警告框插件。 一&#xff0e;弹出框 弹出框即点击一个元素弹出一个包含标题和内容的容器。 基本用法 注意&#xff1a;必须在js结合popover()方法使用 da…

Intellij IDEA2017 的控制台里不识别maven命令问题处理

2019独角兽企业重金招聘Python工程师标准>>> cmd里运行 mvn -v可以显示出maven的版本信息&#xff0c;可是在IDEA的控制台里却提示不识别maven命令&#xff0c;此情况以管理员的身份运行IDEA即可。 转载于:https://my.oschina.net/u/2364025/blog/1788797

使用IDEA 提交代码到svn

2019独角兽企业重金招聘Python工程师标准>>> 新手第一次使用教程&#xff1a; 一、安装svn TortoiseSVN是个客户端&#xff0c;需要安装VisualSVN服务端。 二、IDEA配置&#xff08;Ctrl alt S&#xff09; 需要配置服务端svn.exe文件。 三、上传代码 svn路径&…

如何在 BackgroundService 获取 ASP.NET Core 启动地址

前言上次&#xff0c;我们介绍了《如何获取 ASP.NET Core 启动地址》。但是&#xff0c;如果要在 BackgroundService 中获取启动地址可不那么容易&#xff0c;因为 BackgroundService 在 app 启动前就开始执行了:var builder WebApplication.CreateBuilder(args); builder.Ser…

016-Spring Boot JDBC

一、数据源装配 通过查看代码可知&#xff0c;默认已装配了数据源和JdbcTemplate System.out.println(context.getBean(DataSource.class)); System.out.println(context.getBean(JdbcTemplate.class)); 1.1、环境搭建 主要是pom引用&#xff1a;spring-boot-starter-jdbc、增加…

分库分表和 NewSQL 到底怎么选?

文章来源&#xff1a;【公众号&#xff1a;CoderW】 目录 背景 分表 分库 分库分表的成本 NewSQL NewSQL 平滑接入方案 NewSQL 真的有那么好吗&#xff1f; NewSQL 的应用 分库分表和 NewSQL 到底怎么选&#xff1f; 背景 曾几何时&#xff0c;“并发高就分库&#xff…

jQuery/javascript实现简单网页计算器

1 <html>2 <head>3 <meta charset"utf-8">4 <title>jQuery实现</title>5 <script src"jquery.js"></script>6 7 <style type"text/css">8 table{background-color:pink;width:300px;height…

雷军招人反被3句话问懵:当我在面试牛人的时候,牛人也在面试我

来 源&#xff5c;环球人力资源智库&#xff08;GHRlib&#xff09; 作 者&#xff5c;Black “你做过手机吗&#xff1f;” “没做过。” “你认识中移动老总王建宙吗&#xff1f;” “不认识。” “你认识富士康老板郭台铭吗&#xff1f;” “我认识他&#xff0c;他不认识我…

C# 11 中的 required members

C# 11 中的 required membersIntro在 C# 11 中引入了一个新的特性 —— Required Members&#xff0c;引入了一个新的 required 关键词&#xff0c;可以用来表示字段或者属性在类型初始化的时候必须要进行初始化&#xff0c;这一特性也进一步的改进了可空引用类型的用法。Sampl…

互联网大佬简史:马云/雷军/罗永浩/刘强东...

燃财经&#xff08;ID:rancaijing&#xff09;原创 作者 | 杜枫 编辑 | 魏佳中国互联网的发展&#xff0c;是一部由大佬撑起的奋斗史&#xff0c;也是一部由大佬主演的打脸史。和传统行业不同&#xff0c;互联网行业日新月异&#xff0c;从业者趋于年轻。马云唱起了摇滚&#x…

Windows 11 新版 22621.575 和 22622.575 推送:照片、URL、文件资源管理器

面向 Beta 频道的 Windows 预览体验成员&#xff0c;微软推送了 Windows 11 预览版 Build 22621.575 和 22622.575。 目前 Beta 频道 Windows 11 预览版分为两组进行测试&#xff0c;通过两组 Windows 预览体验成员的使用数据和反馈&#xff0c;以更好的测试新功能的可靠性。Wi…

linux mysql5.6 安装

2019独角兽企业重金招聘Python工程师标准>>> 1、gcc yum install gcc gcc-c ncurses-devel perl 2、cmake安装 wget http://www.cmake.org/files/v2.8/cmake-2.8.10.2.tar.gz tar -xzvf cmake-2.8.10.2.tar.gz cd cmake-2.8.10.2 ./bootstrap ; make ; make insta…

Python常用的12个GUI框架

Graphical User Interface&#xff0c;简称 GUI&#xff0c;又称图形化用户接口&#xff0c;所谓的GUI编程&#xff0c;指的是用户不需要输入代码指令&#xff0c;只通过图形界面的交互就可以操作软件功能。 1.Tkinter 一个轻量级的跨平台图形用户界面&#xff08;GUI&#xff…

PHP下操作Linux消息队列完成进程间通信的方法

2019独角兽企业重金招聘Python工程师标准>>> 来源:http://www.jb51.net/article/24353.htm 关于Linux系统进程通信的概念及实现可查看&#xff1a;http://www.ibm.com/developerworks/cn/linux/l-ipc/   关于Linux系统消息队列的概念及实现可查看&#xff1a;htt…

.NET 7 发布的最后一个预览版Preview 7, 下个月发布RC

微软在2022年8月9日 发布了.NET 7 Preview 7[1]&#xff0c;这是它在11月10日 RTM 之前进入发布候选阶段之前的最后预览版。预览版 7 已在 Visual Studio 17.4 预览版 1 中进行了测试&#xff0c;该预览版也于也与 VS 2022 v17.3 版本一起发布。对于预览版7&#xff0c;开发团队…

2022年全球职业教育行业发展报告

职业教育丨研究报告 核心摘要&#xff1a; 职业教育是职业学校教育与职业培训组成的有机整体&#xff0c;行业参与者除教育培训机构与受训学生外&#xff0c;还涉及企业雇主、行业协会、政府等多方&#xff0c;各群体共同构成密不可分的产业生态。 宏观而言&#xff0c;职业…

实战Cacti网络监控(1)——基础安装配置

实验环境&#xff1a; 物理主机 redhat7.0 内核版本 3.10.0-123.el7.x86_64 虚拟机 redhat6.5 内核版本 2.6.32-431.el6.x86_64 server10.example.com 172.25.254.10 所需软件包&#xff1a; cacti-0.8.8h.tar.g…