Java避坑案例 - 线程池使用中的风险识别与应对

news2024/11/24 12:55:16

文章目录

  • 线程池的基本概念
  • 创建线程池的注意事项
    • 实例1: `newFixedThreadPool` 使用无界队列,可能因任务积压导致 OOM
    • 实例2: `newCachedThreadPool` 会创建大量线程,可能因线程数量过多导致无法创建新线程。
  • 线程池参数设置的最佳实践
    • 线程池默认的工作行为
    • 预先启动核心线程
  • 监控线程池状态的方法
  • 线程池的混用策略

在这里插入图片描述


  1. 线程池的基本概念和使用场景
  2. 线程池创建时的注意事项,包括手动创建与使用 Executors 类的区别
  3. 案例分析 newFixedThreadPoolnewCachedThreadPool 可能引发的问题
  4. 线程池参数设置的最佳实践
  5. 监控线程池状态
  6. 线程池的混用策略

线程池的基本概念

在程序中,我们会用各种池化技术来缓存创建昂贵的对象,比如线程池、连接池、内存池。一般是预先创建一些对象放入池中,使用的时候直接取出使用,用完归还以便复用,还会通过一定的策略调整池中缓存对象的数量,实现池的动态伸缩

线程池是一种管理线程的机制,通过重用线程来减少创建和销毁线程的开销,适用于处理短平快的任务。线程池的核心组成部分包括核心线程数、最大线程数、工作队列及拒绝策略。


创建线程池的注意事项

在 Java 中,Executors 类提供了快速创建线程池的方法,但在生产环境中,建议手动使用 ThreadPoolExecutor 来创建线程池。这是因为 Executors 提供的某些方法可能导致内存溢出(OOM)等问题。

实例1: newFixedThreadPool 使用无界队列,可能因任务积压导致 OOM

/**
 * 触发OOM(OutOfMemoryError)的测试方法
 * 通过向固定大小的线程池中提交大量任务,每个任务在执行时生成大量的字符串并保持在内存中
 * 直到线程池被关闭,以此来模拟和测试OOM的情况
 * 
 * @throws InterruptedException 如果在等待线程池终止时被中断
 */
@GetMapping("oom1")
public void oom1() throws InterruptedException {
    // 创建一个固定大小为1的线程池,以控制并发任务的数量
    ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
    // 打印线程池的统计信息,监控线程池的状态
    printStats(threadPool);
    
    // 提交给线程池大量的任务,以模拟高并发的场景
    for (int i = 0; i < 100000000; i++) {
        // 每个任务在执行时会生成一个很大的字符串,并尝试将其保持在内存中
        threadPool.execute(() -> {
            // 生成一个由大量字符组成的字符串,以占用大量内存
            String payload = IntStream.rangeClosed(1, 1000000)
                    .mapToObj(__ -> "a")
                    .collect(Collectors.joining("")) + UUID.randomUUID().toString();
            // 使当前任务暂停1小时,模拟长时间运行的任务
            try {
                TimeUnit.HOURS.sleep(1);
            } catch (InterruptedException e) {
                // 捕获中断异常,但不执行任何操作
            }
            // 记录生成的字符串,进一步增加内存的使用
            log.info(payload);
        });
    }
    
    // 关闭线程池,不再接受新的任务
    threadPool.shutdown();
    // 等待线程池中的所有任务完成,或直到指定的超时时间结束
    threadPool.awaitTermination(1, TimeUnit.HOURS);
}




/**
 * 定期打印线程池的运行统计信息
 * 此方法内部创建了一个新的单线程调度器,用于定期执行打印线程池统计信息的任务
 * 它提供了线程池大小、活动线程数、已完成任务数和队列中任务数的信息
 * 这些信息有助于监控线程池的性能和工作负载
 * 
 * @param threadPool 线程池对象,其统计信息将被打印
 */
private void printStats(ThreadPoolExecutor threadPool) {
    Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
        // 打印分割线,用于区分不同的统计时间点
        log.info("=========================");
        // 打印线程池当前的线程数量
        log.info("Pool Size: {}", threadPool.getPoolSize());
        // 打印当前活动线程的数量
        log.info("Active Threads: {}", threadPool.getActiveCount());
        // 打印已完成任务的总数
        log.info("Number of Tasks Completed: {}", threadPool.getCompletedTaskCount());
        // 打印队列中等待执行的任务数量
        log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size());
        // 再次打印分割线,结束本次统计信息的打印
        log.info("=========================");
    }, 0, 1, TimeUnit.SECONDS);
}


