【问题标题】:Performance of pthread_mutex_lock/unlockpthread_mutex_lock/unlock 的性能
【发布时间】:2011-09-21 13:52:52
【问题描述】:

我注意到当我有一个锁定和解锁线程的算法时,我的性能受到了很大的影响。

有什么办法可以帮助解决这个开销?使用信号量会提高/降低效率吗?

谢谢

typedef struct _treenode{
   struct _treenode *leftNode;
   struct _treenode *rightNode;
   int32_t data;
   pthread_mutex_t mutex;
}TreeNode;

pthread_mutex_t _initMutex = PTHREAD_MUTEX_INITIALIZER;

int32_t insertNode(TreeNode **_trunk, int32_t data){
   TreeNode **current;
   pthread_mutex_t *parentMutex = NULL, *currentMutex = &_initMutex;

   if(_trunk != NULL){
      current = _trunk;
      while(*current != NULL){
         pthread_mutex_lock(&(*current)->mutex);
         currentMutex = &(*current)->mutex;
         if((*current)->data < data){
            if(parentMutex != NULL)
               pthread_mutex_unlock(parentMutex);
            pthreadMutex = currentMutex;
            current = &(*current)->rightNode;
         }else if((*current)->data > data){
            if(parentMutex != NULL)
               pthread_mutex_unlock(parentMutex);
            parentMutex = currentMutex;
            current = &(*current)->leftNode;
         }else{
            pthread_mutex_unlock(currentMutex);
            if(parentMutex != NULL)
               pthread_mutex_unlock(parentMutex);
            return 0;
         }
      }
      *current = malloc(sizeof(TreeNode));
      pthread_mutex_init(&(*current)->mutex, NULL);
      pthread_mutex_lock(&(*current)->mutex);
      (*current)->leftNode = NULL;
      (*current)->rightNode = NULL;
      (*current)->data = data;
      pthread_mutex_unlock(&(*current)->mutex);
      pthread_mutex_unlock(currentMutex);
   }else{
      return 1;
   }
   return 0;
}

int main(){
   int i;
   TreeNode *trunk = NULL;
   for(i=0; i<1000000; i++){
      insertNode(&trunk, rand() % 50000);
   }
}

【问题讨论】:

  • 信号量做不同的(更复杂的)事情,而且很可能更慢。你的操作系统是什么?你能把锁做得更细,这样你就不会阻塞那么久吗?
  • 或者让它们更粗粒度/每次锁定做更多的工作,所以你不会得到这么多的上下文切换。有一个很好的平衡。
  • 如果您展示/描述我们可能会给出提示的算法。解决方案应该是:使用更少的锁定(在专用核心上划分工作,因此您不需要锁定子区域)或使其无锁(haaaaaaaard)。除了摩尔定律,别无他法
  • 某些算法本质上比其他算法更连续。但是锁定和限制访问以获得正确的结果比不锁定并更快地获得错误的结果要好。
  • @nos - 好点,谢谢。一条黄金法则:如果可以的话,不要锁定网络或文件 I/O 等长时间运行的操作。

标签: c++ c pthreads mutex


【解决方案1】:

不要担心草叶,退后一步观察整个森林。

任何依赖于两个线程的算法都可能紧密地互相踩踏,本质上是低效的。尝试找到一种方法来大幅减少交互需求。

例如,如果一个线程产生数据而另一个线程消费它,人们很容易想到一种低效的算法,生产者将数据发布到共享内存中,然后等待另一个线程消费它。与此同时,消费者正在等待生产者完成,等等等等。生产者写入文件或管道,消费者从中读取,这一切都大大简化了。

