14、线程池ForkJoinPool实战及其工作原理分析

news2024/9/27 12:16:25

1. 由一道算法题引发的思考

算法题:如何充分利用多核CPU的性能,快速对一个2千万大小的数组进行排序?
1)首先这是一道排序的算法题,而且是需要使用高效的排序算法对2千万大小的数组进行排序,可以考虑使用快速排序或者归并排序。
2)可以使用多线程并行排序算法来充分利用多核CPU的性能。

2. 基于归并排序算法实现

快速对一个大小为2千万的数组进行排序,可以使用高效的归并排序算法来实现。

2.1 什么是归并排序

**归并排序(Merge Sort)是一种基于分治思想的排序算法。**归并排序的基本思想是将一个大数组分成两个相等大小的子数组,对每个子数组分别进行排序,然后将两个子数组合并成一个有序的大数组。因为常常使用递归实现(由先拆分后合并的性质决定的),所以我们称其为归并排序。
归并排序的步骤包括以下几个方面:

  • 将数组分成两个子数组
  • 对每个子数组进行排序
  • 合并两个有序的子数组
    归并排序的时间复杂度为O(nlogn),空间复杂度为O(n),其中n为数组的长度。
分治思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。
分治思想的步骤如下:
undefined.分解:将要解决的问题划分成若干规模较小的同类问题;
undefined.求解:当子问题划分得足够小时,用较简单的方法解决;
undefined.合并:按原问题的要求,将子问题的解逐层合并构成原问题的解。
计算机十大经典算法中的归并排序、快速排序、二分查找都是基于分治思想实现的算法
分治任务模型图如下:

在这里插入图片描述

2.2 归并排序动图演示

归并排序演示

2.3 使用归并排序实现上面的算法题

单线程实现归并排序
单线程归并算法的实现,它的基本思路是将序列分成两个部分,分别进行递归排序,然后将排序好的子序列合并起来。

public class MergeSort {

    private final int[] arrayToSort;//要排序的数组

    private final int threshold;//拆分的阈值,低于此阈值就不再进行拆分

    public MergeSort(int[] arrayToSort, int threshold) {
        this.arrayToSort = arrayToSort;
        this.threshold = threshold;
    }

    /**
     * 排序
     * @return
     */
    public int[] sequentialSort(){
        return sequentialSort(arrayToSort,threshold);
    }

    private int[] sequentialSort(int[] arrayToSort, int threshold) {
        //拆分后的数组长度小于阈值,直接进行排序
        if(arrayToSort.length<threshold){
            Arrays.sort(arrayToSort);
        }

        int midpoint = arrayToSort.length / 2;
        //对数组进行拆分
        int[] leftArray = Arrays.copyOfRange(arrayToSort, 0, midpoint);
        int[] rightArray = Arrays.copyOfRange(arrayToSort, midpoint, arrayToSort.length);
        //递归调用
        leftArray = sequentialSort(leftArray, threshold);
        rightArray = sequentialSort(rightArray,threshold);
        //合并排序结果
        return merge(leftArray,rightArray);

    }

    private static int[] merge(final int[] leftArray, final int[] rightArray) {

        int[] mergedArray = new int[leftArray.length + rightArray.length];
        int mergedArrayPos = 0;
        int leftArrayPos = 0;
        int rightArrayPos = 0;
        while (leftArrayPos < leftArray.length & rightArrayPos < rightArray.length){
            if (leftArray[leftArrayPos]<=rightArray[rightArrayPos]){
                mergedArray[mergedArrayPos] = leftArray[leftArrayPos];
                leftArrayPos++;
            }else {
                mergedArray[mergedArrayPos] = rightArray[rightArrayPos];
                rightArrayPos++;
            }
            mergedArrayPos++;
        }

        while (leftArrayPos < leftArray.length){
            mergedArray[mergedArrayPos] = leftArray[leftArrayPos];
            leftArrayPos++;
            mergedArrayPos++;
        }
        
        while (rightArrayPos < rightArray.length){
            mergedArray[mergedArrayPos] = rightArray[rightArrayPos];
            rightArrayPos++;
            mergedArrayPos++;
        }
        
        return mergedArray;
    }
}

