通过 JNI 实现 Java 与 Rust 的 Channel 消息传递

做纯粹的自己。“你要搞清楚自己人生的剧本——不是父母的续集,不是子女的前传,更不是朋友的外篇。对待生命你不妨再大胆一点,因为你好歹要失去它。如果这世上真有奇迹,那只是努力的另一个名字”。

一、crossbeam_channel

参考 crossbeam_channel - Rust

crossbeam_channel 是一个多生产者多消费者通道,用于消息传递,它是std::sync::mpsc的替代品,具有更多的功能和更好的性能。

二、Channel 类型

通道可以使用两个函数创建:

  1. bounded 函数创建一个容量有限的信道,即一个信道一次可以容纳的消息数量是有限制的。
  2. unbounded 函数创建一个容量无界的信道,即它一次可以容纳任意数量的消息。

这两个函数都返回一个发送方 Sender 和一个接收方 Receiver,它们代表通道的相反两端。

创建一个有界 Channel:

use crossbeam_channel::bounded;

// Create a channel that can hold at most 5 messages at a time.
let (s, r) = bounded(5);

// Can send only 5 messages without blocking.
for i in 0..5 {
    s.send(i).unwrap();
}

// Another call to `send` would block because the channel is full.
// s.send(5).unwrap();

创建一个无界 Channel:

use crossbeam_channel::unbounded;

// Create an unbounded channel.
let (s, r) = unbounded();

// Can send any number of messages into the channel without blocking.
for i in 0..1000 {
    s.send(i).unwrap();
}

三、通过 JNI 使用 Channel

Java 端可以通过 JNI 调用 getSender 获取发送端指针,调用 sendMessage 发送消息到 Rust 中的处理线程,由 Rust 负责处理核心逻辑。

1、新建一个 Rust 库项目

cargo new rust_jni_channel_test --lib

添加依赖包, 

# Cargo.toml

[dependencies]
jni = "0.21.1"
lazy_static = "1.5.0"
crossbeam-channel = "0.5.13"
#log = "0.4"
#env_logger = "0.11"

[lib]
crate_type = ["cdylib"]

实现 JNI 模块函数, 

// lib.rs

#[macro_use]
extern crate lazy_static;

use jni::objects::{JClass, JObject};
use jni::sys::{jlong, jobject};
use jni::JNIEnv;
use crossbeam_channel::{unbounded, Sender, Receiver};
use std::thread;

lazy_static! {
    static ref SENDER: Sender<String> = {
        let (sender, receiver) = unbounded();
        // Spawn a thread to handle the receiver
        thread::spawn(move || {
            for message in receiver.iter() {
                println!("Received message: {}", message);
            }
        });
        sender
    };
}

#[no_mangle]
pub extern "system" fn Java_com_yushanma_MyResultHandler_getSender(
    _env: JNIEnv,
    _class: JClass,
) -> jlong {
    let sender_ptr = Box::into_raw(Box::new(SENDER.clone())) as jlong;
    sender_ptr
}

#[no_mangle]
pub extern "system" fn Java_com_yushanma_MyResultHandler_sendMessage(
    mut env: JNIEnv,
    _class: JClass,
    sender_ptr: jlong,
    message: JObject,
) {
    let sender = unsafe { &*(sender_ptr as *mut Sender<String>) };
    let message: String = env.get_string(&message.into()).expect("Couldn't get java string!").into();
    match sender.send(message) {
        Ok(_) => println!("Message sent successfully"),
        Err(e) => eprintln!("Failed to send message: {:?}", e),
    }
}

以上代码主要做了三件事情: 

1、定义静态变量

  • 使用lazy_static!宏定义一个静态变量SENDER,它是一个Sender<String>类型的通道发送端。
  • 创建一个无界通道(unbounded channel),返回一个发送端和接收端。
  • 启动一个新线程,在该线程中不断从接收端读取消息并打印出来。
  • 返回发送端sender