在这里插入图片描述

newFixedThreadPool 方法线程池的工作队列直接 new 了一个
LinkedBlockingQueue,而默认构造方法的 LinkedBlockingQueue 是一个Integer.MAX_VALUE 长度的队列,可以认为是无界的

虽然使用 newFixedThreadPool 可以把工作线程控制在固定的数量上,但任务队列是无界的。如果任务较多并且执行较慢的话,队列可能会快速积压,撑爆内存导致 OOM


实例2: newCachedThreadPool 会创建大量线程,可能因线程数量过多导致无法创建新线程。

/**
 * 触发OOM(OutOfMemoryError)的测试方法
 * 通过创建大量的线程和字符串对象,最终导致内存溢出
 * 此方法主要用于演示和测试目的,实际应用中应避免此类设计
 */
@GetMapping("oom2")
public void oom2() throws InterruptedException {
    // 创建一个可缓存的线程池,按需(每个任务)创建新线程
    ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newCachedThreadPool();
    // 打印线程池的统计信息
    printStats(threadPool);
    // 循环提交大量任务到线程池
    for (int i = 0; i < 100000000; i++) {
        // 每个任务生成一个随机UUID字符串,并尝试休眠1小时
        threadPool.execute(() -> {
            String payload = UUID.randomUUID().toString();
            try {
                TimeUnit.HOURS.sleep(1);
            } catch (InterruptedException e) {
                // 捕获中断异常,但不执行任何操作
            }
            // 日志记录生成的UUID字符串
            log.info(payload);
        });
    }
    // 关闭线程池,不再接受新任务
    threadPool.shutdown();
    // 等待线程池中的所有任务完成,最多等待1小时
    threadPool.awaitTermination(1, TimeUnit.HOURS);
}

在这里插入图片描述

查看newCachedThreadPool 的源码可以看到,这种线程池的最大线程数是Integer.MAX_VALUE,可以认为是没有上限的,而其工作队列 SynchronousQueue 是一个没有存储空间的阻塞队列。这意味着,只要有请求到来,就必须找到一条工作线程来处理,如果当前没有空闲的线程就再创建一条新的。

由于我们的任务需要 1 小时才能执行完成,大量的任务进来后会创建大量的线程。我们知道线程是需要分配一定的内存空间作为线程栈的,比如 1MB,因此无限制创建线程必然会导致 OOM。


不建议使用 Executors 提供的两种快捷的线程池

  • 需要根据自己的场景、并发情况来评估线程池的几个核心参数,包括核心线程数、最大线程数、线程回收策略、工作队列的类型,以及拒绝策略,确保线程池的工作行为符合需求,一般都需要设置有界的工作队列和可控的线程数

  • 任何时候,都应该为自定义线程池指定有意义的名称,以方便排查问题。当出现线程数量暴增、线程死锁、线程占用大量 CPU、线程执行出现异常等问题时,我们往往会抓取线程栈。此时,有意义的线程名称,就可以方便我们定位问题

除了建议手动声明线程池以外,还建议用一些监控手段来观察线程池的状态。线程池这个组件往往会表现得任劳任怨、默默无闻,除非是出现了拒绝策略,否则压力再大都不会抛出一个异常。如果我们能提前观察到线程池队列的积压,或者线程数量的快速膨胀,往往可以提早发现并解决问题


线程池参数设置的最佳实践

根据应用场景,合理设置以下参数:

  • 核心线程数:应根据任务的并发性和执行时间进行调整。
  • 最大线程数:应限制线程数量以防止资源耗尽。
  • 工作队列:应使用有界队列来防止无穷的任务积压。
  • 拒绝策略:根据应用需求选择合适的拒绝策略,例如 AbortPolicyCallerRunsPolicy
import com.google.common.util.concurrent.ThreadFactoryBuilder;