Fork/Join并行归并排序
并行归并排序是一种利用多线程实现的归并排序算法。它的基本思路是将数据分成若干部分,然后在不同线程上对这些部分进行归并排序,最后将排好序的部分合并成有序数组。在多核CPU上,这种算法也能够有效提高排序速度。
可以使用Java的Fork/Join框架来实现归并排序的并行化

public class MergeSortTask extends RecursiveAction {

   private final int threshold; //拆分的阈值,低于此阈值就不再进行拆分
   private int[] arrayToSort; //要排序的数组

   public MergeSortTask(final int[] arrayToSort, final int threshold) {
      this.arrayToSort = arrayToSort;
      this.threshold = threshold;
   }

   @Override
   protected void compute() {
      //拆分后的数组长度小于阈值,直接进行排序
      if (arrayToSort.length <= threshold) {
         // 调用jdk提供的排序方法
         Arrays.sort(arrayToSort);
         return;
      }

      // 对数组进行拆分
      int midpoint = arrayToSort.length / 2;
      int[] leftArray = Arrays.copyOfRange(arrayToSort, 0, midpoint);
      int[] rightArray = Arrays.copyOfRange(arrayToSort, midpoint, arrayToSort.length);

      MergeSortTask leftTask = new MergeSortTask(leftArray, threshold);
      MergeSortTask rightTask = new MergeSortTask(rightArray, threshold);

      //提交任务
      leftTask.fork();
      rightTask.fork();
      //阻塞当前线程,直到获取任务的执行结果
      leftTask.join();
      rightTask.join();

      // 合并排序结果
      arrayToSort = MergeSort.merge(leftTask.getSortedArray(), rightTask.getSortedArray());
   }

   public int[] getSortedArray() {
      return arrayToSort;
   }
}

在这个示例中,我们使用Fork/Join框架实现了归并排序算法,并通过递归调用实现了并行化。使用Fork/Join框架实现归并排序算法的关键在于将排序任务分解成小的任务,使用Fork/Join框架将这些小任务提交给线程池中的不同线程并行执行,并在最后将排序后的结果进行合并。这样可以充分利用多核CPU的并行处理能力,提高程序的执行效率。
测试结果对比
测试代码

public class Utils {

   /**
    * 随机生成数组
    * @param size 数组的大小
    * @return
    */
   public static int[] buildRandomIntArray(final int size) {
      int[] arrayToCalculateSumOf = new int[size];
      Random generator = new Random();
      for (int i = 0; i < arrayToCalculateSumOf.length; i++) {
         arrayToCalculateSumOf[i] = generator.nextInt(100000000);
      }
      return arrayToCalculateSumOf;
   }
}
public class ArrayToSortMain {

    public static void main(String[] args) {
        //生成测试数组  用于归并排序
        int[] arrayToSortByMergeSort = Utils.buildRandomIntArray(20000000);
        //生成测试数组  用于forkjoin排序
        int[] arrayToSortByForkJoin = Arrays.copyOf(arrayToSortByMergeSort, arrayToSortByMergeSort.length);
        //获取处理器数量
        int processors = Runtime.getRuntime().availableProcessors();


        MergeSort mergeSort = new MergeSort(arrayToSortByMergeSort, processors);
        long startTime = System.nanoTime();
        // 归并排序
        mergeSort.mergeSort();
        long duration = System.nanoTime()-startTime;
        System.out.println("单线程归并排序时间: "+(duration/(1000f*1000f))+"毫秒");
        

        //利用forkjoin排序
        MergeSortTask mergeSortTask = new MergeSortTask(arrayToSortByForkJoin, processors);
        //构建forkjoin线程池
        ForkJoinPool forkJoinPool = new ForkJoinPool(processors);
        startTime = System.nanoTime();
        //执行排序任务
        forkJoinPool.invoke(mergeSortTask);
        duration = System.nanoTime()-startTime;
        System.out.println("forkjoin排序时间: "+(duration/(1000f*1000f))+"毫秒");
        
    }
}

根据测试结果可以看出,数组越大,利用Fork/Join框架实现的并行化归并排序比单线程归并排序的效率更高:在这里插入图片描述