2、实现JNI函数:获取发送端:Java_com_yushanma_MyResultHandler_getSender

  • 这个函数名遵循JNI命名规则,表示这是一个供Java调用的本地方法。
  • 函数参数包括JNI环境指针_env和Java类_class
  • 将静态变量SENDER克隆后转换为原始指针,再将其转换为jlong类型返回给Java。

 3、实现JNI函数:发送消息Java_com_yushanma_MyResultHandler_sendMessage

  • 这个函数同样遵循JNI命名规则。
  • 参数包括JNI环境指针env、Java类_class、发送端指针sender_ptr和Java字符串对象message
  • sender_ptr转换回Sender<String>类型的引用。
  • 从Java字符串对象中提取出Rust字符串。
  • 尝试通过发送端发送消息,如果成功则打印成功信息,否则打印错误信息。

2、新建一个 Maven 项目

package com.yushanma;

import java.io.IOException;

/**
 * @version: V1.0
 * @author: 余衫马
 * @description: 主函数
 * @data: 2024-11-22 19:24
 **/

public class MyResultHandler {

    private static native long getSender();

    private static native void sendMessage(long senderPtr, String message);

    // 用于加载包含本地方法实现的本地库。
    static {
        System.loadLibrary("rust_jni_channel_test");
    }

    public static void main(String[] args) throws IOException {

        long senderPtr = getSender();

        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                sendMessage(senderPtr, String.format("Hello from Java! COUNT * %d", i + 1));
            }
        }).start();

        new Thread(() -> {
            for (int i = 100; i < 200; i++) {
                sendMessage(senderPtr, String.format("Hello from Java! COUNT * %d", i + 1));
            }
        }).start();

        System.in.read();
    }
}

 我们声明了两个本地方法getSendersendMessage,它们分别用于获取发送端指针和发送消息,对应 Rust 库中封装的两个 Native 方法。然后在 main 函数中,启动两个新线程,每个线程发送100条消息到Rust端,并且使用System.in.read()阻塞主线程,以防止程序过早退出。

3、添加外部 libs 与 VM 启动参数

-Djava.library.path=D:\JWorkSpace\ChannelDeomo\libs

 通过 VM 参数指定外部库路径,否则会直接使用默认路径,会报错找不到 dll 文件。

4、运行效果

四、封装

1、新建 Callback 类

package com.yushanma.callback;

import java.time.LocalDateTime;

/**
 * @version: V1.0
 * @author: 余衫马
 * @description: 回调类
 * @data: 2024-11-22 19:30
 **/
public class MyCallback {

    // 用于加载包含本地方法实现的本地库。
    static {
        System.loadLibrary("rust_jni_channel_test");
    }

    /**
     * Sender 指针
     */
    private final long senderPtr;

    /**
     * 获取 Sender 指针
     *
     * @return
     */
    private static native long getSender();

    /**
     * 发送信息
     *
     * @param senderPtr Sender 指针
     * @param message   信息内容
     */
    private static native void sendMessage(long senderPtr, String message);


    /**
     * 默认构造方法
     */
    public MyCallback() {
        senderPtr = getSender();
    }

    /**
     * 回调方法
     */
    public void callback(String out) {
        sendMessage(senderPtr, String.format("%s callback data: %s, senderPtr %d", LocalDateTime.now(), out, senderPtr));
    }


}
package com.yushanma;

import com.yushanma.callback.MyCallback;

import java.io.IOException;
import java.util.UUID;

/**
 * @version: V1.0
 * @author: 余衫马
 * @description: 主函数
 * @data: 2024-11-22 19:24
 **/

public class MyResultHandler {


    public static void main(String[] args) throws IOException {

        new Thread(() -> {

            MyCallback myCallback = new MyCallback();
            for (int i = 0; i < 10; i++) {
                myCallback.callback(UUID.randomUUID().toString());
            }
        }).start();

        new Thread(() -> {

            MyCallback myCallback = new MyCallback();
            for (int i = 0; i < 10; i++) {
                myCallback.callback(UUID.randomUUID().toString());
            }
        }).start();

        System.in.read();
    }
}

2、Rust 修改命名路径

#[macro_use]
extern crate lazy_static;

use jni::objects::{JClass, JObject};
use jni::sys::{jlong, jobject};
use jni::JNIEnv;
use crossbeam_channel::{unbounded, Sender, Receiver};
use std::thread;

lazy_static! {
    static ref SENDER: Sender<String> = {
        let (sender, receiver) = unbounded();
        // Spawn a thread to handle the receiver
        thread::spawn(move || {
            for message in receiver.iter() {
                println!("Received message: {}", message);
            }
        });
        sender
    };
}

