当前位置: 首页 > news >正文

并发编程 | CompletionService - 如何优雅地处理批量异步任务

引言

上一篇文章中,我们详细地介绍了 CompletableFuture,它是一种强大的并发工具,能帮助我们以声明式的方式处理异步任务。虽然 CompletableFuture 很强大,但它并不总是最适合所有场景的解决方案。
在这篇文章中,我们将介绍 Java 的 CompletionService,这是一种能处理批量异步任务并在完成时获取结果的并发工具。
CompletionService CompletableFuture 在很多方面都相似。它们都用于处理异步任务,并且都提供了获取任务完成结果的机制。然而,CompletionService 采用了更传统并发模型,它将生产者和消费者的角色更明确地分离开来。

回顾我们在上一篇文章:并发编程 | 从Future到CompletableFuture 中讨论的需求,我们需要查找并计算一系列旅行套餐的价格。我们使用 CompletableFuture 实现了这个需求,并且代码看起来很简洁明了。然而,事情都有两面性。有些人并不习惯这种写法,觉得CompletableFuture 的实现中存在大量的嵌套,会让代码难以阅读和理解。另外,我们的代码中有大量的函数式编程,这在一定程度上增加了对代码阅读的门槛,如果你不熟悉这种编程范式,代码可能会看起来很混乱。

有没有一种方法,既简洁的同时,又不回到Future的回调地狱陷阱中去?有,CompletionService 。来看下CompletionService 是怎么解决问题。


使用CompletionService 解决问题

如果我们用 CompletionService 来实现这个需求,会是什么样呢?我们来看下代码:

public List<TravelPackage> searchTravelPackages(SearchCondition searchCondition) throws InterruptedException, ExecutionException {ExecutorService executorService = Executors.newFixedThreadPool(10);CompletionService<List<TravelPackage>> completionService = new ExecutorCompletionService<>(executorService);List<Flight> flights = searchFlights(searchCondition);for (Flight flight : flights) {// 提交所有的任务completionService.submit(() -> {List<TravelPackage> travelPackagesForFlight = new ArrayList<>();List<Hotel> hotels = searchHotels(flight);for (Hotel hotel : hotels) {TravelPackage travelPackage = calculatePrice(flight, hotel);travelPackagesForFlight.add(travelPackage);}return travelPackagesForFlight;});}List<TravelPackage> allTravelPackages = new ArrayList<>();for (int i = 0; i < flights.size(); i++) {// 等待它们的完成Future<List<TravelPackage>> future = completionService.take();// 如果没完成,这里会阻塞List<TravelPackage> travelPackagesForFlight = future.get();allTravelPackages.addAll(travelPackagesForFlight);}executorService.shutdown();allTravelPackages.sort(Comparator.comparing(TravelPackage::getPrice));return allTravelPackages;
}

通过上面的代码,我们可以看到 CompletionService 提供了一个更传统的并发模型来处理异步任务。相比CompletableFuture 而言,我们的代码中没有复杂的嵌套,代码更加直观。

对初学者来说,这个模型会更容易理解,特别是对于那些不熟悉函数式编程的读者来说。
当然,作为老手的你(假如你弄懂了上篇文章,并实践完),如果你在使用CompletableFuture 过程中发现它嵌套太深太复杂,CompletionService 可能也是个不错的选择。


基于上述代码抽取CompletionService

我们把关键代码抽取出来并简化,就可以得到下面这段代码:

ExecutorService executor = Executors.newFixedThreadPool(4);
CompletionService<String> completionService = new ExecutorCompletionService<>(executor);long start = System.currentTimeMillis();
// 提交3个任务
completionService.submit(() -> {// 业务返回的实践可能不一样,模拟不一样的任务执行时间Thread.sleep(5000);return "任务1完成";
});
completionService.submit(() -> {// 业务返回的实践可能不一样,模拟不一样的任务执行时间Thread.sleep(3000);return "任务2完成";
});
completionService.submit(() -> {// 业务返回的实践可能不一样,模拟不一样的任务执行时间Thread.sleep(500);return "任务3完成";
});
completionService.submit(() -> {// 业务返回的实践可能不一样,模拟不一样的任务执行时间Thread.sleep(500);return "任务4完成";
});// 获取结果
for (int i = 0; i < 4; i++) {try {Future<String> future = completionService.take();// 如果没完成,这里会阻塞System.out.println(future.get());} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}
}
executor.shutdown();
long end = System.currentTimeMillis();
System.out.println("任务花费时间: " + (end - start) + " ms");

结合文中代码注释,我把它总结为一句口诀:批量提交,快速获取。

批量我知道啊,就是遍历呗,但是提交到那里去?快速获取是什么意思?别急,我们接着往下看。


使用ExecutorService 实现需求

在回答这个问题之前,我们先来看一下代码。我们先sumbit()一下…然后get()拿到数据…
嗯?这不是和之前ExecutorService 差不多吗?好像可以用它实现啊,你看代码:

public List<TravelPackage> searchTravelPackages(SearchCondition searchCondition) throws InterruptedException, ExecutionException {ExecutorService executorService = Executors.newFixedThreadPool(10);List<Flight> flights = searchFlights(searchCondition);List<Future<List<TravelPackage>>> futureList = new ArrayList<>();for (Flight flight : flights) {Future<List<TravelPackage>> future = executorService.submit(() -> {List<TravelPackage> travelPackagesForFlight = new ArrayList<>();List<Hotel> hotels = searchHotels(flight);for (Hotel hotel : hotels) {TravelPackage travelPackage = calculatePrice(flight, hotel);travelPackagesForFlight.add(travelPackage);}return travelPackagesForFlight;});futureList.add(future);}List<TravelPackage> allTravelPackages = new ArrayList<>();for (Future<List<TravelPackage>> future : futureList) {List<TravelPackage> travelPackagesForFlight = future.get();allTravelPackages.addAll(travelPackagesForFlight);}executorService.shutdown();allTravelPackages.sort(Comparator.comparing(TravelPackage::getPrice));return allTravelPackages;
}

看,是不是可以实现了。那CompletionService这玩意存在的意义是啥?我们继续往下看。


提交先后顺序 VS 任务完成快慢顺序

我们先把上面抽取出来的代码执行,结果如下:

任务3完成
任务4完成
任务2完成
任务1完成
任务花费时间: 5012 ms
Disconnected from the target VM, address: '127.0.0.1:10373', transport: 'socket'Process finished with exit code 0

然后,我们换成ExecutorService 执行,抽取的ExecutorService 代码如下:

ExecutorService executor = Executors.newFixedThreadPool(3);
ArrayList<Future<String>> futures = new ArrayList<>();
long start = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(4);futures.add(executor.submit(() -> {// 业务返回的实践可能不一样,模拟不一样的任务执行时间Thread.sleep(5000);latch.countDown();return "任务1完成";
}));
futures.add(executor.submit(() -> {// 业务返回的实践可能不一样,模拟不一样的任务执行时间Thread.sleep(3000);latch.countDown();return "任务2完成";
}));
futures.add(executor.submit(() -> {// 业务返回的实践可能不一样,模拟不一样的任务执行时间Thread.sleep(500);latch.countDown();return "任务3完成";
}));
futures.add(executor.submit(() -> {// 业务返回的实践可能不一样,模拟不一样的任务执行时间Thread.sleep(500);latch.countDown();return "任务4完成";
}));for (Future<String> future : futures) {try {// 如果没完成,这里会阻塞System.out.println(future.get());} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}
}
latch.await();
executor.shutdown();
long end = System.currentTimeMillis();
System.out.println("任务花费时间: " + (end - start) + " ms");

执行结果如下:

任务1完成
任务2完成
任务3完成
任务4完成
任务花费时间: 5007 ms
Disconnected from the target VM, address: '127.0.0.1:14882', transport: 'socket'Process finished with exit code 0

细心的你肯定可以看到它们执行结果上的差异。CompletionService 是按照任务时间的顺序消费的。好,搞懂了这个,我们就可以回答上面其中一个问题:

快速获取是什么?

CompletionService是按照任务的快慢,谁先执行完谁就先返回。可以看到上面示例代码的结果,任务3只需要500ms,所以任务3先返回。


CompletionService 的适用场景

既然CompletionService 可以按照任务快慢顺序来返回,我们来看下它适合哪些场景:

执行一组任务并处理结果

上面就是很好的例子,我们可以在任何任务完成后立即获取并处理其结果,以实现快速响应。提高程序的吞吐量(先执行完任务,就有多的线程空闲,可以响应更多任务)。

生产者-消费者模式

我们在最早的开篇说过,CompletionService可以天然地实现生产者-消费者模式。这个模式中,生产者线程负责批量提交任务,消费者线程负责获取并处理任务的结果,而且它也可以安全地在多个线程之间共享


新的问题又出现了,为什么又可以在多个线程之间共享?提交到那里去?快速获取是怎么做到的?以问题为导向,我们来分析下源码。

CompletionService源码分析

提交到那里去?为什么可以在多线程之间共享?

我们先看下构造函数中做了什么:

public ExecutorCompletionService(Executor executor) {if (executor == null)throw new NullPointerException();this.executor = executor;this.aes = (executor instanceof AbstractExecutorService) ?(AbstractExecutorService) executor : null;this.completionQueue = new LinkedBlockingQueue<Future<V>>();
}

ExecutorCompletionService使用了一个BlockingQueue来存储已完成的任务。因为,任务的提交ExecutorBlockingQueue都是线程安全的。所以多线程共享的数据竞争问题已经在内部解决了。

快速获取是怎么做到的?

我们可以看下submit()方法是怎么实现的。当你提交一个任务时,这个任务被封装在一个QueueingFuture对象中:

public Future<V> submit(Callable<V> task) {if (task == null) throw new NullPointerException();RunnableFuture<V> f = newTaskFor(task);executor.execute(new QueueingFuture(f));return f;
}

QueueingFuture重写了done()方法。当任务完成时,done()方法会被调用,QueueingFuture会将自己添加到completionQueue中:

private class QueueingFuture extends FutureTask<Void> {QueueingFuture(RunnableFuture<V> task) {super(task, null);this.task = task;}protected void done() { completionQueue.add(task); } //当任务完成时,将任务添加到队列中private final Future<V> task;
}

这样似乎就可以解释,快速获取的机制。完成的任务优先被放入BlockingQueue中按照完成顺序排队。
现在,我换一种表述,你看下是否正确:快的任务在消费的时候就会被排在队列前面先被消费,这样就形成一个任务完成快慢的顺序,第一个被消费到的任务一定是最快的。


第一个被消费到的任务一定是最快的吗?

从上面的代码测试示例结果来看, 确实如此。但是,我很遗憾的告诉你,这句话是错误的。
这句话的正确性是建立在任务数等于线程数的前提下。这就显得很鸡肋了,在在生产中很难达到这个效果,因为资源是稀缺的。当然,我们还是拿代码说话:

ExecutorService executor = Executors.newFixedThreadPool(3);CompletionService<String> completionService = new ExecutorCompletionService<>(executor);long start = System.currentTimeMillis();completionService.submit(() -> {// 业务返回的实践可能不一样,模拟不一样的任务执行时间Thread.sleep(5000);return "任务1完成";});completionService.submit(() -> {// 业务返回的实践可能不一样,模拟不一样的任务执行时间Thread.sleep(3000);return "任务2完成";});completionService.submit(() -> {// 业务返回的实践可能不一样,模拟不一样的任务执行时间Thread.sleep(6000);return "任务3完成";});completionService.submit(() -> {// 业务返回的实践可能不一样,模拟不一样的任务执行时间Thread.sleep(500);return "任务4完成";});for (int i = 0; i < 4; i++) {try {System.out.println(completionService.take().get());} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}}executor.shutdown();long end = System.currentTimeMillis();System.out.println("任务花费时间: " + (end - start) + " ms");

假如遵循执行快慢顺序,理想的状态应该是:4 -> 2 -> 1 -> 3;而结果却是:

Connected to the target VM, address: '127.0.0.1:5068', transport: 'socket'
任务2完成
任务4完成
任务1完成
任务3完成
任务花费时间: 6020 ms
Disconnected from the target VM, address: '127.0.0.1:5068', transport: 'socket'

这个结果也是意料之外,但在情理之中。因为线程总共只有3个,在1,2,3之间排序,任务顺序应该是2,1,3;然后当2执行完之后,1和3依然未执行完;这个时候4正好执行完。于是就插队到任务中。最终得到2,4,1,3的结果。
因此,我们可以说:在生产环境中,这个顺序是不可控的,除非你把线程设置为1;


CompletionService相关面试题

如何使用CompletionService处理一组任务并获取结果?

比较ExecutorService和CompletionService,它们有什么相同之处和不同之处?

在何种情况下,你会选择使用CompletionService而不是ExecutorService?

解释CompletionService是如何保证按任务完成顺序获取结果的

当一个任务被提交到CompletionService后,它的生命周期是怎样的?在任务执行过程中,CompletionService内部都发生了什么?
在使用CompletionService处理任务时,如果某个任务执行异常,应该如何处理?
如果我想取消CompletionService中的所有任务,应该如何做?
谈谈你对Java中的Executor,ExecutorService,CompletionService和Future之间关系的理解

看完上面的文章,你可以试着来回答了吗?


参考文献

  1. Java并发编程小册

总结

让我们一起回顾今天所学。首先,我引导你使用了CompletionService和ExecutorService来实现了先前复杂的需求。相较于CompletableFuture,它们可能显得更为传统,但也更易理解。然后,我们一起探索了CompletionService的存在意义。我们试图解答,既然ExecutorService已经足够应对需求,为什么还要有CompletionService这样的设计。为了揭示这个疑惑,我们深入到源码中,同时也纠正了一个错误观点,以帮助你对CompletionService有更深刻的理解。最后,我们通过面试题形式,来巩固和复习我们所学的知识。

相关文章:

并发编程 | CompletionService - 如何优雅地处理批量异步任务

引言 上一篇文章中&#xff0c;我们详细地介绍了 CompletableFuture&#xff0c;它是一种强大的并发工具&#xff0c;能帮助我们以声明式的方式处理异步任务。虽然 CompletableFuture 很强大&#xff0c;但它并不总是最适合所有场景的解决方案。 在这篇文章中&#xff0c;我们…...

医学案例|ROC曲线之面积对比

一、案例介绍 为评价CT和CT增强对肝癌的诊断效果&#xff0c;共检查了32例患者&#xff0c;每例患者分别用两种方法检查&#xff0c;由医生盲态按4个等级诊断&#xff0c;最后经手术病理检查确诊其中有16例患有肝癌&#xff0c;评价CT个CT增强对肝癌是有有诊断效果并且试着比较…...

Kotlin线程的基本用法

线程的基本用法 新建一个类继承自Thread&#xff0c;然后重写父类的run()方法 class MyThread : Thread() {override fun run() {// 编写具体的逻辑} }// 使用 MyThread().start()实现Runnable接口 class MyThread : Runnable {override fun run() {// 编写具体的逻辑} }// …...

2.03 PageHelper分页工具

步骤1&#xff1a;在application.yml中添加分页配置 # 分页插件配置 pagehelper:helperDialect: mysqlsupportMethodsArguments: true步骤2&#xff1a;在顶级工程pom文件下引入分页插件依赖 <!--5.PageHelper --> <dependency><groupId>com.github.pagehe…...

VUE中使用ElementUI组件的单选按钮el-radio-button实现第二点击时取消选择的功能

页面样式为&#xff1a; html 代码为&#xff1a; 日志等级&#xff1a; <el-radio-group v-model"logLevel"><el-radio-button label"DEBUG" click.native.prevent"changeLogLevel(DEBUG)">DEBUG</el-radio-button><el-r…...

瓴羊Quick BI:可视化大屏界面设计满足企业个性需求

大数据技术成为现阶段企业缩短与竞争对手之间差距的重要抓手&#xff0c;依托以瓴羊Quick BI为代表的工具开展内部数据处理分析工作&#xff0c;也成为诸多企业持续获取竞争优势的必由之路。早年间国内企业倾向于使用进口BI工具&#xff0c;但随着瓴羊Quick BI等一众国内数据处…...

617. 合并二叉树

题目 题解一&#xff1a;递归 /*** 递归* param root1* param root2* return*/public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {//结束条件if (root1 null) {return root2;} //结束条件if (root2 null) {return root1;}//两节点数值相加TreeNode me…...

【T1】存货成本异常、数量为零金额不为零的处理方法。

【问题描述】 使用T1飞跃专业版的过程中&#xff0c; 由于业务问题或者是操作问题&#xff0c; 经常会遇到某个商品成本异常不准确&#xff0c; 或者是遇到数量为0金额不为0的情况&#xff0c;需要将其成本调为0。 但是T1软件没有出入库调整单&#xff0c;并且结账无法针对数量…...

EtherNet IP转PROFINET网关连接西门子与欧姆龙方法

本文主要介绍了捷米特JM-PN-EIP&#xff08;EtherNet/IP转PROFINET&#xff09;网关西门子200智能PLC&#xff08;PROFINET&#xff09;和欧姆龙系统EtherNet/IP通信的配置过程。 1, 将 EDS 文件复制到欧姆龙软件的对应文件夹下 2, 首先添加捷米特JM-PN-EIP网关的全局变量&…...

低代码开发重要工具:jvs-flow(流程引擎)审批功能配置说明

流程引擎场景介绍 流程引擎基于一组节点与执行界面&#xff0c;通过人机交互的形式自动地执行和协调各个任务和活动。它可以实现任务的分配、协作、路由和跟踪。通过流程引擎&#xff0c;组织能够实现业务流程的优化、标准化和自动化&#xff0c;提高工作效率和质量。 在企业…...

[SQL挖掘机] - GROUP BY语句

介绍: group by 是 sql 中用于对结果集进行分组的关键字。通过使用 group by&#xff0c;可以根据一个或多个列的值将结果集中的行分组&#xff0c;并对每个分组应用某种聚合函数&#xff08;如 count、sum、avg 等&#xff09;以生成汇总信息。这样可以方便地对数据进行分类、…...

【ubuntu|内核】ubuntu 22.04修改内核为指定版本

every blog every motto: You can do more than you think. https://blog.csdn.net/weixin_39190382?typeblog 0. 前言 ubuntu 22.04 安装指定内核 1. 正文 查看已安装的内核镜像 dpkg --get-selections | grep linux-image1.1 安装指定版本的内核 安装镜像 sudo apt-g…...

Carla教程一:动力学模型到LQR

Carla教程一、动力学模型到LQR 从运动学模型和动力学模型到LQR 模型就是可以描述车辆运动规律的模型。车辆建模都是基于自行车模型的设定,也就是将四个轮子抽象为自行车一样的两个轮子来建模。 1、运动学模型 运动学模型是基于几何关系分析出来的,一般适用于低俗情况下,…...

IDE/mingw下动态库(.dll和.a文件)的生成和部署使用(对比MSVC下.dll和.lib)

文章目录 概述问题的产生基于mingw的DLL动态库基于mingw的EXE可执行程序Makefile文件中使用Qt库的\*.a文件mingw下的*.a 文件 和 *.dll 到底谁起作用小插曲 mingw 生成的 \*.a文件到底是什么为啥mingw的dll可用以编译链接过程转换为lib引导文件 概述 本文介绍了 QtCreator mi…...

点击加号添加新的输入框

实现如上图的效果 html部分&#xff1a; <el-form-item class"forminput" v-for"(item,index) in formdata.description" :key"index" :label"描述(index1)" prop"description"><el-input v-model"formdata…...

SQL AND OR 运算符

AND & OR 运算符用于基于一个以上的条件对记录进行过滤。 如果第一个条件和第二个条件都成立&#xff0c;则 AND 运算符显示一条记录。 如果第一个条件和第二个条件中只要有一个成立&#xff0c;则 OR 运算符显示一条记录。 下面是选自 "students" 表的数据&a…...

6、C++内存模型

原文&#xff1a; https://my.oschina.net/u/2516597/blog/805489 背景 C11开始支持多线程&#xff0c;其中提供了原子类型atomic, 和atomic关系比较密切的是memory_order&#xff0c;所有的内存模型都是指atomic类型 enum memory_order {memory_order_relaxed,memory_order…...

上海市青少年算法2023年1月月赛(丙组)

上海市青少年算法2023年1月月赛(丙组)T1 实验日志 题目描述 小爱正在完成一个物理实验,为期n天,其中第i天,小爱会记录ai条实验数据在实验日志中。 已知小爱的实验日志每一页最多纪录m条数据,每天做完实验后他都会将日志合上,第二天,他便从第一页开始依次翻页,直到找到…...

移动开发之Wifi列表获取功能

一、场景 业务需要通过App给设备配置无线网络连接&#xff0c;所以需要App获取附近的WiFi列表&#xff0c;并进行网络连接验证。 二、安卓端实现 1、阅读谷歌官网文档&#xff0c;关于Wifi 接口使用 https://developer.android.com/guide/topics/connectivity/wifi-scan?hl…...

MyBatisPlus - 实体类 的 常用注解

TableName(“表名”) 假设 表名是 book&#xff0c;实体类类名是 Book MyBatisPlus会进行自动映射 但如果 表名是 tab_book&#xff0c;实体类类名是 Book 那么MyBatisPlus就无法进行自动映射&#xff0c;需要我们使用 TableName注解 去指定实体类对应的表 如下 TableNa…...

测试微信模版消息推送

进入“开发接口管理”--“公众平台测试账号”&#xff0c;无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息&#xff1a; 关注测试号&#xff1a;扫二维码关注测试号。 发送模版消息&#xff1a; import requests da…...

Ubuntu系统下交叉编译openssl

一、参考资料 OpenSSL&&libcurl库的交叉编译 - hesetone - 博客园 二、准备工作 1. 编译环境 宿主机&#xff1a;Ubuntu 20.04.6 LTSHost&#xff1a;ARM32位交叉编译器&#xff1a;arm-linux-gnueabihf-gcc-11.1.0 2. 设置交叉编译工具链 在交叉编译之前&#x…...

电脑插入多块移动硬盘后经常出现卡顿和蓝屏

当电脑在插入多块移动硬盘后频繁出现卡顿和蓝屏问题时&#xff0c;可能涉及硬件资源冲突、驱动兼容性、供电不足或系统设置等多方面原因。以下是逐步排查和解决方案&#xff1a; 1. 检查电源供电问题 问题原因&#xff1a;多块移动硬盘同时运行可能导致USB接口供电不足&#x…...

什么是库存周转?如何用进销存系统提高库存周转率?

你可能听说过这样一句话&#xff1a; “利润不是赚出来的&#xff0c;是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业&#xff0c;很多企业看着销售不错&#xff0c;账上却没钱、利润也不见了&#xff0c;一翻库存才发现&#xff1a; 一堆卖不动的旧货…...

图表类系列各种样式PPT模版分享

图标图表系列PPT模版&#xff0c;柱状图PPT模版&#xff0c;线状图PPT模版&#xff0c;折线图PPT模版&#xff0c;饼状图PPT模版&#xff0c;雷达图PPT模版&#xff0c;树状图PPT模版 图表类系列各种样式PPT模版分享&#xff1a;图表系列PPT模板https://pan.quark.cn/s/20d40aa…...

dify打造数据可视化图表

一、概述 在日常工作和学习中&#xff0c;我们经常需要和数据打交道。无论是分析报告、项目展示&#xff0c;还是简单的数据洞察&#xff0c;一个清晰直观的图表&#xff0c;往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server&#xff0c;由蚂蚁集团 AntV 团队…...

让回归模型不再被异常值“带跑偏“,MSE和Cauchy损失函数在噪声数据环境下的实战对比

在机器学习的回归分析中&#xff0c;损失函数的选择对模型性能具有决定性影响。均方误差&#xff08;MSE&#xff09;作为经典的损失函数&#xff0c;在处理干净数据时表现优异&#xff0c;但在面对包含异常值的噪声数据时&#xff0c;其对大误差的二次惩罚机制往往导致模型参数…...

return this;返回的是谁

一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请&#xff0c;不同级别的经理有不同的审批权限&#xff1a; // 抽象处理者&#xff1a;审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...

【Android】Android 开发 ADB 常用指令

查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

day36-多路IO复用

一、基本概念 &#xff08;服务器多客户端模型&#xff09; 定义&#xff1a;单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力 作用&#xff1a;应用程序通常需要处理来自多条事件流中的事件&#xff0c;比如我现在用的电脑&#xff0c;需要同时处理键盘鼠标…...