Guava 之 EventBus

news2024/9/29 9:38:57

​​EvenBus​​​ 是 Guava 中 Pub/Sub 模式的轻量级实现。平时开发如果我们要实现自己的 Pub/Sub 模型,要写不少类,设计也挺复杂,对业务代码也有一定的侵入,但是在使用了 ​​EventBus​​ 之后就很方便了。

在 Pub/Sub 模式中有 push 和 pull 两种实现,比如 ActiveMQ 就是 push,Kafka 就是 pull,而 ​​EventBus​​​ 就是 push 这种方式,也可以把 ​​EventBus​​ 看成一个简易版的消息中间件吧。我个人感觉所谓的 Publisher 和 Subscriber 其实没有必要强制分的,当然也不是说不分,主要想表达的是 Publisher 也可以做 Subscriber 的事,同样的,Subscriber 也可以做 Publisher 的事。
在这里插入图片描述

先看一个简单的 Demo,看看 ​​EventBus​​ 是怎么玩的。

初识 Demo

两个 Listener:

/**
 * @author dongguabai
 * @date 2019-03-18 18:09
 * Listener1
 */
public class Listener1 {

    @Subscribe  //监听 参数为 String 的消息
    public void doSth(String info) {
        System.out.println("Listener1 接收到了消息:" + info);
    }
}
/**
 * @author dongguabai
 * @date 2019-03-18 18:09
 * Listener2
 */
public class Listener2 {

    @Subscribe  //监听 参数为 Date 的消息
    public void doSth(Date info) {
        System.out.println("Listener2 接收到了消息:" + info.toLocaleString());
    }
}

Source:

/**
 * @author dongguabai
 * @date 2019-03-18 18:15
 */
public class EventBusSource {

    public static void main(String[] args) {
        EventBus eventBus = new EventBus();
        eventBus.register(new Listener1());
        eventBus.register(new Listener2());
        eventBus.post("EventBus 发送的 String 消息");
        eventBus.post(new Date());
    }
}

运行结果:在这里插入图片描述

在上面的 Demo 中有两个 ​​Listener​​​,一个监听 ​​String​​​ 类型的事件消息,一个监听 ​​Date​​​ 类型的事件消息。这两个 ​​Listener​​​ 都被注册到了 ​​EventBus​​ 中,通过这个 Demo 非常简单的实现了一个 Pub/Sub 模型。

要注意的是 ​​EventBus​​​ 的 ​​post()​​​ 方法只能传入一个参数,如果有多个参数的需求那就只能自己根据需要进行封装了。这里是 ​​@Subscribe​​ 的官方注释,说的也很详细:

/**
 * Marks a method as an event subscriber.
 *
 * <p>The type of event will be indicated by the method's first (and only) parameter. If this annotation is applied to methods with zero parameters, or more than one parameter, the object containing the method will not be able to register for event delivery from the {@link EventBus}.
 *
 * <p>Unless also annotated with @{@link AllowConcurrentEvents}, event subscriber methods will be invoked serially by each event bus that they are registered with.
 *
 * @author Cliff Biffle
 * @since 10.0
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Beta
public @interface Subscribe {}

上面提到了 ​​@AllowConcurrentEvents​​,可以看看这个注解,说白了就是让 Subscribers 的 Method 是线程安全的,即串行执行:

/**
 * Marks an event subscriber method as being thread-safe. This annotation indicates that EventBus
 * may invoke the event subscriber simultaneously from multiple threads.
 *
 * <p>This does not mark the method, and so should be used in combination with {@link Subscribe}.
 *
 * @author Cliff Biffle
 * @since 10.0
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Beta
public @interface AllowConcurrentEvents {}

其实保证线程安全就是使用的 ​​synchronized​​​,可以看 ​​com.google.common.eventbus.Subscriber.SynchronizedSubscriber#invokeSubscriberMethod​​:

@Override
void invokeSubscriberMethod(Object event) throws InvocationTargetException {
  synchronized (this) {
    super.invokeSubscriberMethod(event);
  }
}

而且 EventBus 没有无法获取执行结果的返回值,EventBus 并未提供相应的方法获取执行结果:

在这里插入图片描述

DeadEvent

其实 ​​DeadEvent​​​ 源码很简单,它的具体使用场景我也不知道咋说,说白了,它可以帮你封装一下 ​​event​​​,让你给你可以获取 ​​EventBus​​ 的一些信息。官方注释是这么说的:

/**
 * Wraps an event that was posted, but which had no subscribers and thus could not be delivered.
 *
 * <p>Registering a DeadEvent subscriber is useful for debugging or logging, as it can detect misconfigurations in a system's event distribution.
 *
 * @author Cliff Biffle
 * @since 10.0
 */
@Beta
public class DeadEvent {

  private final Object source;
  private final Object event;
  ...
  ...
}

DeadEvent 就是对 ​​post()​​​ 方法中的 ​​event​​​ 的一个包装。在 ​​com.google.common.eventbus.EventBus#post​​ 中有这样一段描述:

* <p>If no subscribers have been subscribed for {@code event}'s class, and {@code event} is * not already a {@link DeadEvent}, it will be wrapped in a DeadEvent and reposted.

使用也很简单,就是 ​​@Subscribe​​​ 标注的方法的参数要是 ​​DeadEvent​​。

public class DeadEventListener {

    @Subscribe
    public void deadEventMethod(DeadEvent deadEvent){
        System.out.println("DeadEvent_Source_Class:"+deadEvent.getSource().getClass());
        System.out.println("DeadEvent_Source:"+deadEvent.getSource());
        System.out.println("DeadEvent_Event:"+deadEvent.getEvent());
    }
}
public class DeadEventEventBusSource {

    public static void main(String[] args) {
        EventBus eventBus = new EventBus();
        eventBus.register(new DeadEventListener());
        eventBus.post("EventBus 发送的 String 消息");
    }
}

运行结果:

DeadEvent_Source_Class:class com.google.common.eventbus.EventBus
DeadEvent_Source:EventBus{default}
DeadEvent_Event:EventBus 发送的 String 消息

可以看到 ​​DeadEvent​​​ 就是将 ​​event​​​ 进行了一层封装。运行结果第二行输出的 default 是因为 ​​EventBus​​ 默认名称为 default:

/** Creates a new EventBus named "default". */
public EventBus() {
  this("default");
}

异常处理

如果 ​​@Subscribe​​ 标注的方法出现了异常怎么办呢,这里先测试一下:

两个 Listener:

/**
 * @author dongguabai
 * @date 2019-03-18 18:09
 * ExceptionListener1
 */
public class ExceptionListener1 {

    @Subscribe  //监听 参数为 String 的消息
    public void exceptionMethod(String info) {
        System.out.println("ExceptionListener1 接收到了消息:" + info);
        int i = 1/0;
    }
}
/**
 * @author dongguabai
 * @date 2019-03-18 18:09
 * Listener2
 */
public class Listener2 {

    @Subscribe  //监听 参数为 Date 的消息
    public void doSth(Date info) {
        System.out.println("Listener2 接收到了消息:" + info.toLocaleString());
    }
}

Source:

public class ExceptionEventBusSource {

    public static void main(String[] args) {
        EventBus eventBus = new EventBus();
        eventBus.register(new ExceptionListener1());
        eventBus.register(new Listener2());
        eventBus.post("EventBus 发送的 String 消息");
        eventBus.post(new Date());
    }
}

运行结果:在这里插入图片描述

根据异常信息的提示去 ​​com.google.common.eventbus.Subscriber#invokeSubscriberMethod​​ 方法看看:

/**
 * Invokes the subscriber method. This method can be overridden to make the invocation
 * synchronized.
 */
@VisibleForTesting
void invokeSubscriberMethod(Object event) throws InvocationTargetException {
  try {
    method.invoke(target, checkNotNull(event));
  } catch (IllegalArgumentException e) {
    throw new Error("Method rejected target/argument: " + event, e);
  } catch (IllegalAccessException e) {
    throw new Error("Method became inaccessible: " + event, e);
  } catch (InvocationTargetException e) {
    if (e.getCause() instanceof Error) {
      throw (Error) e.getCause();
    }
    throw e;
  }
}

从这个方法也可以看出本质还是在通过反射调用相应的 subscribe 方法。其实如果再往上琢磨的话还可以看出这个方法是这么执行的:

/** Dispatches {@code event} to this subscriber using the proper executor. */
final void dispatchEvent(final Object event) {
  executor.execute(
      new Runnable() {
        @Override
        public void run() {
          try {
            invokeSubscriberMethod(event);
          } catch (InvocationTargetException e) {
            bus.handleSubscriberException(e.getCause(), context(event));
          }
        }
      });
}

看到 ​​executor​​​ 是不是就有一种眼前一亮的感觉了,就是使用线程池去执行的,额,不能这么说,只能说我们可以根据构造传入 ​​Executor​​​ 的实现,自定义方法执行的方式,当然也可以使用 JUC 中的线程池,这个在 ​​AsyncEventBus​​​ 中会进一步介绍。这里异常也使用了 ​​try…catch​​​ 处理,在 ​​catch​​​ 中调用了 ​​handleSubscriberException()​​ 方法:

/** Handles the given exception thrown by a subscriber with the given context. */
void handleSubscriberException(Throwable e, SubscriberExceptionContext context) {
  checkNotNull(e);
  checkNotNull(context);
  try {
    exceptionHandler.handleException(e, context);
  } catch (Throwable e2) {
    // if the handler threw an exception... well, just log it
    logger.log(
        Level.SEVERE,
        String.format(Locale.ROOT, "Exception %s thrown while handling exception: %s", e2, e),
        e2);
  }
}

很自然要去看看这个 ​​exceptionHandler​​ 是个啥东东,因为出现异常怎么处理就是它来操作的。

原来就是个接口,我们可以通过 ​​EventBus​​ 的构造传入:

public EventBus(SubscriberExceptionHandler exceptionHandler) {
  this(
      "default",
      MoreExecutors.directExecutor(),
      Dispatcher.perThreadDispatchQueue(),
      exceptionHandler);
}

这也是一种让代码可扩展的思路,是不是有一种好多源码都是这个套路的感觉,而且这里还使用了 ​​context​​ 去封装一些相应的参数:

/**
 * Context for an exception thrown by a subscriber.
 *
 * @since 16.0
 */
public class SubscriberExceptionContext {
  private final EventBus eventBus;
  private final Object event;
  private final Object subscriber;
  private final Method subscriberMethod;
}

自定义异常处理

既然可扩展,那我们就扩展一个来玩玩:

/**
 * @author dongguabai
 * @date 2019-04-01 13:21
 * 自定义异常处理
 */
public class ExceptionHandler implements SubscriberExceptionHandler {
    @Override
    public void handleException(Throwable exception, SubscriberExceptionContext context) {
        System.out.println("自定义异常处理....");
        System.out.println(exception.getLocalizedMessage());
        System.out.println("异常方法名称:"+context.getSubscriberMethod().getName());
        System.out.println("异常方法参数:"+context.getSubscriberMethod().getParameters()[0]);
    }
}

在构造 EventBus 的时候传入自定义的 Handler:

public class ExceptionEventBusSource {

    public static void main(String[] args) {
        //传入自定义异常处理 Handler
        EventBus eventBus = new EventBus(new ExceptionHandler());
        eventBus.register(new ExceptionListener1());
        eventBus.register(new Listener2());
        eventBus.post("EventBus 发送的 String 消息");
        eventBus.post(new Date());
    }
}

执行结果:

ExceptionListener1 接收到了消息:EventBus 发送的 String 消息
自定义异常处理…
/ by zero
异常方法名称:exceptionMethod
异常方法参数:java.lang.String info
Listener2 接收到了消息:2019-4-1 13:27:15

AsyncEventBus

​​AsyncEventBus​​​ 是 ​​EventBus​​ 的子类。看名称就能猜出可以异步执行 subscribe 方法。先看一个 Demo:

public class Listener1 {

    @Subscribe  //监听 参数为 String 的消息
    public void doSth0(String info) {
        System.out.println(LocalDateTime.now()+"   方法0 执行完毕");
    }



    @Subscribe  //监听 参数为 String 的消息
    public void doSth1(String info) {
        System.out.println(LocalDateTime.now()+"   方法1 执行开始");

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(LocalDateTime.now()+"   方法1 执行完毕");
    }

    @Subscribe  //监听 参数为 String 的消息
    public void doSth2(String info) {
        System.out.println(LocalDateTime.now()+"   方法2 执行开始");

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(LocalDateTime.now()+"   方法2 执行完毕");
    }

