繁体   English   中英

遍历具有多个线程的二叉树

[英]Traversing a Binary Tree with multiple threads

所以我正在开展Java速度竞赛。 我有(处理器数量)线程正在工作,他们都需要添加到二叉树。 最初我只是使用了一个同步的add方法,但是我想这样做,这样线程就可以通过树相互跟随(每个线程只对它正在访问的对象有锁)。 不幸的是,即使对于一个非常大的文件(48,000行),我的新二叉树比旧的更慢。 我想这是因为我每次搬进树时都会得到并释放一把锁。 这是最好的方法吗?还是有更好的方法?

每个节点都有一个名为lock的ReentrantLock,而getLock()和releaseLock()只调用lock.lock()和lock.unlock();

我的代码:

public void add(String sortedWord, String word) {

    synchronized(this){
        if (head == null) {
            head = new TreeNode(sortedWord, word);
            return;
        }
        head.getLock();
    }

    TreeNode current = head, previous = null;
    while (current != null) {

        // If this is an anagram of another word in the list..
        if (current.getSortedWord().equals(sortedWord)) {
            current.add(word);
            current.releaseLock();
            return;
        }
        // New word is less than current word
        else if (current.compareTo(sortedWord) > 0) {
            previous = current;
            current = current.getLeft();
            if(current != null){
                current.getLock();
                previous.releaseLock();
            }
        }
        // New word greater than current word
        else {
            previous = current;
            current = current.getRight();
            if(current != null){
                current.getLock();
                previous.releaseLock();
            }
        }
    }

    if (previous.compareTo(sortedWord) > 0) {
        previous.setLeft(sortedWord, word);
    }
    else {
        previous.setRight(sortedWord, word);
    }
    previous.releaseLock();
}

编辑:只是为了澄清,我的代码结构如下:主线程从文件中读取输入并将单词添加到队列中,每个工作线程从队列中拉出单词并完成一些工作(包括对它们进行排序并将它们添加到二叉树)。

另一件事。 在性能关键代码中绝对没有二叉树的位置。 缓存行为将终止所有性能。 它应该有一个更大的扇出(一个缓存行)[编辑]使用二叉树你可以访问太多非连续的内存。 看看Judy树上的材料。

并且您可能希望在启动树之前以至少一个字符的基数开始。

并首先在int键上进行比较,而不是字符串。

也许看看尝试

并摆脱所有线程和同步。 只是尝试使问题内存访问绑定

[编辑]我会这样做有点不同。 我会为字符串的每个第一个字符使用一个线程,并给它们自己的BTree(或者也许是Trie)。 我在每个线程中放置一个非阻塞工作队列,并根据字符串的第一个字符填充它们。 通过预先分配添加队列并对BTree进行合并排序,可以获得更高的性能。 在BTree中,我使用表示前4个字符的int键,仅引用叶页中的字符串。

在速度竞赛中,您希望受到内存访问限制,因此对线程没有用处。 如果没有,你仍然在为每个字符串做太多处理。

我实际上会开始考虑使用compare()equals() ,看看是否可以在那里改进。 您可以将String对象包装在另一个具有不同的类中,并针对您的usecase, compare()方法compare()优化。 例如,考虑使用hashCode()而不是equals() 哈希码被缓存,因此将来的调用会更快。 考虑实习字符串。 我不知道vm是否会接受那么多字符串,但值得一试。

(这将是对答案的评论但是太过于罗嗦)。

在读取节点时,您需要在到达时为每个节点获取读锁定。 如果您读取整个树,那么您什么也得不到。 到达要修改的节点后,释放该节点的读锁定并尝试获取写锁定。 代码将是这样的:

TreeNode当前; //为每个节点添加ReentrantReadWriteLock。

