multithreading总是比单线程产生更好的性能吗?

我知道答案是否,这是一个例子为什么单线程比Java中的multithreading更快? 。

因此,当在线程中处理任务是微不足道的时,创建线程的成本将比分发任务产生更多的开销。 这是一个单线程比multithreading更快的情况。

问题

  • 是否存在单个线程比multithreading更快的情况?

  • 我们什么时候应该决定放弃multithreading,只使用一个线程来实现我们的目标?

虽然问题标记为java ,但也欢迎在Java之外进行讨论。 如果我们能在答案中有一个小例子来解释,那将会很棒。

这是一个关于线程及其与实际工作的链接的非常好的问题,这意味着可用的物理CPU及其核心和超线程。

  1. 如果您的CPU具有多个可用核心,则多个线程可能允许您并行执行操作。 因此,在一个理想的世界中,例如计算一些素数,使用4个线程可能会快4倍,如果你的CPU有4个可用内核并且你的算法工作真正并行。
  2. 如果在核心可用时启动更multithreading,则操作系统的线程管理将花费越来越多的时间在线程交换机中,因此使用CPU的效率会变差。
  3. 如果编译器,CPU缓存和/或运行时意识到您运行多个线程,访问内存中的相同数据区域,则以不同的优化模式运行:只要编译/运行时确保只有一个线程访问数据,可以避免过于频繁地将数据写入外部RAM,并可能有效地使用CPU的L1缓存。 如果不是:必须激活信号量,并且还经常将缓存数据从L1 / L2缓存刷新到RAM。

所以我从高度并行的multithreading中学到的经验是:

  • 如果可能,使用单线程,无共享进程更高效
  • 如果需要线程,请尽可能地分离共享数据访问
  • 如果可能,请勿尝试分配比可用核心更多的加载工作线程

这里有一个小程序(javafx)可以玩。 它:

  • 分配一个100.000.000大小的字节数组,填充随机字节
  • 提供一种方法,计算此数组中设置的位数
  • 该方法允许对每个“第n个”字节位进行计数
  • count(0,1)将计算所有字节位
  • count(0,4)将计数0’,4’,8’字节位,允许并行交错计数

使用MacPro(4核)导致:

  1. 运行一个线程,count(0,1)需要1326ms来计算所有399993625位
  2. 运行两个线程,并行计数(0,2)和计数(1,2)需要920ms
  3. 运行四个线程,需要618ms
  4. 运行八个线程,需要631ms

在此处输入图像描述在此处输入图像描述在此处输入图像描述在此处输入图像描述

更改计数方式,例如递增共享整数(AtomicInteger或synchronized)将极大地改变许multithreading的性能。

 public class MulithreadingEffects extends Application { static class ParallelProgressBar extends ProgressBar { AtomicInteger myDoneCount = new AtomicInteger(); int myTotalCount; Timeline myWhatcher = new Timeline(new KeyFrame(Duration.millis(10), e -> update())); BooleanProperty running = new SimpleBooleanProperty(false); public void update() { setProgress(1.0*myDoneCount.get()/myTotalCount); if (myDoneCount.get() >= myTotalCount) { myWhatcher.stop(); myTotalCount = 0; running.set(false); } } public boolean isRunning() { return myTotalCount > 0; } public BooleanProperty runningProperty() { return running; } public void start(int totalCount) { myDoneCount.set(0); myTotalCount = totalCount; setProgress(0.0); myWhatcher.setCycleCount(Timeline.INDEFINITE); myWhatcher.play(); running.set(true); } public void add(int n) { myDoneCount.addAndGet(n); } } int mySize = 100000000; byte[] inData = new byte[mySize]; ParallelProgressBar globalProgressBar = new ParallelProgressBar(); BooleanProperty iamReady = new SimpleBooleanProperty(false); AtomicInteger myCounter = new AtomicInteger(0); void count(int start, int step) { new Thread(""+start){ public void run() { int count = 0; int loops = 0; for (int i = start; i < mySize; i+=step) { for (int m = 0x80; m > 0; m >>=1) { if ((inData[i] & m) > 0) count++; } if (loops++ > 99) { globalProgressBar.add(loops); loops = 0; } } myCounter.addAndGet(count); globalProgressBar.add(loops); } }.start(); } void pcount(Label result, int n) { result.setText("("+n+")"); globalProgressBar.start(mySize); long start = System.currentTimeMillis(); myCounter.set(0); globalProgressBar.runningProperty().addListener((p,o,v) -> { if (!v) { long ms = System.currentTimeMillis()-start; result.setText(""+ms+" ms ("+myCounter.get()+")"); } }); for (int t = 0; t < n; t++) count(t, n); } void testParallel(VBox box) { HBox hbox = new HBox(); Label result = new Label("-"); for (int i : new int[]{1, 2, 4, 8}) { Button run = new Button(""+i); run.setOnAction( e -> { if (globalProgressBar.isRunning()) return; pcount(result, i); }); hbox.getChildren().add(run); } hbox.getChildren().addAll(result); box.getChildren().addAll(globalProgressBar, hbox); } @Override public void start(Stage primaryStage) throws Exception { primaryStage.setTitle("ProgressBar's"); globalProgressBar.start(mySize); new Thread("Prepare"){ public void run() { iamReady.set(false); Random random = new Random(); random.setSeed(4711); for (int i = 0; i < mySize; i++) { inData[i] = (byte)random.nextInt(256); globalProgressBar.add(1); } iamReady.set(true); } }.start(); VBox box = new VBox(); Scene scene = new Scene(box,400,80,Color.WHITE); primaryStage.setScene(scene); testParallel(box); GUIHelper.allowImageDrag(box); primaryStage.show(); } public static void main(String[] args) { launch(args); } } 

并非所有算法都可以并行处理(严格顺序的算法;其中Amdahl定律中P = 0)或至少不能有效(参见P-complete )。 其他算法更适合并行执行(极端情况称为“令人尴尬的并行” )。

与类似的顺序算法相比,并行算法的简单实现在复杂性或空间方面可能效率较低。 如果没有明显的方法来并行化算法以使其获得加速,那么您可能需要选择另一种类似的并行算法来解决相同的问题但可能效率更高或更低。 如果忽略线程/进程创建和直接进程间通信开销,则在使用IO瓶颈等共享资源或由较高内存消耗引起的增加分页时,仍可能存在其他限制因素。

我们什么时候应该决定放弃multithreading,只使用一个线程来实现我们的目标?

在单线程和multithreading之间进行决策时,应考虑更改实现所需的时间以及开发人员增加的复杂性。 如果使用多个线程只有很小的收益,你可能会认为通常由multithreading应用程序引起的增加的维护成本不值得加速。

开销不仅可以用于创建,还可以用于线程互通。 应该注意的另一件事是,例如单个对象上的线程同步可能导致单线程执行。

线程是利用空闲资源来处理更多工作。 如果没有空闲资源,multithreading没有优势,因此开销实际上会使整个运行时更长。

例如,如果要执行任务集合,则它们是CPU密集型计算。 如果你有一个CPU,multithreading可能不会加快这个过程(虽然你直到测试才知道)。 我希望它会略微放缓。 您正在改变工作的分配方式,但容量没有变化。 如果您在一个CPU上有4个任务要做,那么连续执行这些任务就是1 * 4 。 如果你并行执行它们,你基本上会得到4 * 1 ,这是相同的。 另外,合并结果和上下文切换的开销。

现在,如果您有多个CPU,那么在多个线程中运行CPU密集型任务将允许您使用未使用的资源,因此每单位时间可完成更多操作。

另外,考虑其他资源。 如果您有4个查询数据库的任务,那么并行运行它们会有助于数据库有额外的资源来处理它们。 虽然,您还添加了更多工作,从数据库服务器中删除资源,所以我可能不会这样做。

现在,假设我们需要对3个外部系统进行Web服务调用,并且所有调用都没有相互依赖的输入。 与多个线程并行执行它们意味着我们不必在另一个线程开始之前等待一个结束。 这也意味着并行运行它们不会对每项任务产生负面影响。 这将是multithreading的一个很好的用例。

正如@Jim Mischel在评论中提到的那样,你可以使用

阿姆达尔定律

计算这个。 Amdahl定律指出,通过添加处理器来解决任务所获得的加速是

在此处输入图像描述

哪里

N是处理器的数量,和

P是可以并行执行的代码的一部分(0 … 1)

现在,如果T是在单个处理器上执行任务所花费的时间,并且O是总“开销”时间(创建并设置第二个线程,通信……),则单个线程更快,如果

T

或者,在重新排序后,如果

O / T> P / 2

开销/执行时间比率大于P / 2时 ,单个线程更快。

是否存在单个线程比multithreading更快的情况?

因此,在GUI应用程序中,您将受益于multithreading。 在最基本的层面上,您将更新前端以及前端呈现的内容。 如果你正在运行像hello world这样基本的东西,那么就像你展示的那样会增加开销。

这个问题非常广泛……您是否将unit testing视为应用程序? 如果是这样,那么可能有更多的应用程序使用单线程,因为任何复杂的系统都会(希望)至少有1个unit testing。 您是否将每个Hello世界风格的程序视为不同的应用程序或相同? 如果某个应用程序被删除,它仍然会被计算?

正如你所看到的,除了你必须缩小你的问题的范围以获得有意义的答案之外,我不能给出好的回答。 话虽如此,这可能是我不知道的统计数据。

我们什么时候应该决定放弃multithreading,只使用一个线程来实现我们的目标?

当multithreading通过您认为重要的任何指标执行“更好”时。

您的问题可以分解为可以同时处理的部分吗? 不是像一个人为的方式将Hello World分成两个线程,其中一个线程在另一个线程上等待打印。 但是在某种程度上,2 +线程能够比一个线程更有效地完成任务?

即使任务很容易并行化也不意味着它应该是可行的。 我可以multithreading一个应用程序,不断地控制成千上万的新网站来获取我的新闻。 对我个人而言,这会很糟糕,因为它会占用我的烟斗而且我无法获得我的FPS。对于CNN而言,这可能正是他们想要的,并将构建一个大型机来容纳它。

你能缩小你的问题吗?