/**
 * 处理 "good" GET 请求的控制器方法
 * 该方法演示了如何在Spring MVC环境中使用线程池执行异步任务
 * 它创建了一个固定大小的线程池,并提交了多个任务去执行
 * 
 * @return 返回 AtomicInteger 的值,用于跟踪任务的完成数量
 * @throws InterruptedException 如果在等待过程中线程被中断
 */
@GetMapping("good")
public int good() throws InterruptedException {
    // 使用 AtomicInteger 来跟踪任务的唯一标识
    AtomicInteger atomicInteger = new AtomicInteger();
    
    // 创建一个 ThreadPoolExecutor,配置核心线程数、最大线程数、空闲线程存活时间、工作队列等
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
            2, 5,
            5, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(10),
            new ThreadFactoryBuilder().setNameFormat("demo-threadpool-%d").build(),
            new ThreadPoolExecutor.AbortPolicy());
    
    // 打印线程池的统计信息,监控线程池状态
    printStats(threadPool);
    
    // 生成并提交 20 个任务到线程池
    IntStream.rangeClosed(1, 20).forEach(i -> {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 获取并递增任务ID
        int id = atomicInteger.incrementAndGet();
        
        try {
            // 提交任务到线程池执行
            threadPool.submit(() -> {
                log.info("{} started", id);
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                }
                log.info("{} finished", id);
            });
        } catch (Exception ex) {
            // 如果任务提交失败,记录错误信息并递减任务ID
            log.error("error submitting task {}", id, ex);
            atomicInteger.decrementAndGet();
        }
    });

    // 主线程休眠60秒,等待任务完成
    TimeUnit.SECONDS.sleep(60);
    
    // 返回完成的任务数量
    return atomicInteger.intValue();
}

初始化线程池:

创建一个 AtomicInteger 对象 atomicInteger,用于记录任务ID。
创建一个 ThreadPoolExecutor 对象 threadPool,配置核心线程数为2,最大线程数为5,空闲线程存活时间为5秒,任务队列容量为10,线程工厂设置名称格式,拒绝策略为AbortPolicy。

打印线程池状态:

调用 printStats 方法,每隔1秒打印线程池的当前状态。

提交任务:

  • 使用 IntStream.rangeClosed(1, 20) 生成1到20的整数流。
  • 每次迭代中,休眠1秒,获取任务ID,尝试提交任务到线程池。
  • 任务内容为记录开始日志,休眠10秒,记录结束日志。
  • 如果提交失败,记录错误并减少任务计数。

等待和返回结果:

等待60秒后,返回提交任务的总数。

线程池默认的工作行为

  1. 核心线程数 (corePoolSize):

    • 线程池在没有任务时,会保持corePoolSize个线程活跃。
    • 当有任务提交时,线程池会首先尝试复用这些核心线程。
  2. 任务堆积

    • 如果所有核心线程都在忙碌,后续的任务会被添加到工作队列(如LinkedBlockingQueue)中等待处理。
    • 这个工作队列的大小是有限的,默认情况下是无界的。
  3. 扩容

    • 一旦工作队列满了,线程池会尝试扩容,创建新的工作线程,直到达到maximumPoolSize
    • maximumPoolSize是线程池允许的最大线程数。
  4. 拒绝策略

    • 如果队列和线程池都已满,线程池会根据预设的拒绝策略(如AbortPolicyCallerRunsPolicy等)处理新提交的任务。
  5. 线程回收

    • 当线程数大于corePoolSize时,如果线程在keepAliveTime内没有处理新任务,它们会被终止,回收至核心线程数。

以下是ThreadPoolExecutor构造函数的参数及其影响:

  • corePoolSize:核心线程数,保持活跃的最小线程数。
  • maximumPoolSize:最大线程数,能够创建的最大线程数量。
  • keepAliveTime:非核心线程闲置时间,超过此时间后将被回收。
  • unit:时间单位,与keepAliveTime一起使用。
  • workQueue:用于存储等待执行任务的队列。
  • handler:拒绝策略,用于处理无法被执行的任务。

预先启动核心线程

在Java的ThreadPoolExecutor中,预先启动核心线程意味着在创建线程池时,线程池会立即启动并激活corePoolSize个核心线程。这可以减少任务到达时的响应延迟,尤其是在预计会有大量任务同时提交的场景下。