#[no_mangle]
pub extern "system" fn Java_com_yushanma_callback_MyCallback_getSender(
    _env: JNIEnv,
    _class: JClass,
) -> jlong {
    let sender_ptr = Box::into_raw(Box::new(SENDER.clone())) as jlong;
    sender_ptr
}

#[no_mangle]
pub extern "system" fn Java_com_yushanma_callback_MyCallback_sendMessage(
    mut env: JNIEnv,
    _class: JClass,
    sender_ptr: jlong,
    message: JObject,
) {
    let sender = unsafe { &*(sender_ptr as *mut Sender<String>) };
    let message: String = env.get_string(&message.into()).expect("Couldn't get java string!").into();
    match sender.send(message) {
        Ok(_) => println!("Message sent successfully"),
        Err(e) => eprintln!("Failed to send message: {:?}", e),
    }
}

路径要保持一致,否则调用本地方法时会报错找不到函数。

3、运行效果

五、内存释放

在 Java 中,JVM(Java Virtual Machine)管理着内存的分配和释放。对于纯 Java 对象,JVM 的垃圾回收机制会自动处理对象的生命周期。然而,当涉及到与本地代码(如 Rust 或 C/C++)交互时,需要特别注意资源的管理。

在这个示例中,MyCallback 是一个通过 JNI(Java Native Interface)或类似技术与 Rust 代码交互的类。senderPtr 是一个指向本地(Rust)资源的指针。当 MyCallback 实例 lib 被垃圾回收时,JVM 并不会自动知道如何释放 senderPtr 所指向的本地资源。这意味着我们需要手动管理这些资源,以避免内存泄漏。

1、显式释放资源

在 MyCallback 类中提供一个方法,用于显式释放本地资源。

package com.yushanma.callback;

import java.time.LocalDateTime;

/**
 * @version: V1.0
 * @author: 余衫马
 * @description: 回调类
 * @data: 2024-11-22 19:30
 **/
public class MyCallback {

    // 用于加载包含本地方法实现的本地库。
    static {
        System.loadLibrary("rust_jni_channel_test");
    }

    /**
     * Sender 指针
     */
    private long senderPtr;

    /**
     * 获取 Sender 指针
     *
     * @return
     */
    private static native long getSender();

    /**
     * 发送信息
     *
     * @param senderPtr Sender 指针
     * @param message   信息内容
     */
    private static native void sendMessage(long senderPtr, String message);

    /**
     * 释放资源
     * @param senderPtr Sender 指针
     */
    private static native void releaseSender(long senderPtr);

    /**
     * 默认构造方法
     */
    public MyCallback() {
        senderPtr = getSender();
    }

    /**
     * 回调方法
     */
    public void callback(String out) {
        sendMessage(senderPtr, String.format("%s callback data: %s, senderPtr %d", LocalDateTime.now(), out, senderPtr));
    }

    /**
     * 释放资源
     */
    public void release(){
        releaseSender(senderPtr);
        senderPtr = 0;
    }

    /**
     * 重载 Object 类的 finalize 方法
     * 不推荐依赖 finalize 方法,因为它的执行时间是不确定的,但作为最后的防线,可以在 finalize 方法中释放资源。
     * @throws Throwable
     */
    @Override
    protected void finalize() throws Throwable {
        try {
            if (senderPtr != 0) {
                releaseSender(senderPtr);
            }
        } finally {
            super.finalize();
        }
    }
}

2、使用 AutoCloseable 接口

package com.yushanma.callback;

import java.time.LocalDateTime;

/**
 * @version: V1.0
 * @author: 余衫马
 * @description: 回调类
 * @data: 2024-11-22 19:30
 **/
public class MyCallback implements AutoCloseable  {

    // 用于加载包含本地方法实现的本地库。
    static {
        System.loadLibrary("rust_jni_channel_test");
    }

    /**
     * Sender 指针
     */
    private long senderPtr;

    /**
     * 获取 Sender 指针
     *
     * @return
     */
    private static native long getSender();

    /**
     * 发送信息
     *
     * @param senderPtr Sender 指针
     * @param message   信息内容
     */
    private static native void sendMessage(long senderPtr, String message);

