并发和串行、并行的概念

先抛开语言不管,只聊概念,说起并发,就很容易想到它和串行并行的区别。

串行:一次只能取得一个任务并执行这个任务,这个任务执行完后面的任务才能继续;

并发:指的是在同一个时间段内,多条指令在CPU上同时执行;

并行:指的是在同一时刻,多条指令在CPU上同时执行。

并发和并行都是为了充分利用 CPU 多核计算资源提出来的概念。并发程序其实并不要求 CPU 具备多核计算的能力,在同一时间段内,多个线程会被分配一定的执行时间片,在 CPU 上被快速轮换执行。

串行还是并发,这都是任务安排者视角看到的东西。前者要求你看到前一个任务结束了,下一个任务才能安排;而后者呢,你可以同时提交许多任务,执行者(们)之间会相互协调并自己安排执行顺序(但未必合理,比如可能出现死锁),你把任务安排下去就不用管了。相比之下,“并行”是任务执行者视角的东西,和前两者所处平面不同。

尤其是,纠正一个错误的观念:并不像一般人以为的“单核单线程没有并行”;事实上,哪怕用了单核单线程 CPU 的电脑,它上面也存在真正的“并行”。只不过,这个并行并不是 CPU 内部的、线程之间的并行;而是 CPU 执行程序的同时,DMA 控制器也在执行着网络报文收发、磁盘读写、音视频播放/录制等等任务。

典型案例:你买了个新硬盘,打算把自己的重要文件复制过去。于是你找到 music 目录,把所有的音乐文件夹选中,复制 50G 音乐到新硬盘;然后打开 photo 目录,把100G照片复制到新硬盘;又打开 mov 目录,把 800G 视频复制到新硬盘……

最后,你看到 Windows 显示了10个文件复制窗口;其中一个窗口的提示是“还有一千六百个文件待复制,需要三天零八小时七分钟三十二秒”。

这就是典型的“并发”任务。

在这个场景里,你同时启动了10个文件复制进程,帮你复制十大类文件。如果没有“并发”支持,你只能先复制一个文件夹,等上半小时,看它复制完了才能继续复制下一个。这当然很累人。一旦有了并发支持,你就能同时启动十个复制任务。在计算机忙碌的同时,你完全可以出去旅个游。

但是,细心的你可能会注意到:如果这十个文件复制任务没有分成十个进程去做,而是写个批处理甚至干脆用 Linux 的 dd 命令全盘复制,那么复制完所有文件只需五六个小时。这是因为,十个进程会彼此争抢资源;而每次进程执行权切换,硬盘就不得不重新寻道——这是非常非常浪费时间的。其结果,就是把本来五六个小时就能搞定的事情,争抢成了三天都搞不定……

换句话说,这里面没有并行,只有并发。

说的更清晰点,对电脑操作者,你的确是“并发”了十个任务;但对程序这个执行者来说,它们仍然是“串行”使用硬盘——进程1用200ms,交出控制权;换进程2用200ms硬盘,交出控制权;然后是进程3、4、5、6、7……

它们只是快速切换执行权、从而让你得到了一个“同时执行”的假象而已。

因此,对这类任务,其实你还是自己写个批处理更好。节省你的生命,也节省硬盘的使用寿命。

当然,如果你的两台电脑分别装了块新硬盘,显然它们对各自硬盘读写就是“并行”的,互不干扰。你完全可以用第三台电脑远程登陆上去,然后分别在两者上面启动各自的复制进程。只要没有数据相关,先让电脑A复制完再去捣鼓电脑B,这显然是不明智的。

类似的,同一台电脑里面,网卡收发信息和硬盘读写并不相关;CPU忙碌时让显卡空闲也是极大的浪费。换句话说,不同任务有不同的执行实体;那么我们当然不应该“在CPU上执行任务A”时“禁止任务B使用网卡”。没错,只要执行任务的硬件不同(包括但不限于不同的CPU核心、网卡A和网卡B、C、D、显卡、硬盘、打印机等等),它们就可以并行工作。

一个好的程序,一方面不应该在单个硬件上造成过多切换(比如在一块硬盘上同时开10个文件复制进程就是一种极其低效的使用方式),另一方面则要尽量利用每个空闲的硬件(比如任务A使用硬盘时应该允许任务B使用网卡),这才不至于降低执行效率、使得硬件使用不够充分。

综上,串行在执行单个简单任务时,执行速率是最高的。因为完全没有干扰,任何硬件想用就用。但是,串行方式的硬件利用率不高。比如当某个任务不需要使用打印机时,在它完成之前,打印机就只能闲置。

为了解决这个问题,我们首先要允许“并发”。“并发”的意思是,你可以同时提交多个任务,但系统并不能保证它们可以并行执行。甚至于,在极端的、类似“单个硬盘上同时启动10个复制进程”的场景里,“并发”反而引起了过多的切换动作,成几倍甚至几十倍的降低了文件复制效率——这种场景下,并发甚至要不如串行。