2.4 并行实现归并排序的优化和注意事项

在实际应用中,我们需要考虑数据分布的均匀性、内存使用情况、线程切换开销等因素,以充分利用多核CPU并保证算法的正确性和效率。
以下是并行实现归并排序的一些优化和注意事项:

  • 任务的大小:任务大小的选择会影响并行算法的效率和负载均衡,如果任务太小,会造成任务划分和合并的开销过大;如果任务太大,会导致任务无法充分利用多核CPU并行处理能力。因此,在实际应用中需要根据数据量、CPU核心数等因素选择合适的任务大小。
  • 负载均衡:并行算法需要保证负载均衡,即各个线程执行的任务大小和时间应该尽可能相等,否则会导致某些线程负载过重,而其他线程负载过轻的情况。在归并排序中,可以通过递归调用实现负载均衡,但是需要注意递归的层数不能太深,否则会导致任务划分和合并的开销过大。
  • 数据分布:数据分布的均匀性也会影响并行算法的效率和负载均衡。在归并排序中,如果数据分布不均匀,会导致某些线程处理的数据量过大,而其他线程处理的数据量过小的情况。因此,在实际应用中需要考虑数据的分布情况,尽可能将数据分成大小相等的子数组。
  • 内存使用:并行算法需要考虑内存的使用情况,特别是在处理大规模数据时,内存的使用情况会对算法的执行效率产生重要影响。在归并排序中,可以通过对数据进行原地归并实现内存的节约,但是需要注意归并的实现方式,以避免数据的覆盖和不稳定排序等问题。
  • 线程切换:线程切换是并行算法的一个重要开销,需要尽量减少线程的切换次数,以提高算法的执行效率。在归并排序中,可以通过设置线程池的大小和调整任务大小等方式控制线程的数量和切换开销,以实现算法的最优性能。

3. Fork/Join框架介绍

3.1 什么是Fork/Join

Fork/Join是一个是一个并行计算的框架,主要就是用来支持分治任务模型的,这个计算框架里的 Fork 对应的是分治任务模型里的任务分解,Join 对应的是结果合并。它的核心思想是将一个大任务分成许多小任务,然后并行执行这些小任务,最终将它们的结果合并成一个大的结果。它适用于可以采用分治策略的计算密集型任务,例如大规模数组的排序、图形的渲染、复杂算法的求解等。在这里插入图片描述

3.2 应用场景

1.并行计算:
ForkJoinPool 提供了一种方便的方式来执行大规模的计算任务,并充分利用多核处理器的性能优势。通过将大任务分解成小任务,并通过工作窃取算法实现任务的并行执行,可以提高计算效率。
2.递归任务处理:
ForkJoinPool 特别适用于递归式的任务分解和执行。它可以将一个大任务递归地分解成许多小任务,并通过工作窃取算法动态地将这些小任务分配给工作线程执行。
3.并行流操作:
Java 8 引入了 Stream API,用于对集合进行函数式编程风格的操作。ForkJoinPool 通常用于执行并行流操作中的并行计算部分,例如对流中的元素进行过滤、映射、聚合等操作。
4.高性能任务执行:
ForkJoinPool 提供了一种高性能的任务执行机制,通过对任务进行动态调度和线程池管理,可以有效地利用系统资源,并在多核处理器上实现任务的并行执行。
总的来说,ForkJoinPool 类在 Java 中具有广泛的应用场景,特别适用于大规模的并行计算任务和递归式的任务处理。它通过工作窃取算法和任务分割合并机制,提供了一种高效的并行计算方式,可以显著提高计算效率和性能。

3.3 Fork/Join使用