    /**
     * 释放资源
     * @param senderPtr Sender 指针
     */
    private static native void releaseSender(long senderPtr);

    /**
     * 默认构造方法
     */
    public MyCallback() {
        senderPtr = getSender();
    }

    /**
     * 回调方法
     */
    public void callback(String out) {
        sendMessage(senderPtr, String.format("%s callback data: %s, senderPtr %d", LocalDateTime.now(), out, senderPtr));
    }

    
//    /**
//     * 释放资源
//     */
//    public void release(){
//        releaseSender(senderPtr);
//        senderPtr = 0;
//    }
//
//    /**
//     * 重载 Object 类的 finalize 方法
//     * 不推荐依赖 finalize 方法,因为它的执行时间是不确定的,但作为最后的防线,可以在 finalize 方法中释放资源。
//     * @throws Throwable
//     */
//    @Override
//    protected void finalize() throws Throwable {
//        try {
//            if (senderPtr != 0) {
//                releaseSender(senderPtr);
//            }
//        } finally {
//            super.finalize();
//        }
//    }

    /**
     * 释放资源
     */
    @Override
    public void close() {
        if (senderPtr != 0) {
            releaseSender(senderPtr);
            senderPtr = 0;
        }
    }
    
}
try (MyCallback myCallback = new MyCallback()) {
    for (int i = 0; i < 10; i++) {
        myCallback.callback(UUID.randomUUID().toString());
    }
} // 自动调用 myCallback.close() 方法

3、Rust 释放内存逻辑

添加一个新的 JNI 函数来释放 Sender,并确保在释放时不会发生内存泄漏或其他问题。

#[macro_use]
extern crate lazy_static;

use jni::objects::{JClass, JObject};
use jni::sys::{jlong, jobject};
use jni::JNIEnv;
use crossbeam_channel::{unbounded, Sender, Receiver};
use std::thread;

lazy_static! {
    static ref SENDER: Sender<String> = {
        let (sender, receiver) = unbounded();
        // Spawn a thread to handle the receiver
        thread::spawn(move || {
            for message in receiver.iter() {
                println!("Received message: {}", message);
            }
        });
        sender
    };
}

#[no_mangle]
pub extern "system" fn Java_com_yushanma_callback_MyCallback_getSender(
    _env: JNIEnv,
    _class: JClass,
) -> jlong {
    let sender_ptr = Box::into_raw(Box::new(SENDER.clone())) as jlong;
    sender_ptr
}

#[no_mangle]
pub extern "system" fn Java_com_yushanma_callback_MyCallback_sendMessage(
    mut env: JNIEnv,
    _class: JClass,
    sender_ptr: jlong,
    message: JObject,
) {
    let sender = unsafe { &*(sender_ptr as *mut Sender<String>) };
    let message: String = env.get_string(&message.into()).expect("Couldn't get java string!").into();
    match sender.send(message) {
        Ok(_) => println!("Message sent successfully"),
        Err(e) => eprintln!("Failed to send message: {:?}", e),
    }
}


#[no_mangle]
pub extern "system" fn Java_com_yushanma_callback_MyCallback_releaseSender(
    _env: JNIEnv,
    _class: JClass,
    sender_ptr: jlong,
) {
    if sender_ptr != 0 {
        unsafe {
            // Convert the raw pointer back to a Box and drop it
            let _ = Box::from_raw(sender_ptr as *mut Sender<String>);
        }
        println!("Sender released");
    }
}

释放内存的关键在这里:

let _ = Box::from_raw(sender_ptr as *mut Sender<String>);

这行代码的作用是将一个原始指针(raw pointer)转换为一个 Box,从而恢复对堆上分配的内存的所有权。在 Rust 中,Box 是一个智能指针类型,用于管理堆上的内存。具体来说,这行代码执行了以下操作:

  1. 类型转换sender_ptr as *mut Sender<String> 将 sender_ptr 转换为一个可变的原始指针,指向 Sender<String> 类型的数据。
  2. 从原始指针创建 BoxBox::from_raw 接受一个原始指针,并返回一个 Box,从而接管该指针所指向的内存的所有权。