    @Subscribe  //监听 参数为 String 的消息
    public void doSth3(String info) {
        System.out.println(LocalDateTime.now()+"   方法3 执行开始");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(LocalDateTime.now()+"   方法3 执行完毕");
    }

    @Subscribe  //监听 参数为 String 的消息
    public void doSth4(String info) {
        System.out.println(LocalDateTime.now()+"   方法4 执行开始");
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(LocalDateTime.now()+"   方法4 执行完毕");
    }
}
public class EventBusSource {

    public static void main(String[] args) {
        EventBus eventBus = new AsyncEventBus(Executors.newFixedThreadPool(10));
        eventBus.register(new Listener1());
        eventBus.post("EventBus 发送的 String 消息");
    }
}

运行结果:

2019-04-01T16:35:56.223   方法3 执行开始
2019-04-01T16:35:56.223   方法0 执行完毕
2019-04-01T16:35:56.223   方法1 执行开始
2019-04-01T16:35:56.223   方法4 执行开始
2019-04-01T16:35:56.223   方法2 执行开始
2019-04-01T16:35:57.226   方法1 执行完毕
2019-04-01T16:35:58.226   方法2 执行完毕
2019-04-01T16:35:59.224   方法3 执行完毕
2019-04-01T16:36:00.225   方法4 执行完毕

可以看到的确是异步执行了。其实 ​​AsyncEventBus​​​ 和 ​​EventBus​​​ 所谓的异步和同步主要是跟 ​​executor​​​ 有关。从 ​​post()​​​ 方法一路进去到 ​​com.google.common.eventbus.Subscriber#dispatchEvent​​ 方法:

/** Dispatches {@code event} to this subscriber using the proper executor. */
final void dispatchEvent(final Object event) {
  executor.execute(
      new Runnable() {
        @Override
        public void run() {
          try {
            invokeSubscriberMethod(event);
          } catch (InvocationTargetException e) {
            bus.handleSubscriberException(e.getCause(), context(event));
          }
        }
      });
}

本质是通过 ​​executor​​​ 去执行的,那么这个 ​​executor​​ 是个啥呢:

/** Executor to use for dispatching events to this subscriber. */
private final Executor executor;

private Subscriber(EventBus bus, Object target, Method method) {
  this.bus = bus;
  this.target = checkNotNull(target);
  this.method = method;
  method.setAccessible(true);

  this.executor = bus.executor();
}

就是 ​​Subscriber​​​ 中的 ​​EventBus​​​ 的 ​​executor​​​,是 ​​Executor​​​ 接口。在同步的 ​​EventBus​​​ 中的 ​​executor​​ 是 Guava 自己提供的:

@GwtCompatible
enum DirectExecutor implements Executor {
  INSTANCE;

  @Override
  public void execute(Runnable command) {
    command.run();
  }

  @Override
  public String toString() {
    return "MoreExecutors.directExecutor()";
  }
}

说白了就是一个很普通的 ​​Executor​​ 实现。

在 ​​AsyncEventBus​​​ 中 ​​executor​​ 我们可以自行传入:

public AsyncEventBus(Executor executor) {
  super("default", executor, Dispatcher.legacyAsync(), LoggingHandler.INSTANCE);
}

其实这个地方设计就不太好,前面也分析过了所谓的同步和异步主要与 ​​executor​​​ 有关。如果我传入的是一个同步的 ​​executor​​​ 那怎么办呢,这里测试一下(这里偷个懒,直接用 ​​EventBus​​​ 中的同步的 ​​Executor​​ 实现):

public class EventBusSource {

    public static void main(String[] args) {
        AsyncEventBus eventBus = new AsyncEventBus(MoreExecutors.directExecutor());
        eventBus.register(new Listener1());
        eventBus.post("EventBus 发送的 String 消息");
    }
}

运行结果:

2019-04-01T17:13:23.158   方法4 执行开始
2019-04-01T17:13:27.162   方法4 执行完毕
2019-04-01T17:13:27.162   方法0 执行完毕
2019-04-01T17:13:27.162   方法1 执行开始
2019-04-01T17:13:28.166   方法1 执行完毕
2019-04-01T17:13:28.166   方法2 执行开始
2019-04-01T17:13:30.171   方法2 执行完毕
2019-04-01T17:13:30.171   方法3 执行开始
2019-04-01T17:13:33.175   方法3 执行完毕

