【问题标题】:Why is my multi threaded sorting algorithm not faster than my single threaded mergesort为什么我的多线程排序算法不比我的单线程归并排序快
【发布时间】:2011-02-22 05:22:58
【问题描述】:

当一个人分割一个任务并让每个部分并行完成时,某些算法的运行时间会显着减少。其中一种算法是归并排序,其中一个列表被分成无限小的部分,然后按排序顺序重新组合。我决定做一个实验来测试我是否可以通过使用多个线程来提高这种排序的速度。我在装有 Windows Vista 的四核戴尔上用 Java 运行以下功能。

一个函数(控制用例)是简单的递归:

// x is an array of N elements in random order
public int[] mergeSort(int[] x) {
    if (x.length == 1) 
        return x;

    // Dividing the array in half
    int[] a = new int[x.length/2];
    int[] b = new int[x.length/2+((x.length%2 == 1)?1:0)];
    for(int i = 0; i < x.length/2; i++) 
        a[i] = x[i];
    for(int i = 0; i < x.length/2+((x.length%2 == 1)?1:0); i++) 
        b[i] = x[i+x.length/2];

    // Sending them off to continue being divided
    mergeSort(a);
    mergeSort(b);

    // Recombining the two arrays
    int ia = 0, ib = 0, i = 0;
    while(ia != a.length || ib != b.length) {
        if (ia == a.length) {
            x[i] = b[ib];
            ib++;
        }
        else if (ib == b.length) {
            x[i] = a[ia];
            ia++;
        }
        else if (a[ia] < b[ib]) {
            x[i] = a[ia];
            ia++;
        }
        else {
            x[i] = b[ib];
            ib++;
        }
        i++;
    }

    return x;
}

另一个是在一个扩展线程的类的'run'函数中,每次调用都会递归创建两个新线程:

public class Merger extends Thread
{
    int[] x;
    boolean finished;

    public Merger(int[] x)
    {
        this.x = x;
    }

    public void run()
    {
        if (x.length == 1) {
            finished = true;
            return;
        }

        // Divide the array in half
        int[] a = new int[x.length/2];
        int[] b = new int[x.length/2+((x.length%2 == 1)?1:0)];
        for(int i = 0; i < x.length/2; i++) 
            a[i] = x[i];
        for(int i = 0; i < x.length/2+((x.length%2 == 1)?1:0); i++) 
            b[i] = x[i+x.length/2];

        // Begin two threads to continue to divide the array
        Merger ma = new Merger(a);
        ma.run();
        Merger mb = new Merger(b);
        mb.run();

        // Wait for the two other threads to finish 
        while(!ma.finished || !mb.finished) ;

        // Recombine the two arrays
        int ia = 0, ib = 0, i = 0;
        while(ia != a.length || ib != b.length) {
            if (ia == a.length) {
                x[i] = b[ib];
                ib++;
            }
            else if (ib == b.length) {
                x[i] = a[ia];
                ia++;
            }
            else if (a[ia] < b[ib]) {
                x[i] = a[ia];
                ia++;
            }
            else {
                x[i] = b[ib];
                ib++;
            }
            i++;
        }

        finished = true;
    }
}

事实证明,不使用多线程的函数实际上运行得更快。为什么?操作系统和 java 虚拟机是否没有足够有效地“通信”以将不同的线程放在不同的内核上?还是我遗漏了一些明显的东西?

【问题讨论】:

  • 只有一个线程时,所有数据都在缓存中。如果在线程之间拆分数据,则需要将部分数据复制到第二个线程的缓存中,然后再次复制回来以进行最终合并。如果比较的成本很小,那么复制的成本可能会更高。您可能会发现使用的线程越多,开销就越大。
  • 我没有看到你在任何地方调用 Thread.start()。您的合并看起来很糟糕,因为您只是在一个线程中调用 run() 方法。
  • 程序员确实需要摆脱“单线程”的思维模式:这就像八十年代打电话想要他们的单核 80386 CPU 回来一样。我们正在转向一个高度多核的世界,未来最大的性能增强将是由于水平扩展,跨多核。拒绝学习多线程优势的人将生产性能不佳的 API/软件。 OP 可以检查我的答案:正确的多线程算法(包括排序)执行 own 单线程算法。克服它,我们正在走向多核世界。

标签: java multithreading parallel-processing


【解决方案1】:

问题不在于多线程:我已经用 Java 编写了正确的多线程 QuickSort,它拥有默认的 Java 排序。我是在目睹一个巨大的数据集正在处理并且只有一个 16 核机器的一个核心在工作之后才这样做的。

您的一个问题(一个大问题)是您正忙于循环:

 // Wait for the two other threads to finish 
 while(!ma.finished || !mb.finished) ;

这是一个巨大的禁忌:它被称为忙循环,你正在破坏性能。

(另一个问题是您的代码没有产生任何新线程,正如已经向您指出的那样)

您需要使用其他方式进行同步:例如使用CountDownLatch

另一件事:划分工作负载时不需要生成两个新线程:只生成一个新线程,在当前线程中执行另一半。

另外,您可能不想创建比可用内核更多的线程。

在这里查看我的问题(要求一个好的开源多线程合并排序/快速排序/其他)。我用的是专有的,我不能粘贴。

Multithreaded quicksort or mergesort

我还没有实现 Mergesort,但是 QuickSort,我可以告诉你,没有数组复制正在进行。

我要做的是:

  • 选择一个支点
  • 根据需要交换值
  • 我们是否达到线程限制? (取决于核心数)
    • 是:对本帖的第一部分进行排序
    • 否:产生一个新线程
  • 对当前线程中的第二部分进行排序
  • 如果第一部分尚未完成,请等待它完成(使用 CountDownLatch)。