相关参数和方法

  • 核心线程数 (corePoolSize):指定要保持活跃的最小线程数。
  • allowCoreThreadTimeOut:如果设置为true,当核心线程在keepAliveTime内没有执行任务时,它们会被回收。

预先启动核心线程的方法

可以使用prestartAllCoreThreads()方法来预先启动所有核心线程:

  • prestartCoreThread():启动单个核心线程(如果核心线程未被创建)。
  • prestartAllCoreThreads():启动所有核心线程。

演示如何预先启动核心线程:

import java.util.concurrent.*;

public class PrestartCoreThreadsExample {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            2, // corePoolSize
            4, // maximumPoolSize
            60, // keepAliveTime
            TimeUnit.SECONDS, // unit
            new LinkedBlockingQueue<>() // workQueue
        );

        // 预先启动所有核心线程
        executor.prestartAllCoreThreads();

        // 提交任务
        for (int i = 0; i < 5; i++) {
            final int taskId = i;
            executor.execute(() -> {
                System.out.println("Executing task " + taskId);
                try {
                    Thread.sleep(1000); // 模拟任务执行
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        executor.shutdown(); // 关闭线程池
    }
}

监控线程池状态的方法

建议在生产环境中添加监控,定期输出线程池的状态信息。可以使用定时任务定期打印线程池的基本信息,如线程数、活跃线程数、完成任务数量等。

/**
 * 定期打印线程池的运行统计信息
 * 此方法内部创建了一个新的单线程调度器,用于定期执行打印线程池统计信息的任务
 * 它提供了线程池大小、活动线程数、已完成任务数和队列中任务数的信息
 * 这些信息有助于监控线程池的性能和工作负载
 * 
 * @param threadPool 线程池对象,其统计信息将被打印
 */
private void printStats(ThreadPoolExecutor threadPool) {
    Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
        // 打印分割线,用于区分不同的统计时间点
        log.info("=========================");
        // 打印线程池当前的线程数量
        log.info("Pool Size: {}", threadPool.getPoolSize());
        // 打印当前活动的线程数量
        log.info("Active Threads: {}", threadPool.getActiveCount());
        // 打印已完成任务的总数
        log.info("Number of Tasks Completed: {}", threadPool.getCompletedTaskCount());
        // 打印队列中等待执行的任务数量
        log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size());
        // 再次打印分割线,结束本次统计信息的打印
        log.info("=========================");
    }, 0, 1, TimeUnit.SECONDS);
}


线程池的混用策略

要根据任务的“轻重缓急”来指定线程池的核心参数,包括线程数、回收策略和任务队列

  1. IO密集型任务

    • 特点:执行时间较长、数量较少,通常涉及网络请求、文件操作等。
    • 配置建议:
      • 可以增加核心线程数,因为这些任务通常会在等待IO时处于阻塞状态。
      • 不需要太大的任务队列,以避免内存消耗过大。
  2. 计算密集型任务

    • 特点:执行时间短、数量较多,主要涉及CPU计算。
    • 配置建议:
      • 线程数量应接近CPU核心数或核心数的两倍,以优化CPU资源利用。
      • 需要较长的任务队列来处理任务高峰,防止因线程不足导致任务拒绝。

优化策略

  1. 根据任务特性选择线程池

    • 对于IO密集型任务,使用较大的线程池以处理多任务并发。
    • 对于计算密集型任务,使用较小的线程池,避免线程切换的开销。
  2. 合理设置核心参数

    • corePoolSize:根据任务特性选择适当的线程数。
    • maximumPoolSize:根据系统资源设置合理的最大线程数。
    • keepAliveTime:调节非核心线程的存活时间,以便更好地应对任务波动。
  3. 使用不同类型的线程池

    • 对于短期任务,可以考虑使用CachedThreadPool,它会动态创建和回收线程。
    • 对于长期任务,可以使用FixedThreadPool,确保线程数不变,适合处理稳定负载的任务。

CachedThreadPoolFixedThreadPool的特性

  1. CachedThreadPool

    • 特点:可以动态创建线程,根据需要创建新的线程,空闲的线程会被回收,适合短时间内大量并发任务。
    • 优点:灵活性高,能够适应突发的任务需求。
    • 缺点:在任务量大且长时间运行时,可能导致资源耗尽或系统负载过高。
  2. FixedThreadPool

    • 特点:线程池中线程数固定,适合长期稳定的负载。
    • 优点:可控性强,避免了因动态创建线程导致的资源问题。
    • 缺点:在任务量大时可能会出现任务被阻塞的情况,因为线程数不够。