可以发现居然同步执行了。这个地方也是设计不严谨的地方,估计作者本意是让我们传入一个线程池,如果是这样的话可以把 ​​executor​​​ 级别调低点,比如使用 ​​ThreadPoolExecutor​​。

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

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

相关文章

Java——《面试题——tomcat篇》

全文章节 Java——《面试题——基础篇》 Java——《面试题——JVM篇》 Java——《面试题——多线程&并发篇》 Java——《面试题——Spring篇》 Java——《面试题——SpringBoot篇》 Java——《面试题——MySQL篇》​​​​​​ Java——《面试题——SpringCloud》 Java——…

干货 | 智慧教育平台生成式人工智能应用的安全要求

以下内容整理自清华大学《数智安全与标准化》课程大作业期末报告同学的汇报内容。 第一部分&#xff1a;编制说明 标准制定的基本原则主要包括以下四个方面&#xff1a; 综合性&#xff1a;本标准全面漫盖了智慧教育平台ChatGPT安全保护的要求&#xff0c;以便用户参考&#xf…

Spring:Bean

Bean 概述配置方式自动装配继承与依赖作用域外部属性文件的使用 概述 Spring 容器负责管理依赖注入&#xff0c;它将被管理的对象都称为 bean 。我们通过 xml 文件配置方式进行对 bean 的声明和管理。 写法如下&#xff1a; <beans><bean id"bean的唯一标识符…

Scrapy框架--CrawlSpider (详解+例子)

目录 CrawlSpider 简介 基本运行 特性和概念 基本使用 创建CrawlSpider 运行 使用CrawlSpider中核心的2个类对象 Rule对象 LinkExtractors 作用 使用 查看效果-shell中验证 示例 注意 CrawlSpider 简介 CrawlSpider 是 Scrapy 框架提供的一个特殊的 Spider 类…

Jvm内存模型剖析优化-JVM(四)

上篇文章代码实例详解如何自定义双亲委派&#xff0c;主要实现ClassLoader&#xff0c;有两个方法&#xff0c;一个直接loadClass用父类的&#xff0c;如果想在破坏&#xff0c;则需要重写loadClass&#xff0c;一个findClass必须要重新&#xff0c;因为父类是空的&#xff0c;…

SpringBoot3之GraalVM之Linux详细安装及使用教程

Linux安装底层工具相关依赖 yum install -y gcc glibc-devel zlib-devel安装GraalVM JDK 《GraalVM官网下载》 找到最近的GraalVM Community Edition X.X.X点击Assets&#xff08;因为我的是SpringBoot3项目&#xff0c;起始JDK就要求17&#xff0c;所以我下载17&#xff09;下…

青少年机器人技术一级核心知识点:机械结构及模型(一)

随着科技的不断进步&#xff0c;机器人技术已经成为了一个重要的领域。在这个领域中&#xff0c;机械结构是机器人设计中至关重要的一部分&#xff0c;它决定了机器人的形态、运动方式和工作效率。对于青少年机器人爱好者来说&#xff0c;了解机械结构的基础知识&#xff0c;掌…

vim背景颜色设置

cd ~进入个人家目录下&#xff0c;vim .vimrc进入vimrc文件&#xff1a; 在主题设置部分对颜色背景进行设置&#xff0c;onedark表示黑色背景&#xff0c;default表示白色背景&#xff0c;按需设置即可&#xff01;

网络知识点-链路聚合

链路聚合&#xff08;英语&#xff1a;Link Aggregation&#xff09;是一个计算机网络术语&#xff0c;指将多个物理端口汇聚在一起&#xff0c;形成一个逻辑端口&#xff0c;以实现出/入流量吞吐量在各成员端口的负荷分担&#xff0c;交换机根据用户配置的端口负荷分担策略决定…

【数据结构】算法的时间和空间复杂度

目录 1.什么是算法&#xff1f; 1.1算法的复杂度 2.算法的时间复杂度 2.1 时间复杂度的概念 计算Func1中count语句总共执行了多少次 2.2 大O的渐进表示法 2.3常见时间复杂度计算举例 实例1:执行2N10次 实例2:执行MN次 实例3:执行了100000000次 实例4:计算strchr的时…