产生一个新线程并创建 CountDownLatch 的代码可能如下所示:

            final CountDownLatch cdl = new CountDownLatch( 1 );
            final Thread t = new Thread( new Runnable() {
                public void run() {
                    quicksort(a, i+1, r );
                    cdl.countDown();
                }
            } };

使用 CountDownLatch 等同步工具的优势在于它非常高效,而且您不会浪费时间处理低级 Java 同步特性。

在您的情况下,“拆分”可能如下所示(未经测试,只是提供一个想法):

if ( threads.getAndIncrement() < 4 ) {
    final CountDownLatch innerLatch = new CountDownLatch( 1 );
    final Thread t = new Merger( innerLatch, b );
    t.start();
    mergeSort( a );
    while ( innerLatch.getCount() > 0 ) {
        try {
            innerLatch.await( 1000, TimeUnit.SECONDS );
        } catch ( InterruptedException e ) {
            // Up to you to decide what to do here
        }
    }
} else {
    mergeSort( a );
    mergeSort( b );
}

(不要忘记在每次合并完成时对锁存器“倒计时”)

您可以将线程数(此处最多 4 个)替换为可用内核数。您可以使用以下内容(例如,在程序开始时初始化一些静态变量:内核数量不太可能改变[除非您使用的机器允许像某些 Sun 系统允许的那样 CPU 热交换]):

Runtime.getRuntime().availableProcessors()

【讨论】:

  • 哎呀傻了我,我应该重写它而不是使用你的代码:在你不产生新线程的情况下,它是没有意义的,所以分成'a'和'b'然后做一个 mergeSort(a) 和 mergeSort(b)... 在拆分之前直接对整个数组进行合并排序。
  • 你到底为什么要把对CDL.await()的调用放在一个while循环中?此外,您是有条件的 (threads.getAndIncrement()
  • @Tim Bender:因为损坏的第 3 方 API 可能会错误地中断()我在等待。在这种情况下,您可能可能不想继续等待,这就是为什么我发表了一个巨大的评论说“//由你决定做什么这里”(如果你认为中断是合法的,你可以决定减少锁存器并退出循环)。线程数在增加时很好:它的唯一目的是不产生超过“x”个线程。看到我永远不会“翻转” 2**32-1 线程,这不会溢出,并且代码可以正常工作。挑剔或帮助 OP:选择一个。 ;)
  • CountDownLatch 非常神奇,实际上比 thread.join() 好得多。我决定也进行快速排序。能够动态确定处理器的数量也是一个很棒的补充。如果您没有建议,我不会想到只启动一个线程然后按顺序完成另一“一半”。所以,谢谢,祝你好运,网络管理员!
【解决方案2】:

正如其他人所说;这段代码不起作用,因为它没有启动新线程。您需要调用 start() 方法而不是 run() 方法来创建新线程。它也有并发错误:对finished变量的检查不是线程安全的。

如果您不了解基础知识,并发编程可能会非常困难。您可能会阅读本书Java Concurrency in Practice by Brian Goetz。它解释了基础知识并解释了构造(例如 Latch 等)以简化构建并发程序。

【讨论】:

    【解决方案3】:

    同步的开销成本可能比较大,会阻碍很多优化。

    此外,您创建的线程太多。

    另一个是在扩展线程的类的“运行”函数中,在每次调用时递归地创建两个新线程

    使用固定数量的线程会更好,建议在四核上使用 4 个。这可以通过线程池 (tutorial) 来实现,模式将是“任务包”。但也许更好的是,最初将任务分成四个同样大的任务,并对这些任务进行“单线程”排序。这样可以更好地利用缓存。


    您应该查看Thread.join(),而不是等待线程完成的“忙循环”(窃取 cpu 周期)。

    【讨论】:

    • 虽然这些问题通常是这种情况,但在此示例中没有同步。
    • 糟糕。在那种情况下应该有一些讨厌的竞争条件?
    • 哦,我现在看到了忙等待循环。
    • 是的,这段代码可能也会受到内存一致性的影响,因为finished 不是易失性的(繁忙的等待循环可能永远不会完成)。我认为数组元素也需要一个联合来形成一个happens-before关系。
    • 感谢您的建议和教程,aioobe。我发现 Thread.join() 实际上远不如使用 CountDownLatch 有效。
    【解决方案4】:

    您必须对数组中的多少个元素进行排序?如果元素太少,同步和 CPU 切换的时间将超过您为并行划分作业而节省的时间

    【讨论】:

    • 数组中有N个元素,N是一个非常大的数字(大于100万)。
    • @Robz,我相信您会震惊地发现 Sun 的 Arrays.sort 实现具有使用插入排序的最小阈值。大小很重要,时期。
    • 实际上,我不确定数组的任意大对于这个潜在的“大小很重要”问题的发生是否重要。合并排序和快速排序一样,使用分治法。因此,如果 X 是某个截止值并且 N 大于 X,那么最终在对整个数组进行排序的过程中,一个子数组将被发送到长度小于 X 的合并/快速排序函数。有趣的是,在我个人体验不使用截止或插入排序调用的简单顺序快速排序,其速度与 Arrays.sort 一样快,N 为 100 万或更多。
    猜你喜欢
    • 1970-01-01
    • 2017-12-26
    • 2011-01-13
    • 2020-03-25
    • 1970-01-01
    • 1970-01-01
    • 2016-04-07
    • 2015-06-25
    • 1970-01-01
    相关资源
    最近更新 更多