Fork/Join框架的主要组成部分是ForkJoinPool、ForkJoinTask。ForkJoinPool是一个线程池,它用于管理ForkJoin任务的执行。ForkJoinTask是一个抽象类,用于表示可以被分割成更小部分的任务。
ForkJoinPool
ForkJoinPool是Fork/Join框架中的线程池类,它用于管理Fork/Join任务的线程。ForkJoinPool类包括一些重要的方法,例如submit()、invoke()、shutdown()、awaitTermination()等,用于提交任务、执行任务、关闭线程池和等待任务的执行结果。ForkJoinPool类中还包括一些参数,例如线程池的大小、工作线程的优先级、任务队列的容量等,可以根据具体的应用场景进行设置。
构造器在这里插入图片描述
ForkJoinPool中有四个核心参数,用于控制线程池的并行数、工作线程的创建、异常处理和模式指定等。各参数解释如下:

  • int parallelism:指定并行级别(parallelism level)。ForkJoinPool将根据这个设定,决定工作线程的数量。如果未设置的话,将使用Runtime.getRuntime().availableProcessors()来设置并行级别;
  • ForkJoinWorkerThreadFactory factory:ForkJoinPool在创建线程时,会通过factory来创建。注意,这里需要实现的是ForkJoinWorkerThreadFactory,而不是ThreadFactory。如果你不指定factory,那么将由默认的DefaultForkJoinWorkerThreadFactory负责线程的创建工作;
  • UncaughtExceptionHandler handler:指定异常处理器,当任务在运行中出错时,将由设定的处理器处理;
  • boolean asyncMode:设置队列的工作模式。当asyncMode为true时,将使用先进先出队列,而为false时则使用后进先出的模式。
//获取处理器数量
int processors = Runtime.getRuntime().availableProcessors();
//构建forkjoin线程池
ForkJoinPool forkJoinPool = new ForkJoinPool(processors);

任务提交方式
任务提交是ForkJoinPool的核心能力之一,提交任务有三种方式:

返回值方法
提交异步执行voidexecute(ForkJoinTask<?> task) execute(Runnable task)
等待并获取结果Tinvoke(ForkJoinTask task)
提交执行获取Future结果ForkJoinTasksubmit(ForkJoinTask task) submit(Callable task) submit(Runnable task) submit(Runnable task, T result)

ForkJoinTask
ForkJoinTask是Fork/Join框架中的抽象类,它定义了执行任务的基本接口。用户可以通过继承ForkJoinTask类来实现自己的任务类,并重写其中的compute()方法来定义任务的执行逻辑。通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下三个子类:

  • RecursiveAction:用于递归执行但不需要返回结果的任务。
  • RecursiveTask :用于递归执行需要返回结果的任务。
  • CountedCompleter :在任务完成执行后会触发执行一个自定义的钩子函数

调用方法
ForkJoinTask 最核心的是 fork() 方法和 join() 方法,承载着主要的任务协调作用,一个用于任务提交,一个用于结果获取。

  • fork()——提交任务
    fork()方法用于向当前任务所运行的线程池中提交任务。如果当前线程是ForkJoinWorkerThread类型,将会放入该线程的工作队列,否则放入common线程池的工作队列中。
  • join()——获取任务执行结果
    join()方法用于获取任务的执行结果。调用join()时,将阻塞当前线程直到对应的子任务完成运行并返回结果。
    处理递归任务
    计算斐波那契数列
    斐波那契数列指的是这样一个数列:1,1,2,3,5,8,13,21,34,55,89… 这个数列从第3项开始,每一项都等于前两项之和。
public class Fibonacci extends RecursiveTask<Integer> {
    final int n;

    Fibonacci(int n) {
        this.n = n;
    }

    /**
     * 重写RecursiveTask的compute()方法
     * @return
     */
    protected Integer compute() {
        if (n <= 1)
            return n;
        Fibonacci f1 = new Fibonacci(n - 1);
        //提交任务
        f1.fork();
        Fibonacci f2 = new Fibonacci(n - 2);
        //合并结果
        return f2.compute() + f1.join();
    }

    public static void main(String[] args) {
        //构建forkjoin线程池
        ForkJoinPool pool = new ForkJoinPool();
        Fibonacci task = new Fibonacci(10);
        //提交任务并一直阻塞直到任务 执行完成返回合并结果。
        int result = pool.invoke(task);
        System.out.println(result);
    }
}

思考:如果n为100000,执行上面的代码会发生什么问题?在这里插入图片描述
在上面的例子中,由于递归计算Fibonacci数列的任务数量呈指数级增长,当n较大时,就容易出现StackOverflowError错误。这个错误通常发生在递归过程中,由于递归过程中每次调用函数都会在栈中创建一个新的栈帧,当递归深度过大时,栈空间就会被耗尽,导致StackOverflowError错误。
栈溢出如何解决
我们可以使用迭代的方式计算Fibonacci数列,以避免递归过程中占用大量的栈空间。下面是一个使用迭代方式计算Fibonacci数列的例子:

public class Fibonacci {
    public static void main(String[] args) {
        int n = 100000;
        long[] fib = new long[n + 1];
        fib[0] = 0;
        fib[1] = 1;
        for (int i = 2; i <= n; i++) {
            fib[i] = fib[i - 1] + fib[i - 2];
        }
        System.out.println(fib[n]);
    }
}

处理递归任务注意事项

对于一些递归深度较大的任务,使用Fork/Join框架可能会出现任务调度和内存消耗的问题。
当递归深度较大时,会产生大量的子任务,这些子任务可能被调度到不同的线程中执行,而线程的创建和销毁以及任务调度的开销都会占用大量的资源,从而导致性能下降。
此外,对于递归深度较大的任务,由于每个子任务所占用的栈空间较大,可能会导致内存消耗过大,从而引起内存溢出的问题。
因此,在使用Fork/Join框架处理递归任务时,需要根据实际情况来评估递归深度和任务粒度,以避免任务调度和内存消耗的问题。如果递归深度较大,可以尝试采用其他方法来优化算法,如使用迭代方式替代递归,或者限制递归深度来减少任务数量,以避免Fork/Join框架的缺点。

处理阻塞任务
在ForkJoinPool中使用阻塞型任务时需要注意以下几点:
1.防止线程饥饿:当一个线程在执行一个阻塞型任务时,它将会一直等待任务完成,这时如果没有其他线程可以窃取任务,那么该线程将一直被阻塞,直到任务完成为止。为了避免这种情况,应该避免在ForkJoinPool中提交大量的阻塞型任务。
2.使用特定的线程池:为了最大程度地利用ForkJoinPool的性能,可以使用专门的线程池来处理阻塞型任务,这些线程不会被ForkJoinPool的窃取机制所影响。例如,可以使用ThreadPoolExecutor来创建一个线程池,然后将这个线程池作为ForkJoinPool的执行器,这样就可以使用ThreadPoolExecutor来处理阻塞型任务,而使用ForkJoinPool来处理非阻塞型任务。
3.不要阻塞工作线程:如果在ForkJoinPool中使用阻塞型任务,那么需要确保这些任务不会阻塞工作线程,否则会导致整个线程池的性能下降。为了避免这种情况,可以将阻塞型任务提交到一个专门的线程池中,或者使用CompletableFuture等异步编程工具来处理阻塞型任务。
下面是一个使用阻塞型任务的例子,这个例子展示了如何使用CompletableFuture来处理阻塞型任务:

public class BlockingTaskDemo {
    public static void main(String[] args) {
        //构建一个forkjoin线程池
        ForkJoinPool pool = new ForkJoinPool();

        //创建一个异步任务,并将其提交到ForkJoinPool中执行
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                // 模拟一个耗时的任务
                TimeUnit.SECONDS.sleep(5);
                return "Hello, world!";
            } catch (InterruptedException e) {
                e.printStackTrace();
                return null;
            }
        }, pool);

        try {
            // 等待任务完成,并获取结果
            String result = future.get();

            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } finally {
            //关闭ForkJoinPool,释放资源
            pool.shutdown();
        }
    }
}

在这个例子中,我们使用了CompletableFuture来处理阻塞型任务,因为它可以避免阻塞ForkJoinPool中的工作线程。另外,我们也可以使用专门的线程池来处理阻塞型任务,例如ThreadPoolExecutor等。不管是哪种方式,都需要避免在ForkJoinPool中提交大量的阻塞型任务,以免影响整个线程池的性能。

3.4 ForkJoinPool工作原理