适用场景与局限性

  • 短期任务

    • 适合使用CachedThreadPool。当任务数量不确定且突发性强时,CachedThreadPool能够快速响应并执行任务。
    • 注意事项:应监控系统资源,以防因过多线程创建导致内存或CPU负担过重。
  • 长期任务

    • 适合使用FixedThreadPool。对于稳定负载的情况,FixedThreadPool可以确保线程数不变,提升执行效率。
    • 注意事项:线程数过少可能会导致任务排队,线程数过多可能会增加上下文切换的开销。

在这里插入图片描述

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

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

相关文章

基于SSM+微信小程序的社区垃圾回收管理系统(垃圾1)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 基于ssm微信小程序的社区垃圾回收管理系统&#xff0c;有管理员&#xff0c;回收员&#xff0c;用户三个角色。 1、管理员功能有个人中心&#xff0c;用户管理&#xff0c;回收员管理&am…

确保组织决策权清晰的有效策略

在组织中&#xff0c;明确的决策权、有效的沟通机制、权责明确的结构是确定和维护清晰决策权的关键要素。明确的决策权确保了每个成员知道自己的职责和权限&#xff0c;有效的沟通机制促进了信息的流通和理解&#xff0c;权责明确的结构则为组织的运作提供了清晰的框架。明确的…

SpringBoot3+SpringSecurity6基于若依系统整合自定义登录流程

SpringBoot3SpringSecurity6基于若依系统整合自定义登录流程 问题背景 在做项目时遇到了要对接统一认证的需求&#xff0c;但是由于框架的不兼容性&#xff08;我们项目是springboot3&#xff0c;jdk17&#xff0c;springsecurity6.1.5&#xff09;等因素&#xff0c;不得不使…

WPF+MVVM案例实战(十四)- 封装一个自定义消息弹窗控件(下)

文章目录 1、案例效果2、弹窗空间使用1.引入用户控件2、按钮命令实现 3、总结4、源代码获取 1、案例效果 2、弹窗空间使用 1.引入用户控件 打开 Wpf_Examples 项目&#xff0c;在引用中添加用户控件库&#xff0c;在 MainWindow.xaml 界面引用控件库&#xff0c;代码如下&…

【论文精读】ID-like Prompt Learning for Few-Shot Out-of-Distribution Detection

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;论文精读_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 注&#xff1a;下文…

PIDNet(语义分割)排坑

PIDNet训练自己的数据集 1. 前言2. 准备工作3. 配置环境4. 排坑过程4.1.1 configs增加了VOC文件夹 并在里面写了yaml参数文件4.1.2 加载VOC格式数据集的类4.1.3 train.py调试 1. 前言 paper小修时reviewer说baseline太老&#xff0c;所以对CVPR2023的PIDNet进行复现&#xff0…

Google Recaptcha V2 简单使用

最新的版本是v3&#xff0c;但是一直习惯用v2&#xff0c;就记录一下v2 的简单用法&#xff0c;以免将来忘记了 首先在这里注册你域名&#xff0c;如果是本机可以直接直接填 localhost 或127.0.0.1 https://www.google.com/recaptcha/about/ 这是列子 网站密钥&#xff1a;是…

autMan奥特曼机器人-内置Redis

autMan内置了redis服务&#xff0c;有的脚本运行需要redis支持 几个注意事项&#xff1a; 启用redis服务后要重启autMan生效&#xff0c;关闭一样的道理。启用redis服务后会增加约200M的内存占用多个autMan的redis服务可以组成集群redis服务

五、快速入门K8s之Pod容器的生命周期

一、容器的初始化init ⭐️ init c &#xff1a; init contariner 初始化容器&#xff0c;只是用来初始化&#xff0c;初始化完成就会死亡可以大于的等于一也可以没有&#xff0c;每个init只有在前一个init c执行完成后才可以执行下一个、init容器总是运行到成功完成为止&#…

sqoop问题汇总记录