想要提高并发的效率,我们就必须深入进去,关注“这些任务之间究竟有没有出现并行”。比如,如果文件复制程序写的非常糙,那么很可能是“先从旧硬盘读取数据,然后写入新硬盘;数据写入新硬盘后,继续从旧硬盘读取数据”。这在单硬盘上是合理的,少了一些寻道操作;但在两块硬盘的场景下,这就相当于“串行使用两块硬盘”,这个利用效率显然太低了,每块硬盘只有50%左右的利用率(当然,现代OS会主动多读一些数据到磁盘缓存,这个机制可以有效提高硬盘利用率)。那么,如果同时启动两个复制进程,反而会不时出现“进程A读旧硬盘,同时进程B写新硬盘”这种场景,从而把每块硬盘的利用率提高到60%~80%。

换句话说,“并发”的确经常能让“并行”自然而然的出现,硬盘利用率也的确被提高了;只是这种提高缺乏保证(比如,运气不好时,复制进程A可能和进程B争着读取旧硬盘,从而导致很多不必要的寻道动作);而且,由于并发并不保证合理的执行顺序,反而经常“搬起石头砸自己的脚”。比如,一旦同时启动更多复制进程(比如三五个),那么过多的进程切换引起的过多的磁盘重新寻道动作就会抵消一切好处。

因此,很多时候,我们需要一个优秀的、头脑清醒的程序员;只有在他的有意识的安排下,才能在确保硬件利用率的同时、不因过多的争抢和切换降低执行效率。

参考:

并发和并行的区别

并发与并行的区别是什么? - 知乎

[并发概念] 串行、并行与并发_大唐雨夜的博客-CSDN博客_并行与串行的概念

Have Fun

  • 8
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,计算π的方法主要有两种:蒙特卡罗方法和数学公式法。这里以数学公式法为例,介绍Java的串行、并行并发计算π的实现方法。 1. 串行计算π 首先,我们可以使用莱布尼茨级数公式来计算π。该公式的计算公式为:π/4 = 1 - 1/3 + 1/5 - 1/7 + 1/9 - 1/11 + ... + (-1)^(n+1)/(2n-1)。我们可以通过循环来计算这个公式,直到达到预设的精度。下面是一个Java串行计算π的示例代码: ``` public class SerialPi { public static void main(String[] args) { long start = System.currentTimeMillis(); int n = 100000000; // 循环次数 double sum = 0.0; for (int i = 1; i <= n; i++) { double item = (i % 2 == 1 ? 1 : -1) * 1.0 / (2 * i - 1); sum += item; } double pi = 4 * sum; long end = System.currentTimeMillis(); System.out.println("Pi: " + pi); System.out.println("Time: " + (end - start) + "ms"); } } ``` 2. 并行计算π 对于计算π这种密集型的计算任务,我们可以使用并行计算来提高计算效率。Java中提供了多线程的机制来实现并行计算。我们可以将计算任务分配给多个线程来同时进行计算,最后将结果进行合并。下面是一个Java并行计算π的示例代码: ``` public class ParallelPi { private static final int NTHREADS = 4; // 线程数量 private static final int N = 100000000; // 循环次数 private static double[] sumArray = new double[NTHREADS]; public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); Thread[] threads = new Thread[NTHREADS]; for (int i = 0; i < NTHREADS; i++) { int id = i; threads[i] = new Thread(() -> { double sum = 0.0; for (int j = id; j < N; j += NTHREADS) { double item = (j % 2 == 1 ? 1 : -1) * 1.0 / (2 * j - 1); sum += item; } sumArray[id] = sum; }); threads[i].start(); } for (Thread thread : threads) { thread.join(); } double sum = 0.0; for (double item : sumArray) { sum += item; } double pi = 4 * sum; long end = System.currentTimeMillis(); System.out.println("Pi: " + pi); System.out.println("Time: " + (end - start) + "ms"); } } ``` 3. 并发计算π Java中的并发计算可以通过线程池来实现。线程池可以有效地管理线程的数量,避免线程创建和销毁的开销。下面是一个Java并发计算π的示例代码: ``` public class ConcurrentPi { private static final int NTHREADS = 4; // 线程数量 private static final int N = 100000000; // 循环次数 private static double[] sumArray = new double[NTHREADS]; public static void main(String[] args) throws InterruptedException, ExecutionException { long start = System.currentTimeMillis(); ExecutorService executor = Executors.newFixedThreadPool(NTHREADS); List<Future<Double>> futures = new ArrayList<>(); for (int i = 0; i < NTHREADS; i++) { int id = i; futures.add(executor.submit(() -> { double sum = 0.0; for (int j = id; j < N; j += NTHREADS) { double item = (j % 2 == 1 ? 1 : -1) * 1.0 / (2 * j - 1); sum += item; } return sum; })); } double sum = 0.0; for (Future<Double> future : futures) { sum += future.get(); } double pi = 4 * sum; long end = System.currentTimeMillis(); System.out.println("Pi: " + pi); System.out.println("Time: " + (end - start) + "ms"); executor.shutdown(); } } ``` 以上就是Java串行、并行并发计算π的示例代码,希望能对您有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值