ForkJoinPool 内部有多个任务队列,当我们通过 ForkJoinPool 的 invoke() 或者 submit() 方法提交任务时,ForkJoinPool 根据一定的路由规则把任务提交到一个任务队列中,如果任务在执行过程中会创建出子任务,那么子任务会提交到工作线程对应的任务队列中。
如果工作线程对应的任务队列空了,是不是就没活儿干了呢?不是的,ForkJoinPool 支持一种叫做“任务窃取”的机制,如果工作线程空闲了,那它可以“窃取”其他工作任务队列里的任务。如此一来,所有的工作线程都不会闲下来了。
核心设计:

  • ForkJoinPool的任务会被内部存储了一个WorkQueue数组,提交给ForkJoinPool的任务会被分配到指定的WorkQueue上执行
  • 每个WorkQueue内部维护了一个ForkJoinTask数组用来存储待执行的任务,以及一个独立的ForkJoinWorkerThread用来真正执行任务

工作线程ForkJoinWorkerThread
ForkJoinWorkerThread是ForkJoinPool中的一个专门用于执行任务的线程。当一个ForkJoinWorkerThread被创建时,它会自动注册一个WorkQueue到ForkJoinPool中。这个WorkQueue是该线程专门用于存储自己的任务的队列,只能出现在WorkQueues[]的奇数位。
ForkJoinWorkerThread工作线程启动后就会扫描偷取任务执行,另外当其在 ForkJoinTask#join() 等待返回结果时如果被 ForkJoinPool 线程池发现其任务队列为空或者已经将当前任务执行完毕,也会通过工作窃取算法从其他任务队列中获取任务分配到其任务队列中并执行。在这里插入图片描述
工作队列WorkQueue
WorkQueue是一个双端队列,用于存储工作线程自己的任务。每个工作线程都会维护一个本地的WorkQueue,并且优先执行本地队列中的任务。当本地队列中的任务执行完毕后,工作线程会尝试从其他线程的WorkQueue中窃取任务。在这里插入图片描述
工作窃取
ForkJoinPool与ThreadPoolExecutor有个很大的不同之处在于,ForkJoinPool引入了工作窃取设计,它是其性能保证的关键之一。工作窃取,就是允许空闲线程从繁忙线程的双端队列中窃取任务。默认情况下,工作线程从它自己的双端队列的头部获取任务。但是,当自己的任务为空时,线程会从其他繁忙线程双端队列的尾部中获取任务。这种方法,最大限度地减少了线程竞争任务的可能性。
在这里插入图片描述

通过工作窃取,Fork/Join框架可以实现任务的自动负载均衡,以充分利用多核CPU的计算能力,同时也可以避免线程的饥饿和延迟问题

3.5 ForkJoinPool执行流程

在这里插入图片描述

3.6 总结

Fork/Join是一种基于分治思想的模型,在并发处理计算型任务时有着显著的优势。其效率的提升主要得益于两个方面:

  • 任务切分:将大的任务分割成更小粒度的小任务,让更多的线程参与执行;
  • 任务窃取:通过任务窃取,充分地利用空闲线程,并减少竞争。
    在使用ForkJoinPool时,需要特别注意任务的类型是否为纯函数计算类型,也就是这些任务不应该关心状态或者外界的变化,这样才是最安全的做法。如果是阻塞类型任务,那么你需要谨慎评估技术方案。虽然ForkJoinPool也能处理阻塞类型任务,但可能会带来复杂的管理成本。

和普通线程池之间的区别

  • 工作窃取算法
    ForkJoinPool采用工作窃取算法来提高线程的利用率,而普通线程池则采用任务队列来管理任务。在工作窃取算法中,当一个线程完成自己的任务后,它可以从其它线程的队列中获取一个任务来执行,以此来提高线程的利用率。
  • 任务的分解和合并
    ForkJoinPool可以将一个大任务分解为多个小任务,并行地执行这些小任务,最终将它们的结果合并起来得到最终结果。而普通线程池只能按照提交的任务顺序一个一个地执行任务。
  • 工作线程的数量
    ForkJoinPool会根据当前系统的CPU核心数来自动设置工作线程的数量,以最大限度地发挥CPU的性能优势。而普通线程池需要手动设置线程池的大小,如果设置不合理,可能会导致线程过多或过少,从而影响程序的性能。
  • 任务类型
    ForkJoinPool适用于执行大规模任务并行化,而普通线程池适用于执行一些短小的任务,如处理请求等。

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

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