//输入当前节点:
。current.getLock()readLock()锁();
if(isTheRightPlace(current){
current.getLock()readLock()解锁()。;
。current.getLock()writeLock()锁(); // NB:getLock返回ConcurrentRWLock
//做东西然后释放锁定
。current.getLock()writeLock()解锁();
} else {
current.getLock()readLock()解锁()。;
}

锁定和解锁是开销,您执行的越多,程序就越慢。

另一方面,分解任务并并行运行部分将使您的程序更快完成。

“收支平衡”点所在的位置高度依赖于程序中特定锁的争用量,以及运行程序的系统体系结构。 如果没有争用(如本程序中似乎存在)和许多处理器,这可能是一个很好的方法。 但是,随着线程数量的减少,开销将占主导地位,并发程序将变慢。 您必须在目标平台上分析您的程序以确定这一点。

另一个需要考虑的选择是使用不可变结构的非锁定方法。 例如,您可以将旧(链接)列表附加到新节点,然后使用AtomicReference上的compareAndSet操作,而不是修改列表,确保您赢得数据竞争以在当前树节点中设置words集合。 如果没有,请再试一次。 您也可以在树节点中对左右子节点使用AtomicReferences 无论是否更快,都必须在目标平台上进行测试。

您可以尝试使用可升级的读/写锁(可能称为可升级的共享锁等,我不知道Java提供了什么):对整个树使用单个RWLock。 在遍历B-Tree之前,您获取了读取(共享)锁定,并在完成后释放它(在add方法中获取一个和一个释放,而不是更多)。

在您必须修改 B树的位置,您将获得写入(独占)锁定(或从“读取”到“写入锁定”的“升级”),插入节点并降级到读取(共享)锁定。

使用这种技术,也可以删除检查和插入头节点的同步!

它应该看起来像这样:

public void add(String sortedWord, String word) {

    lock.read();

    if (head == null) {
        lock.upgrade();
        head = new TreeNode(sortedWord, word);
        lock.downgrade();
        lock.unlock();
        return;
    }

    TreeNode current = head, previous = null;
    while (current != null) {

            if (current.getSortedWord().equals(sortedWord)) {
                    lock.upgrade();
                    current.add(word);
                    lock.downgrade();
                    lock.unlock();
                    return;
            }

            .. more tree traversal, do not touch the lock here ..
            ...

    }

    if (previous.compareTo(sortedWord) > 0) {
        lock.upgrade();
        previous.setLeft(sortedWord, word);
        lock.downgrade();
    }
    else {
        lock.upgrade();
        previous.setRight(sortedWord, word);
        lock.downgrade();
    }

    lock.unlock();
}

不幸的是,经过一些谷歌搜索后,我找不到适合Java的“ugradeable”rwlock。 “一流的ReentrantReadWriteLock”不升级,但是,而不是升级的,你可以解锁阅读,然后锁定写,和(非常重要):重新检查一遍导致这些线(例如条件if( current.getSortedWord().equals(sortedWord) ) {...} )。 这很重要,因为另一个线程可能在读取解锁和写入锁定之间发生了变化。

有关详细信息,请查看此问题及其答案

最后,B树的遍历将并行运行。 只有在找到目标节点时,线程才会获得独占锁(并且其他线程将仅在插入时阻止)。

考虑到每行一个数据集,48k行并不是那么多,你只能对你的操作系统和虚拟机如何破坏你的文件IO以尽可能快地进行猜测。

尝试使用生产者/消费者范例在这里可能会有问题,因为您必须仔细平衡锁的开销与实际的IO数量。 如果您只是尝试改进文件IO的方式(考虑类似mmap() ),您可能会获得更好的性能。

我会说这样做是不可能的,甚至不考虑同步性能问题。

此实现比原始完全同步版本慢的事实可能是一个问题,但更大的问题是此实现中的锁定根本不是强大的。

想象一下,例如,你为sortedWord传递null ; 这将导致抛出NullPointerException ,这意味着您最终会保留当前线程中的锁,从而使您的数据结构处于不一致状态。 另一方面,如果您只是synchronize此方法,则无需担心此类问题。 考虑到同步版本也更快,它是一个简单的选择。

您似乎已经实现了二叉搜索树,而不是B树。

无论如何,您是否考虑过使用ConcurrentSkipListMap? 这是一个有序的数据结构(在Java 6中引入),它应具有良好的并发性。

我有一个愚蠢的问题:因为你正在阅读和修改一个文件,你将完全受限于读/写头移动速度和磁盘可以旋转的速度。 那么使用线程和处理器有什么用呢? 光盘不能同时做两件事。

或者这一切都在RAM?

补充:好的,我不清楚这里有多少并行可以帮助你(有些可能),但无论如何,我建议的是挤出每个线程中的每个循环。 这就是我所说的。 例如,我想知道那些看起来像“get”和“compare”方法的无辜的睡眠代码是否比你预期的花费了更多的时间。 如果它们是,你或许可以做一次而不是2到3次 - 那种事情。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM