1

我试图将我的大脑围绕并行/并发编程(在 Java 中)并被挂在我一直在阅读的任何教程中似乎没有涵盖的一些基础知识上。

当我们谈论“多线程”或“并行/并发编程”时,这是否意味着我们正在处理一个大问题并将其分散到多个线程上,或者我们是否首先明确地将其分解为更小的子问题,然后通过每个子问题都有自己的线程?

例如,假设我们有EndWorldHungerTask implements Runnable,并且任务完成了一些巨大的问题。为了完成它的目标,它必须做一些非常繁重的工作,比如一亿次:

public class EndWorldHungerTask implements Runnable {

    public void run() {
        for(int i = 0; i < 100000000; i++)
            someReallyExpensiveOperation();
    }
}

为了使这个“并发”或“多线程”,我们是否会将其传递EndWorldHungerTask给 100 个工作线程(JVM 告诉 100 个工作线程中的每一个何时处于活动状态并在下一次迭代/someReallyExpensiveOperation()调用中工作),还是我们会手动/显式地重构它,以便 100 个工作人员中的每一个都在循环/待完成工作的不同部分进行迭代?在这两种情况下,100 名工人中的每一个都只迭代了一百万次。

但是,在第一个范例下,Java 告诉每个线程何时执行。第二种情况下,开发者需要提前手动(在代码中)划分问题,并将每个子问题分配给一个新的线程。

我想我在问它在 Java 领域是如何“正常完成”的。而且,不仅针对这个问题,而且总体而言。

4

4 回答 4

1

我想我在问它在 Java 领域是如何“正常完成”的。而且,不仅针对这个问题,而且总体而言。

这高度依赖于手头的任务。

Java 中的标准范例是您必须自己将工作拆分成块。将这些块分布到多个线程/核心是一个单独的问题,并且存在多种模式(队列、线程池等)。

有趣的是,存在可以自动利用多个内核for并行执行循环之类的框架(例如,OpenMP)。但是,我不知道有任何此类 Java 框架。

最后,可能是完成大部分工作的低级库可以使用多个内核。在这种情况下,更高级别的代码可能能够保持单线程并且仍然受益于多核硬件。一个例子可能是在幕后使用MKL的数字代码。

于 2012-04-27T18:15:05.137 回答
1

当我们谈论“多线程”或“并行/并发编程”时,这是否意味着我们正在处理一个大问题并将其分散到多个线程上,或者我们是否首先明确地将其分解为更小的子问题,然后通过每个子问题都有自己的线程?

我认为这在很大程度上取决于问题。有时,您需要使用相同的代码调用 1000 次或数百万次相同的任务。这是ExecutorSerivce.submit()模式的类型。您有来自文件的数百万行,并且您在每一行上运行一些处理方法。我想这是您的“将其传播到多个线程”类型的问题。这适用于简单的线程模型。

但在其他情况下,问题空间由大量非同质任务组成。有时你可能会产生一个线程来处理一些后台保持活动,而有时你可能会在这里和那里产生一个线程池来处理一些工作队列。通常,问题的范围越大,并发模型越复杂,使用的池和线程类型就越多。我想这是您的“将其分解为较小的子问题”类型。

为了使这个“并发”或“多线程”,我们是否会将这个 EndWorldHungerTask 传递给 100 个工作线程(其中 100 个工作线程中的每一个都由 JVM 告知何时处于活动状态并在下一次迭代中工作/ someReallyExpensiveOperation() 调用),还是我们会手动/显式地重构它,以便 100 个工作人员中的每一个都在循环/待完成工作的不同部分上进行迭代?在这两种情况下,100 名工人中的每一个都只迭代了一百万次。

就您而言,我看不出您如何使用一组线程代码来解决世界饥饿问题(使用您的类比)。我认为您必须“将其分解为较小的子问题”,这对应于我上面解释的后一种情况:运行不同代码的整个线程系列。一些子解决方案可以在线程池中完成,而一些子解决方案将使用单独的线程完成,每个线程运行单独的代码。

我想我在问它在 Java 领域是如何“正常完成”的。而且,不仅针对这个问题,而且总体而言。

“通常”在很大程度上取决于问题及其复杂性。根据我的经验,我通常会ExecutorService尽可能多地使用这些构造。但是对于任何体面的问题,您会发现自己有许多不同的线程池、Spring 计时器线程、自定义一次性线程任务、生产者/消费者模型等。

于 2012-04-27T18:15:06.520 回答
0

通常,您希望每个线程从头到尾执行一个任务形式,将任务完成一半,然后停止该线程上的执行并“调用”另一个线程来完成工作,您将一无所获。Java当然为这种线程同步提供了工具,但是当一个任务依赖于另一个任务来完成时,它们确实被使用了——而不是让另一个线程可以完成任务。

大多数时候你会遇到一个大问题,它由几个任务组成,如果这些任务可以同时执行,那么产生线程来执行这些任务是有意义的。与创建线程相关的开销,因此如果所有任务都是顺序的并且必须等待另一个完成,那么产生多个线程根本没有好处,只产生一个线程,这样你就不会阻塞主线程.

于 2012-04-27T18:14:50.940 回答
0

“多线程”<>“并行/并发编程”。

编写多线程应用程序通常是为了利用抢占式多任务程序的高 I/O 性能。一个例子可能是网络爬虫/下载器。即使在只有一个 CPU 内核的机器上运行时,多线程爬虫通常也会比单线程版本好很多。DNS查询获取站点地址,连接到站点,下载页面,将其写入磁盘文件的操作都是需要很少CPU但需要大量IO等待的操作。因此,许多这些不可避免的等待可以由许多线程并行执行。当 DNS 查询进入、HTTP 客户端连接或磁盘操作完成时,请求它的线程已准备好/正在运行,并且可以继续进行下一个操作。

出于这个原因,绝大多数应用程序主要是编写为多线程的。这就是为什么我在上面写这篇文章的盒子有 98 个进程(其中 94 个有多个线程)、1360 个线程和 3% 的 CPU 使用率——这与将 CPU 工作拆分为核心无关——主要是关于 IO表现。

并行/并发编程实际上可以使用多个 CPU 内核进行。对于那些具有 CPU 密集型工作的应用程序,可以将其分解为较大的包以跨内核分发,可以小心地使用接近内核数量的加速因子。

自然会有一些溢出——I/O 绑定的网络爬虫往往会在具有更多内核的机器上表现更好,这仅仅是因为中断/驱动程序开销对整体性能的影响较小,但它不会好很多。

如果他们都在等待庄稼生长,那么您有多少工人可用于 EndWorldHunger 任务并不重要。

于 2012-04-27T20:19:04.360 回答