【问题标题】:Thread-safe sorted linked list线程安全的排序链表
【发布时间】:2011-05-14 15:14:57
【问题描述】:

我正在尝试编写一个线程安全的排序单链表。我写了两个版本:粗粒度同步和细粒度同步。以下是两个实现:

细粒度:

public void add(T t) {                                                         
  Node curr = head;
  curr.lock.lock();

  while (curr.next != null) {
    // Invariant: curr is locked                                               
    // Invariant: curr.data < t                                                
    curr.next.lock.lock();                                                     

    if (t.compareTo(curr.next.data) <= 0) {                                    
      break;                                                                   
    }                                                                          

    Node tmp = curr.next;                                                      
    curr.lock.unlock();                                                        
    curr = tmp;                                                                
  }                                                                            

  // curr is acquired                                                          
  curr.next = new Node(curr.next, t);                                          
  if (curr.next.next != null) {  // old curr's next is acquired                
    curr.next.next.lock.unlock();                                              
  }                                                                            
  curr.lock.unlock();                                                          
}                                                                              

粗粒度:

public void add(T t) {
  lock.lock();
  Node curr = head;
  while (curr.next != null) {
    if (t.compareTo(curr.next.data) <= 0) {
      break;
    }                                                                          
    curr = curr.next;                                                          
  }                                                                            
  curr.next = new Node(curr.next, t);                                          
  lock.unlock();                                                               
}

我在插入 20000 个整数的 4 个线程(在 4 个逻辑 CPU 内核上)对两个版本进行了计时。每个线程的时间显示 CPU 时间(即不包括等待时间)。

Fine grained:
Worked 1 spent 1080 ms
Worked 2 spent 1230 ms
Worked 0 spent 1250 ms
Worked 3 spent 1260 ms
wall time: 1620 ms

Coarse grained:
Worked 1 spent 190 ms
Worked 2 spent 270 ms
Worked 3 spent 410 ms
Worked 0 spent 280 ms
wall time: 1298 ms

我最初的想法是 .lock().unlock() 是问题所在,但我分析了实现,它们加起来只消耗了 30% 的时间。我的第二个猜测是细粒度解决方案有更多的缓存未命中,但我对此表示怀疑,因为与数组不同,单个链表天生就容易出现缓存未命中。

知道为什么我没有得到预期的并行化吗?

【问题讨论】:

  • 简要看一下,我会说细粒度解决方案对每个插入执行 O(n) 锁定,粗粒度解决方案仅 O(1)。只有 20000 个整数,锁定开销似乎占主导地位。
  • 我也想过。我增加到 100_000 个元素,结果相似。锁定/解锁只需要 30% 的时间(这是 jprofiler 告诉我的),并且没有考虑时间差。
  • 30% 整体还是每个线程?
  • Sorted 链表是一个糟糕的主意,如果你有任何大量的修改。如果有树,你会更好(好多了)。

标签: java multithreading linked-list


【解决方案1】:

是的,这可能是由于缓存未命中。包含锁的高速缓存行在 CPU 之间不断跳动。

另外,请注意,您获得了相当多的并行性:

Fine grained:
Worked 1 spent 1080 ms
Worked 2 spent 1230 ms
Worked 0 spent 1250 ms
Worked 3 spent 1260 ms
wall time: 1620 ms

Coarse grained:
Worked 1 spent 190 ms
Worked 2 spent 270 ms
Worked 3 spent 410 ms
Worked 0 spent 280 ms
wall time: 1298 ms

虽然每个单独的线程需要更多的时间,但由于缓存未命中(以及增加的开销),整个过程只是稍微慢了一点。

【讨论】:

  • 时间差似乎实际上是锁定所花费的时间。所以这可能是某个地方的虚假分享。
【解决方案2】:

您可以实现接近每线程粗粒度版本的挂墙时间 首先在没有锁的情况下遍历列表以找到间隙,然后从 当前,这次使用锁,遍历列表以确保没有干预 在 current 和 current->next 之间由其他线程插入。 (当然我忽略了“头”总是至高无上的事实:)

【讨论】:

    【解决方案3】:

    似乎确实存在性能问题。我认为您应该将性能与内置实现和单线程版本进行比较。

    for (int r = 0; r < 5; r++) {
        long start = System.nanoTime();
        ConcurrentLinkedQueue<Integer> list = new ConcurrentLinkedQueue<Integer>();
        for (int i = 0; i < 500000; i++)
            list.add(i);
        long time = System.nanoTime() - start;
        System.out.printf("Adding 500K %,d took ms%n", time / 1000 / 1000);
    }
    

    打印

    Adding 500K 192 took ms
    Adding 500K 154 took ms
    Adding 500K 95 took ms
    Adding 500K 211 took ms
    Adding 500K 424 took ms
    

    【讨论】:

    • 请注意,我的问题是“排序单链表”。有库版本吗?
    • 好的,有一个 PriorityQueue 理论上是排序的。这更快,但它只对轮询进行排序,这隐藏了排序的成本。
    • ConcurrentSkipListSet 已排序且速度更快,但不是单链表。
    【解决方案4】:

    除了ninjalj的回答——细锁也

    1. 在现有代码中禁用一些编译器优化
    2. 禁用一些 CPU 优化 - 例如预取
    3. 强制内存在锁定时获取语义,并在解锁时释放语义 - 导致跨 CPU 同步和缓存无效 - 这不会直接显示为探查器中的 lock() 成本,但会增加后续数据访问的成本。李>

    【讨论】:

      【解决方案5】:

      我错过了什么吗?我在您的代码中看不到任何类型的缓存。此外,您应该重新考虑使用锁定的方式。您应该只锁定整个列表以限制锁定的数量并防止出现竞争条件,如下所示。

      thread1: Read Element X
      thread2: Removes X + 1
      thread1: Read Element X + 1 and fails since the element is no long valid.
      thread1: Is unable to finish going through the list since it has been removed.
      

      您可以对列表进行分区,但您必须处理读取分区中的最后一个元素并删除下一个分区中的第一个元素的情况。

      您还可以根据正在发生的操作类型(即,它是读取操作,当前没有发生写入操作),只需要锁定/解锁某些功能。

      【讨论】:

      • 我指的是 CPU 缓存命中/未命中。我得再考虑一下,但我认为您的竞争条件无效,因为要删除元素 X+1,您必须获得 X。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-12-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多