【讨论】:

    【解决方案2】:

    pthread_mutex_lockpthread_mutex_unlock 的成本因争用而异:

    1. 单线程使用 - 要么只存在一个线程,要么只有一个线程在使用互斥锁及其保护的资源:锁定几乎免费,最多可能 80-100 个周期。
    2. 多个线程使用该资源,但锁定的时间间隔很短且很少发生争用:锁定有一定成本,而且很难衡量;成本主要包括使其他内核/cpus 的缓存行无效。
    3. 严重的锁争用:几乎每个锁定和解锁操作都需要内核的帮助,每次锁定/解锁的成本很容易达到几千(甚至几万)个周期。

    不过,在大多数情况和大多数实现中,互斥锁应该是成本最低的锁定原语。有时自旋锁可能会执行得更好。我从没想过信号量会表现得更好。

    【讨论】:

    • 在某些情况下,80-100 个周期并不是“几乎免费的”。
    • 也许我应该澄清一下:我将它与一对微不足道的外部函数调用进行比较,即如果 pthread_mutex_lockpthread_mutex_unlock 是近乎空的函数(但仍然可以'不被内联并仍然设置堆栈帧)。我面前没有数字,但我认为“无操作锁”的情况将接近 80 个周期,可能在高端 x86 机器上除外。
    • 对于基于原子环的简单生产者/消费者,当有可用数据时发出信号量信号可以胜过条件变量/互斥体,因为后者还需要锁定以更改条件。
    • @CashCow:我对信号量的评论是指使用二进制信号量作为锁。我的直觉是,这充其量应该与互斥锁的性能相匹配,但也可能会更糟。特别是,POSIX 信号量具有一些可能使它们成本更高的属性——对取消采取行动的能力、要求后期操作是异步信号安全的(这限制了实现选择)等。此外,在 POSIX 的下一期可能会放宽互斥锁要求以允许获取/释放语义,信号量几乎肯定会保持“seq_cst”。
    【解决方案3】:

    据我所知,您的锁定策略不是最优的,因为大多数锁定不会用于更改数据,而只是用于读取并找到穿过树的路径。

    pthread_rwlock_t 可以帮助解决这个问题。您只会在树中向下的路径上使用读锁,直到您遇到要进行修改的节点。然后你会在那里取一个写锁。这样一来,您可以让其他线程在不同分支中沿着树走时执行相同的任务,而不会相互干扰。

    pthread_rwlock_t 的一个体面的实现会为读者提供一个计数器,它会随着原子操作而改变,只要不与作者发生争用。这应该非常快。我认为,一旦发生争用,它的成本将与互斥锁一样高。

    【讨论】:

    • @Andrew,对不起它的pthread_rwlock_t。如果它在您的系统上实现,它应该在&lt;pthread.h. 中。
    • @Andrew 和 Jens。 1) RWLocks 并不总是更好的解决方案。如果所有读取操作都需要很短的时间并且所有写入操作都需要很长时间,那么与 MutEx 相比,它们的开销很容易扼杀它们的概念优势。这取决于读者和作者的数量以及他们并行访问该部分的频率。 2)据我所知,RWLocks 是建立在 MutExes 之上的,但即使不是,它们也比 MuitExes 更昂贵。 3)如果写入很少发生,我会为整个树使用单个 RWLock。然后,所有的读者都可以同时获得快乐。
    【解决方案4】:

    你的锁可能太细了。当然,最佳粒度可能因工作负载而异。

    您可以为整棵树使用一个锁,它可能性能更好。但是,如果你做大量的阅读和相对较少的插入/删除,你最终会经常无缘无故地锁定整个树。您可能想要使用读写锁,这将允许同时允许多个读者。

    你的问题让我想起了this other one,当时有一个链表的细粒度锁定和粗粒度锁定之间的比较。而在粗粒度版本中,每个线程是轮流运行(不是并行),总运行时间略多于每个线程运行时间的总和,而在细粒度版本中,总运行时间远小于每个线程运行时间的总和,细粒度锁定的额外开销完全抵消了这些好处,使得细粒度版本比粗粒度版本慢。

    【讨论】:

      【解决方案5】:

      在 pthread_mutex_lock/unlock 的情况下,锁定和解锁是非常昂贵的操作。有了算法的更多细节,我可以提出一些建议,但据我所知,我不能肯定地告诉你任何事情。信号量是另一种选择(同样取决于算法),障碍也是另一种有用的并发方法。为了减少开销,您可以做一些事情,例如使您的锁更小粒度或更大粒度。多次迭代的循环内的锁是一个坏主意,您可能希望将它们移到循环外。这只是一个例子,但我可能还能想出更多。这是关于确定锁的成本是否大于代码的关键部分的成本。如果您提供您的算法或一些示例代码,我会很乐意看一看。

      【讨论】:

      • 他确实提到了相当昂贵的 pthread_mutex_lock/unlock,尽管您是对的,但我应该编辑我的答案以仅包含 pthread_mutex_lock/unlock,因为 CriticalSection 和 Boost 锁一样相对较快。我还建议他发布一些代码和一些他可以做的事情来更改锁定部分以提高性能。
      【解决方案6】:

      pthread_mutex_lock 和 pthread_cond_wait 是操作系统原语——它们使调用线程进入睡眠状态,将控制权转移到另一个线程。 IE。它们涉及系统调用和大量开销。在两个线程之间的紧密集成中,您真的不想放弃任何控制,即使是一个周期。

      我建议使用volatile int 变量而不是互斥锁:

      volatile int data_ready = 0;
      /*  ... */
      while (!data_ready);
      process_data();
      data_ready = 0;
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-02-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-09-12
        • 2016-12-05
        相关资源
        最近更新 更多