完整的解释如下:

  • sender_ptr 是一个原始指针,通常是通过某种方式获得的,例如通过 Box::into_raw 方法将一个 Box 转换为原始指针。
  • as *mut Sender<String> 是一个类型转换,将 sender_ptr 转换为一个指向 Sender<String> 的可变原始指针。
  • Box::from_raw(sender_ptr as *mut Sender<String>) 使用这个原始指针创建一个 Box<Sender<String>>,这样 Rust 的所有权系统就可以正确地管理这块内存。

需要注意的是,使用 Box::from_raw 时要确保:

  • 原始指针确实指向有效的堆内存。
  • 该内存之前是通过 Box 分配的。
  • 在调用 Box::from_raw 后,不再使用原始指针,因为 Box 会负责释放这块内存。

4、运行效果

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

摆脱复杂配置!使用MusicGPT部署你的私人AI音乐生成环境

文章目录 前言1. 本地部署2. 使用方法介绍3. 内网穿透工具下载安装4. 配置公网地址5. 配置固定公网地址 前言 今天给大家分享一个超酷的技能&#xff1a;如何在你的Windows电脑上快速部署一款文字生成音乐的AI创作服务——MusicGPT&#xff0c;并且通过cpolar内网穿透工具&…

挑战用React封装100个组件【001】

项目地址 https://github.com/hismeyy/react-component-100 组件描述 组件适用于需要展示图文信息的场景&#xff0c;比如产品介绍、用户卡片或任何带有标题、描述和可选图片的内容展示 样式展示 代码展示 InfoCard.tsx import ./InfoCard.cssinterface InfoCardProps {ti…

搭建帮助中心到底有什么作用?

在当今快节奏的商业环境中&#xff0c;企业面临着日益增长的客户需求和竞争压力。搭建一个有效的帮助中心对于企业来说&#xff0c;不仅是提升客户服务体验的重要途径&#xff0c;也是优化内部知识管理和提升团队效率的关键。以下是帮助中心在企业运营中的几个关键作用&#xf…

学习threejs,使用CubeCamera相机创建反光效果

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️CubeCamera 立方体相机 二、…

微前端-MicroApp

微前端即是由一个主应用来集成多个微应用&#xff08;可以不区分技术栈进行集成&#xff09; 下面是使用微前端框架之一 MicroApp 对 react微应用 的详细流程 第一步 创建主应用my-mj-app 利用脚手架 npx create-react-app my-mj-app 快速创建 安装 npm install --save rea…

深度学习—BP算法梯度下降及优化方法Day37

梯度下降 1.公式 w i j n e w w i j o l d − α ∂ E ∂ w i j w_{ij}^{new} w_{ij}^{old} - \alpha \frac{\partial E}{\partial w_{ij}} wijnew​wijold​−α∂wij​∂E​ α为学习率 当α过小时&#xff0c;训练时间过久增加算力成本&#xff0c;α过大则容易造成越过最…

wp the_posts_pagination 与分类页面搭配使用

<ul> <?php while( have_posts() ) : the_post(); <li > <a href"<?php the_permalink(); ?>"> <?php xizhitbu_get_thumbnail(thumb-pro); ?> </a> <p > <a href&q…

深度学习-49-AI应用实战之基于HyperLPR的车牌识别

文章目录 1 车牌识别系统1.1 识别原理1.1.1 车牌定位1.1.2 字符识别2 实例应用2.1 安装hyperlpr32.2 识别结果2.3 可视化显示2.4 结合streamlit3 附录3.1 PIL.Image转换成OpenCV格式3.2 OpenCV转换成PIL.Image格式3.3 st.image嵌入图像内容3.4 参考附录1 车牌识别系统 车牌识别…

ShuffleNet V2:高效卷积神经网络架构设计的实用指南

摘要 https://arxiv.org/pdf/1807.11164 当前&#xff0c;神经网络架构设计大多以计算复杂度的间接指标&#xff0c;即浮点运算数&#xff08;FLOPs&#xff09;为指导。然而&#xff0c;直接指标&#xff08;例如速度&#xff09;还取决于其他因素&#xff0c;如内存访问成本…

在C#中使用OpenCV的.net包装器EmguCV

Emgu.CV OpenCvSharp 两个库都是OpenCV的C#封装。这里不讨论优劣&#xff0c;两个都有相应的用途。 下载安装4.6.0.5131&#xff0c;执行文件exe https://sourceforge.net/projects/emgucv/files/emgucv/4.6.0/ 安装到一个目录下&#xff0c;这里安装到H:\Emgu\ 目录下。…