相关文章

重头开始嵌入式第四十二天(硬件 ARM体系架构)

目录 一&#xff0c;ARM是什么&#xff1f; 1.公司名称 ARM的主流架构&#xff1a; 2.处理器架构 二&#xff0c;什么是处理器架构&#xff1f;什么是处理器&#xff1f; 一、处理器 二、处理器架构 三&#xff0c;一个计算机由什么构成呢&#xff1f; 一、硬件系统 二…

SDK3(note上)

搞了举个窗口设置还有鼠标处理的信息 注释写在代码中了 #include <windows.h> #include<tchar.h> #include <stdio.h> #include <strsafe.h>/*鼠标消息 * 键盘消息 快捷键消息 菜单消息 控件消息 自定义消息 窗口消息 客户区域的概念(Client Aera) 非…

什么是 SIP 及 IMS 中的 Forking

目录 1. SIP 网络中 Forking 的定义 2. SIP Forking 的分类 2.1 Oaraller Forking 的分类 2.2 Sequential Forking 的分类 博主wx:yuanlai45_csdn 博主qq:2777137742 后期会创建粉丝群,为同学们提供分享交流平台以及提供官方发送的福利奖品~ 1. SIP 网络中 Forking 的定…

828华为云征文|华为云Flexus云服务器X实例部署immich相片管理系统

828华为云征文&#xff5c;华为云Flexus云服务器X实例部署immich相片管理系统 前言一、Flexus云服务器X实例介绍1.1 Flexus云服务器X实例简介1.2 Flexus云服务器X实例特点1.3 Flexus云服务器X实例使用场景 二、immich介绍2.1 immich简介2.2 immich注意事项2.3 主要特点2.4 使用…

AI预测体彩排3采取888=3策略+和值012路或胆码测试9月25日升级新模型预测第91弹

经过90多期的测试&#xff0c;当然有很多彩友也一直在观察我每天发的预测结果&#xff0c;得到了一个非常有价值的信息&#xff0c;那就是9码定位的命中率非常高&#xff0c;已到达90%的命中率&#xff0c;这给喜欢打私菜的朋友提供了极高价值的预测结果~当然了&#xff0c;大部…

【Python】利用Python+thinker实现旋转转盘

需求/目的&#xff1a;用Pythonthinker实现转盘&#xff0c;并且能够随机旋转任意角度。 转盘形式&#xff1a; 主界面&#xff1a; from tkinter import *winTk() win.title("大转盘") win.geometry("300x400")win.mainloop() 转盘绘制&#xff1a; 这…

YOLO系列训练生成的exp进行处理找出mAP值

YOLOv8或者YOLOv10训练生成的exp文件进行遍历,找出results.csv文件中metrics/mAP50(B)、metrics/mAP50-95(B)值和args.yaml中的mode文件,将他们保存到一个excel文件中。 YOLO训练train.py代码如下: 模型训练好后生成exp文件如下: 生成excel文件完整代码如下: import o…

ad14转cadence17.4

一、原理图转换 将原理图文件拖入到新建工程中

专业之选!行业专家力荐的十大文件摆渡系统,你选对了吗?

在信息化时代&#xff0c;数据的流通与共享已经成为企业运营中不可或缺的一部分。为了应对日益增长的数据传输需求&#xff0c;各大厂商纷纷推出了各类文件摆渡系统&#xff0c;以确保数据传输的安全性、高效性和便捷性。以下是行业专家力荐的十大文件摆渡系统&#xff0c;供您…

朗国电子(嵌入式)

1.[问答题]已知单片机内置10位精度的ADC&#xff0c;单片机工作电压1.8V&#xff0c;ADC基准电压1.8V&#xff0c;请计算ADC采样的电压最小分辨率是多少?请写出计算过程和结果;如果单片机ADC输入口电压为0.9V&#xff0c;则采样得到的值是多少?请写出计算过程和结果。 2.[问答…

2024 年最新 Protobuf 结构化数据序列化和反序列化详细教程