此篇博客仅记录在使用sqoop时遇到的各种问题。持续更新&#xff0c;有问题评论区一起探讨&#xff0c;写得有不足之处见谅。 Oracle_to_hive 1. main ERROR Could not register mbeans java.security.AccessControlException: access denied ("javax.management.MBeanTr…

C++对象模型:Function 语意学

Member 的各种调用方式 Nonstatic Member Function 使用C时&#xff0c;成员函数和非成员函数在性能上应该是等价的。当设计类时&#xff0c;我们不应该因为担心效率问题而避免使用成员函数。 实现&#xff1a;编译器会将成员函数转换为一个带有额外this指针参数的非成员函数…

二叉树中的深搜 算法专题

二叉树中的深搜 一. 计算布尔二叉树的值 计算布尔二叉树的值 class Solution {public boolean evaluateTree(TreeNode root) {if(root.left null) return root.val 0? false: true;boolean left evaluateTree(root.left);boolean right evaluateTree(root.right);return…

【Linux】环境ChatGLM-4-9B 模型部署

一、模型介绍 GLM-4-9B 是智谱 AI 推出的最新一代预训练模型 GLM-4 系列中的开源版本。 在语义、数学、推理、代码和知识等多方面的数据集测评中&#xff0c; GLM-4-9B 及其人类偏好对齐的版本 GLM-4-9B-Chat 均表现出超越 Llama-3-8B 的卓越性能。除了能进行多轮对话&#xf…

深入理解Java 线程并发编排工具: 概述和应用场景

目录 前言概述1. CountDownLatch2. CyclicBarrier3. Semaphore&#xff08;信号量)4. Condition 案例CountDownLatch-马拉松场景CyclicBarrier-马拉松场景Semaphore-公交车占座场景Condition-线程等待唤醒场景 前言 在 Java 的 java.util.concurrent (JUC) 包中&#xff0c;提…

C++初阶(八)--内存管理

目录 引入&#xff1a; 一、C中的内存布局 1.内存区域 2.示例变量存储位置说明 二、C语言中动态内存管理 三、C内存管理方式 1.new/delete操作内置类型 2.new和delete操作自定义类型 四、operator new与operator delete函数&#xff08;重要点进行讲解&#xff09; …

架构的本质之 MVC 架构

前言 程序员习惯的编程方式就是三步曲。 所以&#xff0c;为了不至于让一个类撑到爆&#x1f4a5;&#xff0c;需要把黄色的对象、绿色的方法、红色的接口&#xff0c;都分配到不同的包结构下。这就是你编码人生中所接触到的第一个解耦操作。 分层框架 MVC 是一种非常常见且常…

Node学习记录-child_process 子进程

来自&#xff1a;https://juejin.cn/post/7277045020422930488 child_process用于处理CPU密集型应用&#xff0c;Nodejs创建子进程有7个API&#xff0c;其中带Async的是同步API,不带的是异步API child_process.exec(command[, options][, callback]) command:要运行的命令&am…

NVR批量管理软件/平台EasyNVR多个NVR同时管理支持对接阿里云、腾讯云、天翼云、亚马逊S3云存储

随着云计算技术的日益成熟&#xff0c;越来越多的企业开始将其业务迁移到云端&#xff0c;以享受更为灵活、高效且经济的服务模式。在视频监控领域&#xff0c;云存储因其强大的数据处理能力和弹性扩展性&#xff0c;成为视频数据存储的理想选择。NVR批量管理软件/平台EasyNVR&…

2024年编程语言排行榜:技术世界的新星与常青树

随着技术的不断进步&#xff0c;编程语言的流行度也在不断变化。今天&#xff0c;就让我们一起来看看2024年的编程语言排行榜&#xff0c;探索哪些语言在技术世界中占据了主导地位。 1. Python&#xff1a;稳居榜首 Python以其在人工智能、数据科学、网络开发等多个领域的广泛…

MFC工控项目实例二十八模拟量信号每秒采集100次

采用两个多媒体定时器&#xff0c;一个0.1秒计时,另一个用来对模拟量信号采集每秒100次.。 1、在SEAL_PRESSUREDlg.h中添加代码 class CSEAL_PRESSUREDlg : public CDialog { public:CSEAL_PRESSUREDlg(CWnd* pParent NULL); // standard constructor&#xff0e;&#xff0e…