HarmonyOS:@Provide装饰器和@Consume装饰器:与后代组件双向同步

一、前言 Provide和Consume&#xff0c;应用于与后代组件的双向数据同步&#xff0c;应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递&#xff0c;Provide和Consume摆脱参数传递机制的束缚&#xff0c;实现跨层级传递。 其中Provi…

webrtc 3A移植以及实时处理

文章目录 前言一、交叉编译1.Pulse Audio webrtc-audio-processing2.交叉编译 二、基于alsa进行实时3A处理1.demo源码2.注意项3.效果展示 总结 前言 由于工作需要&#xff0c;硬件3A中的AEC效果实在太差&#xff0c;后面使用SpeexDSP的软3A&#xff0c;效果依旧不是很好&#…

Java 反射(Reflection)

Java 反射&#xff08;Reflection&#xff09; Java 反射&#xff08;Reflection&#xff09;是一个强大的特性&#xff0c;它允许程序在运行时查询、访问和修改类、接口、字段和方法的信息。反射提供了一种动态地操作类的能力&#xff0c;这在很多框架和库中被广泛使用&#…

深入浅出剖析典型文生图产品Midjourney

2022年7月,一个小团队推出了公测的 Midjourney,打破了 AIGC 领域的大厂垄断。作为一个精调生成模型,以聊天机器人方式部署在 Discord,它创作的《太空歌剧院》作品,甚至获得了美国「数字艺术/数码摄影」竞赛单元一等奖。 这一事件展示了 AI 在绘画领域惊人的创造力,让人们…

【Linux】磁盘 | 文件系统 | inode

&#x1fa90;&#x1fa90;&#x1fa90;欢迎来到程序员餐厅&#x1f4ab;&#x1f4ab;&#x1f4ab; 主厨&#xff1a;邪王真眼 主厨的主页&#xff1a;Chef‘s blog 所属专栏&#xff1a;青果大战linux 总有光环在陨落&#xff0c;总有新星在闪烁 模电好难啊&#xff…

PHP 去掉特殊不可见字符 “\u200e“

描述 最近在排查网站业务时&#xff0c;发现有数据匹配失败的情况 肉眼上完全看不出问题所在 当把字符串 【M24308/23-14F‎】复制出来发现 末尾有个不可见的字符 使用删除键或左右移动时才会发现 最后测试通过 var_dump 打印 发现这个"空字符"占了三个长度 &#xf…

【C#设计模式(15)——命令模式(Command Pattern)】

前言 命令模式的关键通过将请求封装成一个对象&#xff0c;使命令的发送者和接收者解耦。这种方式能更方便地添加新的命令&#xff0c;如执行命令的排队、延迟、撤销和重做等操作。 代码 #region 基础的命令模式 //命令&#xff08;抽象类&#xff09; public abstract class …

使用zabbix监控k8s

一、 参考文献 小阿轩yx-案例&#xff1a;Zabbix监控kubernetes云原生环境 手把手教你实现zabbix对Kubernetes的监控 二、部署经验 关于zabbix监控k8s&#xff0c;总体来说是分为两块内容&#xff0c;一是在k8s集群部署zabbix-agent和zabbix- proxy。二是在zabbix进行配置。…

ThingsBoard规则链节点:GCP Pub/Sub 节点详解

目录 引言 1. GCP Pub/Sub 节点简介 2. 节点配置 2.1 基本配置示例 3. 使用场景 3.1 数据传输 3.2 数据分析 3.3 事件通知 3.4 任务调度 4. 实际项目中的应用 4.1 项目背景 4.2 项目需求 4.3 实现步骤 5. 总结 引言 ThingsBoard 是一个开源的物联网平台&#xff0…

10.机器学习--集成学习

机器学习领域有一个强大的思路&#xff1a;集成学习&#xff0c;该方法在诸多机器学习竞赛中往往能够获得最优的结果。集成学习的基本思想实际上非常简单&#xff1a;三个臭皮匠顶一个诸葛亮&#xff0c;即将多个模型组合在一起获得的效果往往要强于单一模型。 目录 集成学习…