单核中的多线程与异步编程

Posted

技术标签:

【中文标题】单核中的多线程与异步编程【英文标题】:Multithreaded vs Asynchronous programming in a single core 【发布时间】:2021-12-14 03:24:41 【问题描述】:

如果 CPU 一次只实时执行一项任务,那么多线程与单处理器系统中的异步编程(在效率方面)有何不同?

假设我们必须从 1 数到 IntegerMax。在我的多核机器的以下程序中,两个线程的最终计数几乎是单线程计数的一半。如果我们在单核机器上运行它会怎样?有什么方法可以实现同样的结果吗?

class Demonstration 
    public static void main( String args[] ) throws InterruptedException 
        SumUpExample.runTest();
    


class SumUpExample 

    long startRange;
    long endRange;
    long counter = 0;
    static long MAX_NUM = Integer.MAX_VALUE;

    public SumUpExample(long startRange, long endRange) 
        this.startRange = startRange;
        this.endRange = endRange;
    

    public void add() 

        for (long i = startRange; i <= endRange; i++) 
            counter += i;
        
    

    static public void twoThreads() throws InterruptedException 

        long start = System.currentTimeMillis();
        SumUpExample s1 = new SumUpExample(1, MAX_NUM / 2);
        SumUpExample s2 = new SumUpExample(1 + (MAX_NUM / 2), MAX_NUM);

        Thread t1 = new Thread(() -> 
            s1.add();
        );

        Thread t2 = new Thread(() -> 
            s2.add();
        );

        t1.start();
        t2.start();
        
        t1.join();
        t2.join();

        long finalCount = s1.counter + s2.counter;
        long end = System.currentTimeMillis();
        System.out.println("Two threads final count = " + finalCount + " took " + (end - start));
    

    static public void oneThread() 

        long start = System.currentTimeMillis();
        SumUpExample s = new SumUpExample(1, MAX_NUM );
        s.add();
        long end = System.currentTimeMillis();
        System.out.println("Single thread final count = " + s.counter + " took " + (end - start));
    


    public static void runTest() throws InterruptedException 

        oneThread();
        twoThreads();

    

输出:

Single thread final count = 2305843008139952128 took 1003
Two threads final count = 2305843008139952128 took 540

【问题讨论】:

IDK 关于效率,但编写线程代码而不是执行异步代码的最初原因是为了可读性。每个线程都可以像我们在初学者时都学会编写的简单的单线程程序程序一样。当您有多个异步活动正在进行时,程序必须显式存储每个活动的状态,并且必须显式地从活动切换到活动。对于线程,每个活动的状态是隐式在其线程的局部变量中的,所有的调度都由“系统”为你处理。 【参考方案1】:

对于纯粹受 CPU 限制的操作,您是正确的。大多数(99.9999%)程序需要进行输入、输出和调用其他服务。这些速度比 CPU 慢几个数量级,因此在等待外部操作的结果时,操作系统可以在时间片中调度和运行其他(许多其他)进程。

硬件多线程主要在满足两个条件时受益:

    CPU 密集型操作; 可以有效地划分为独立的子集

或者您有许多不同的任务要运行,可以在多个硬件处理器之间有效地划分。

【讨论】:

那么我们可以得出结论,除非发明了多处理器系统,否则就没有使用多线程的概念吗?因为当时异步编程本可以达到同样的效果。 相反,从 1960 年代初开始,多道程序(多线程)被大量使用,正是因为 I/O 太慢了。 所以你的意思是说它是用javascript(不支持多线程)使用异步编程的方式使用的? 部分。这不是一个教育网站,我们无法解释操作系统如何管理资源的所有细节。网络上有很多网站都非常详细地介绍了这一切,您应该先做一些研究。 谢谢 在我浏览了多篇解释多线程、异步和并发等的文章之后,我只是想弄清楚我的概念。这让我有点困惑。这就是我使用***作为媒介的原因。但现在想想我走在正确的轨道上:)【参考方案2】:

在我的多核机器的以下程序中,两个线程的最终计数几乎是单线程计数的一半。

当应用程序使用两个内核时,这就是我对有效基准测试的期望。

然而,看着你的代码,我有点惊讶你得到这些结果......如此可靠。

您的基准测试没有考虑 JVM 预热效应,尤其是 JIT 编译。

JIT 编译器可能会优化您的基准测试的add 方法以完全摆脱循环。 (但至少这些计数被“使用”了......通过打印出来。)

我猜你很幸运......但我不相信这些结果对于所有 Java 版本都是可重现的,或者如果你调整了基准。

请阅读:

How do I write a correct micro-benchmark in Java?

如果我们在单核机器上运行它会怎样?

假设如下:

您重写了基准以纠正上述缺陷。 您在禁用硬件超线程12的系统上运行。

那么...我希望它需要两个线程才能花费 超过 两倍于一个线程版本。

问:为什么是“超过”?

A:因为启动一个新线程有很大的开销。根据您的硬件、操作系统和 Java 版本,它可能超过一毫秒。当然,如果您反复使用和丢弃线程,所花费的时间会很长。


我们有什么办法可以达到同样的效果吗?

不知道你在这里问什么。但是,如果您要问如何在多核机器上模拟一个内核的行为,您可能需要在操作系统级别执行此操作。请参阅https://superuser.com/questions/309617(适用于 Windows)和 https://askubuntu.com/questions/483824(适用于 Linux)。


1 - Hyperthreading 是一种硬件优化,其中单个内核的处理硬件支持(通常)两个超线程。每个超线程 有自己的寄存器集,但它与另一个超线程共享诸如 ALU 之类的功能单元。因此,这两个超线程的行为类似于(通常)两个内核,只是它们可能更慢,具体取决于精确的指令组合。典型的操作系统会将超线程处理,就好像它是一个常规内核一样。超线程通常在启动时启用/禁用;例如通过 Bios 设置。 2 - 如果启用了超线程,在这样的 CPU 密集型计算中,两个 Java 线程的速度可能不会是一个的两倍……因为各个内核上的“其他”超线程可能会导致速度变慢。有人提到基准测试很复杂吗?

【讨论】:

以上是关于单核中的多线程与异步编程的主要内容,如果未能解决你的问题,请参考以下文章

我理解的JavaScript异步编程

并发编程 - 总结

Python异步编程全攻略

Dart 中的多线程 与 Future

C#的多线程——使用async和await来完成异步编程(Asynchronous Programming with async and await)

java同步异步和多线程编程