java jwt生成token并在网关设置全局过滤器进行token的校验并在给请求头设置参数及在微服务中解析参数

1、首先引入jjwt的依赖 <dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version> </dependency>2、编写生成token的工具类 package com.jjw.result.util;import com.jjw.res…

软考高级系统架构设计师(九) 作文模板-论设计模式及其应用(未完待续)

目录 掌握的知识点 创建型 结构型 行为型 掌握的知识点 设计模式分为哪3类 每一类包含哪些具体的设计模式 创建型 创建型模式是对对象实例化过程的抽象&#xff0c;他通过抽象类所定义的接口&#xff0c;封装了系统中对象如何创建、组合等信息。 创建型模式主要用于创建对…

【物联网】微信小程序接入阿里云物联网平台

微信小程序接入阿里云物联网平台 一 阿里云平台端 1.登录阿里云 阿里云物联网平台 点击进入公共实例&#xff0c;之前没有的点进去申请 2.点击产品&#xff0c;创建产品 3.产品名称自定义&#xff0c;按项目选择类型&#xff0c;节点类型选择之恋设备&#xff0c;联网方式W…

Linux下安装Redis的详细安装步骤

一.Redis安装 1.下载linux压缩包 【redis-5.0.5.tar.gz】 2.通过FlashFXP把压缩包传送到服务器 3.解压缩 tar -zxvf redis-5.0.5.tar.gz4.进入redis-5.0.5可以看到redis的配置文件redis.conf 5.基本的环境安装 使用gcc -v 命令查看gcc版本已经是4.8.5了&#xff0c;于是就…

ubuntu系统突然失去网络问题

修复ubuntu系统网络问题 1. 服务不存在&#xff1f;2. 修改配置&#xff0c;自动启动网络 每天都在用的ubuntu系统突然ssh连接不上&#xff0c;进系统ifconfig也不显示ip。当然也ping不通任何网页。 1. 服务不存在&#xff1f; 初步怀疑网络服务被关闭了&#xff0c;需要修改配…

【C6】数据类型/移植/对齐,内核中断,通过IO内存访问外设,PCI

文章目录 1.内核基础数据类型/移植性/数据对齐&#xff1a;页大小为PAGE_SIZE&#xff0c;不要假设4K&#xff0c;保证可移植性1.1 kdatasize.c&#xff1a;不同的架构&#xff08;x86_64,arm&#xff09;&#xff0c;基础类型大小可能不同&#xff0c;主要区别在long和指针1.2…

chatgpt赋能python:用Python访问数据库的SEO文章

用Python访问数据库的SEO文章 在当今互联网飞速发展的时代&#xff0c;数据处理和数据库技术的重要性不言而喻。在这些应用中&#xff0c;Python是使用最广泛和最受欢迎的编程语言之一。Python的简单和易学性使其成为理想的选项&#xff0c;可以通过Python来访问各种类型的数据…

荣耀90推出最新MagicOS7.1更新,增加控制中心功能

荣耀 90 系列机型推出了最新的 Magic OS 7.1更新&#xff0c;版本号为7.1.0.137 (C00E130R2P2)。该更新主要增加了控制中心功能&#xff0c;并对部分场景拍摄效果进行了优化。此外&#xff0c;该更新还提升了系统与部分三方应用的兼容性&#xff0c;以提高系统性能和稳定性。 …

选择最适合您自动化系统的控制方式

自动化系统可采用多种不同的控制方式&#xff0c;其中硬件控制和PLC&#xff08;可编程逻辑控制器&#xff09;是常见的选择。 刚好&#xff0c;我这里有上位机入门&#xff0c;学习线路图&#xff0c;各种项目&#xff0c;需要留个6。 硬件控制通常指使用专用硬件电路实现控…

C++3(sizeof和逗号运算符,类型转换)

1.sizeof的用法 逗号运算符 口诀&#xff1a;从左到右算&#xff0c;返回最右边的值 类型转换 如何实现的隐式类型转换&#xff1f; 先算右边的&#xff0c;右边的3&#xff08;int&#xff09;先提升为double &#xff0c;然后算得&#xff08;7.541&#xff08;double&#…