Protobuf 序列化概述 Protobuf&#xff08;Protocol Buffers&#xff09;是由Google开发的一种语言中立、平台中立、可扩展的序列化结构数据的方法。它用于在不同系统之间高效地交换数据。Protobuf使用定义文件&#xff08;.proto&#xff09;来描述数据结构&#xff0c;并通过…

excel导出图片---HSSFWorkbook--SXSSFWorkbook

1 概述 平时在工作中&#xff0c;excel导出图片经常会用到&#xff0c;但奈何HSSFWorkbook导出数据数量有限制问题&#xff0c;所以企业里大多都用SXSSFWorkbook格式&#xff0c;很少用HSSFWorkbook。所以今天以这两种格式分别记录下&#xff0c;图片的导出过程。 2 HSSFWork…

【刷题3】找到字符串中所有字母异位词、串联所有单词的子串

目录 一、找到字符串中所有字母异位词二、串联所有单词的子串 一、找到字符串中所有字母异位词 题目&#xff1a; 思路&#xff1a; 用一个变量count来统计有效字符的个数。哈希表2统计字符串p的每个字符出现的个数&#xff0c;然后遍历字符串s&#xff0c;先进窗口&#xf…

怎么测试射频芯片质量的好坏?

无论是手机通信&#xff0c;还是卫星导航&#xff0c;射频芯片都是其核心组件之一。本文将探讨如何准确判断射频芯片的质量&#xff0c;以确保技术设备的稳定运行。 1. 外观检查 检查射频芯片是否有破损、引脚断裂、缺陷等。 2. 电气参数测试 对射频芯片的输入输出阻抗、功耗、…

DBAPI如何实现插入数据前先判断数据是否存在,存在就更新,不存在就插入

DBAPI实现数据不存在即插入、存在即更新 场景 往数据库插入数据的时候&#xff0c;需要先判断一下记录是否在数据库已经存在&#xff0c;如果已经存在就更新记录&#xff0c;如果不存在&#xff0c;才插入数据。 实现方案 采用存储过程实现&#xff0c;以mysql为例子 创建存储过…

【运维系列资料】运维系统建设方案(PPT源文件)

1.智慧运维系统建设背景 2.智慧运维系统建设目标 3.智慧运维系统建设内容 4.智慧运维系统建设技术 5.智慧运维系统建设流程 6.智慧运维系统建设收益 软件全套资料部分文档清单&#xff1a; 工作安排任务书&#xff0c;可行性分析报告&#xff0c;立项申请审批表&#xff0c;产品…

小阿轩yx-Ansible部署与应用基础

小阿轩yx-Ansible部署与应用基础 前言 由于互联网的快速发展导致产品更新换代速度逐步增长&#xff0c;运维人员每天都要进行大量的维护操作&#xff0c;按照传统方式进行维护使得工作效率低下。这时部署自动化运维就可以尽可能安全、高效的完成这些工作。 Ansible 概述 什…

编程遇到问题了?一个命令让 AI 解决你的困惑!

作为一名程序员&#xff0c;我们在开发过程中常常会遇到各种各样的问题&#xff0c;尤其是在开发新功能或使用新技术时。在这些时候&#xff0c;我们往往会寻求帮助并寻找最佳的解决方案。 去年的时候大家还是在使用百度或者谷歌来搜寻问题的答案&#xff0c;但是今年大家肯定…

MICS:PythonJail沙箱逃逸(持续更新中)

沙箱是一种防护机制&#xff0c;是用来运行不受信任的代码&#xff0c;通常是用户上传的代码&#xff0c;但这些代码可能是恶意代码&#xff0c;而沙箱就是防止恶意代码运行的机制。所谓沙箱逃逸&#xff0c;就是利用相关操作绕过沙箱防护&#xff0c;从而获得目标主机的文件信…

JVM(HotSpot):堆空间(Heap)以及常用相关工具介绍

文章目录 内存结构图二、堆的定义三、堆内存溢出四、堆内存排查工具 内存结构图 二、堆的定义 1、通过new关键字创建的对象&#xff0c;都会放到堆空间中。 2、它是线程共享的&#xff0c;堆中的对象都要考虑线程安全问题。 那有同学肯定会问&#xff0c;方法内通过new创建的…