为什么我的多线程排序算法不比单线程mergesort快

时间:2010-05-21 06:22:12

标签: java multithreading parallel-processing

有些算法在分割任务并使每个部分并行完成时,其运行时间会显着减少。这些算法之一是合并排序,其中列表被划分为无限小的部分,然后以排序的顺序重新组合。我决定做一个实验来测试我是否可以通过使用多个线程来提高这种速度。我在带有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虚拟机是否没有足够有效地“通信”以将不同的线程放在不同的核心上?或者我错过了一些明显的东西?

4 个答案:

答案 0 :(得分:12)

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

你的一个问题(一个巨大的问题)是你正在忙着循环:

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

这是一个巨大禁忌:它被称为忙碌循环,你正在摧毁这些行为。

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

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

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

此外,您可能不希望创建比可用内核更多的线程。

在这里查看我的问题(要求一个好的开源多线程mergesort / quicksort /等等)。我使用的是专有的,我无法粘贴它。

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个)。您可以使用以下内容(一次,比如在程序开头初始化一些静态变量:核心数量不太可能发生变化[除非您在允许CPU热交换的机器上,如某些Sun系统允许的那样]):

Runtime.getRuntime().availableProcessors()

答案 1 :(得分:3)

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

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

答案 2 :(得分:1)

同步的开销成本可能相对较大,并且会阻止许多优化。

此外,你创造的线程太多了。

  

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

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


不要让“忙循环”等待线程完成(窃取cpu-cycles),你应该看看Thread.join()

答案 3 :(得分:0)

您需要对数组中的元素进行排序?如果元素太少,则同步和CPU切换的时间将超过您为分割作业而保存的时间