`
bollaxu
  • 浏览: 219890 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

Pthreads并行编程之spin lock与mutex性能对比分析[zz]

阅读更多

POSIX threads(简称Pthreads)是在多核平台上进行并行编程的一套常用的API。线程同步(Thread Synchronization)是并行编程中非常重要的通讯手段,其中最典型的应用就是用Pthreads提供的锁机制(lock)来对多个线程之间共 享的临界区(Critical Section)进行保护(另一种常用的同步机制是barrier)。

Pthreads提供了多种锁机制:
(1) Mutex(互斥量):pthread_mutex_***
(2) Spin lock(自旋锁):pthread_spin_***
(3) Condition Variable(条件变量):pthread_con_***
(4) Read/Write lock(读写锁):pthread_rwlock_***

Pthreads提供的Mutex锁操作相关的API主要有:
pthread_mutex_lock (pthread_mutex_t *mutex);
pthread_mutex_trylock (pthread_mutex_t *mutex);
pthread_mutex_unlock (pthread_mutex_t *mutex);

Pthreads提供的与Spin Lock锁操作相关的API主要有:
pthread_spin_lock (pthread_spinlock_t *lock);
pthread_spin_trylock (pthread_spinlock_t *lock);
pthread_spin_unlock (pthread_spinlock_t *lock);

从实现原理上来讲,Mutex属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞(blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而Spin lock则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。

如果大家去查阅Linux glibc中对pthreads API的实现NPTL(Native POSIX Thread Library) 的源码的话(使用”getconf GNU_LIBPTHREAD_VERSION”命令可以得到我们系统中NPTL的版本号),就会发现pthread_mutex_lock()操作如果没有锁成功的话就会调用system_wait()的系统调用(现在NPTL的实现采用了用户空间的futex,不需要频繁进行系统调用,性能已经大有改善),并将当前线程加入该mutex的等待队列里。而spin lock则可以理解为在一个while(1)循环中用内嵌的汇编代码实现的锁操作(印象中看过一篇论文介绍说在linux内核中spin lock操作只需要两条CPU指令,解锁操作只用一条指令就可以完成)。有兴趣的朋友可以参考另一个名为sanos的微内核中pthreds API的实现:mutex.c spinlock.c,尽管与NPTL中的代码实现不尽相同,但是因为它的实现非常简单易懂,对我们理解spin lock和mutex的特性还是很有帮助的。

那么在实际编程中mutex和spin lcok哪个的性能更好呢?我们知道spin lock在Linux内核中有非常广泛的利用,那么这是不是说明spin lock的性能更好呢?下面让我们来用实际的代码测试一下(请确保你的系统中已经安装了最近的g++)。

// Name: spinlockvsmutex1.cc
// Source: http://www.alexonlinux.com/pthread-mutex-vs-pthread-spinlock
// Compiler(spin lock version): g++ -o spin_version -DUSE_SPINLOCK spinlockvsmutex1.cc -lpthread
// Compiler(mutex version): g++ -o mutex_version spinlockvsmutex1.cc -lpthread
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>
#include <sys/time.h>
#include <list>
#include <pthread.h>
 
#define LOOPS 50000000
 
using namespace std;
 
list<int> the_list;
 
#ifdef USE_SPINLOCK
pthread_spinlock_t spinlock;
#else
pthread_mutex_t mutex;
#endif
 
//Get the thread id
pid_t gettid() { return syscall( __NR_gettid ); }
 
void *consumer(void *ptr)
{
    int i;
 
    printf("Consumer TID %lun", (unsigned long)gettid());
 
    while (1)
    {
#ifdef USE_SPINLOCK
        pthread_spin_lock(&spinlock);
#else
        pthread_mutex_lock(&mutex);
#endif
 
        if (the_list.empty())
        {
#ifdef USE_SPINLOCK
            pthread_spin_unlock(&spinlock);
#else
            pthread_mutex_unlock(&mutex);
#endif
            break;
        }
 
        i = the_list.front();
        the_list.pop_front();
 
#ifdef USE_SPINLOCK
        pthread_spin_unlock(&spinlock);
#else
        pthread_mutex_unlock(&mutex);
#endif
    }
 
    return NULL;
}
 
int main()
{
    int i;
    pthread_t thr1, thr2;
    struct timeval tv1, tv2;
 
#ifdef USE_SPINLOCK
    pthread_spin_init(&spinlock, 0);
#else
    pthread_mutex_init(&mutex, NULL);
#endif
 
    // Creating the list content...
    for (i = 0; i < LOOPS; i++)
        the_list.push_back(i);
 
    // Measuring time before starting the threads...
    gettimeofday(&tv1, NULL);
 
    pthread_create(&thr1, NULL, consumer, NULL);
    pthread_create(&thr2, NULL, consumer, NULL);
 
    pthread_join(thr1, NULL);
    pthread_join(thr2, NULL);
 
    // Measuring time after threads finished...
    gettimeofday(&tv2, NULL);
 
    if (tv1.tv_usec > tv2.tv_usec)
    {
        tv2.tv_sec--;
        tv2.tv_usec += 1000000;
    }
 
    printf("Result - %ld.%ldn", tv2.tv_sec - tv1.tv_sec,
        tv2.tv_usec - tv1.tv_usec);
 
#ifdef USE_SPINLOCK
    pthread_spin_destroy(&spinlock);
#else
    pthread_mutex_destroy(&mutex);
#endif
 
    return 0;
}
 


该程序运行过程如下:主线程先初始化一个list结构,并根据LOOPS的值将对应数量的entry插入该list,之后创建两个新线程,它们都执行consumer()这个任务。两个被创建的新线程同时对这个list进行pop操作。主线程会计算从创建两个新线程到两个新线程结束之间所用的时间,输出为下文中的”Result “。

测试机器参数:
Ubuntu 9.04 X86_64
Intel(R) Core(TM)2 Duo CPU E8400 @ 3.00GHz
4.0 GB Memory

从下面是测试结果:

gchen@gchen-desktop:~/Workspace/mutex$ g++ -o spin_version -DUSE_SPINLOCK spinvsmutex1.cc -lpthread
gchen@gchen-desktop:~/Workspace/mutex$ g++ -o mutex_version spinvsmutex1.cc -lpthread
gchen@gchen-desktop:~/Workspace/mutex$ time ./spin_version
Consumer TID 5520
Consumer TID 5521
Result - 5.888750
 
real    0m10.918s
user    0m15.601s
sys    0m0.804s
 
gchen@gchen-desktop:~/Workspace/mutex$ time ./mutex_version
Consumer TID 5691
Consumer TID 5692
Result - 9.116376
 
real    0m14.031s
user    0m12.245s
sys    0m4.368s

  

可以看见spin lock的版本在该程序中表现出来的性能更好。另外值得注意的是sys时间,mutex版本花费了更多的系统调用时间,这就是因为mutex会在锁冲突时调用system wait造成的。

但是,是不是说spin lock就一定更好了呢?让我们再来看一个锁冲突程度非常剧烈的实例程序:

Name: svm2.c
//Source: http://www.solarisinternals.com/wiki/index.php/DTrace_Topics_Locks
//Compile(spin lock version): gcc -o spin -DUSE_SPINLOCK svm2.c -lpthread
//Compile(mutex version): gcc -o mutex svm2.c -lpthread
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/syscall.h>
 
#define        THREAD_NUM     2
 
pthread_t g_thread[THREAD_NUM];
#ifdef USE_SPINLOCK
pthread_spinlock_t g_spin;
#else
pthread_mutex_t g_mutex;
#endif
__uint64_t g_count;
 
pid_t gettid()
{
    return syscall(SYS_gettid);
}
 
void *run_amuck(void *arg)
{
       int i, j;
 
       printf("Thread %lu started.n", (unsigned long)gettid());
 
       for (i = 0; i < 10000; i++) {
#ifdef USE_SPINLOCK
           pthread_spin_lock(&g_spin);
#else
               pthread_mutex_lock(&g_mutex);
#endif
               for (j = 0; j < 100000; j++) {
                       if (g_count++ == 123456789)
                               printf("Thread %lu wins!n", (unsigned long)gettid());
               }
#ifdef USE_SPINLOCK
           pthread_spin_unlock(&g_spin);
#else
               pthread_mutex_unlock(&g_mutex);
#endif
       }
 
       printf("Thread %lu finished!n", (unsigned long)gettid());
 
       return (NULL);
}
 
int main(int argc, char *argv[])
{
       int i, threads = THREAD_NUM;
 
       printf("Creating %d threads...n", threads);
#ifdef USE_SPINLOCK
       pthread_spin_init(&g_spin, 0);
#else
       pthread_mutex_init(&g_mutex, NULL);
#endif
       for (i = 0; i < threads; i++)
               pthread_create(&g_thread[i], NULL, run_amuck, (void *) i);
 
       for (i = 0; i < threads; i++)
               pthread_join(g_thread[i], NULL);
 
       printf("Done.n");
 
       return (0);
}
这个程序的特征就是临界区非常大,这样两个线程的锁竞争会非常的剧烈。当然这个是一个极端情况,实际应用程序中临界区不会如此大,锁竞争也不会如此激烈。测试结果显示mutex版本性能更好:

gchen@gchen-desktop:~/Workspace/mutex$ time ./spin
Creating 2 threads...
Thread 31796 started.
Thread 31797 started.
Thread 31797 wins!
Thread 31797 finished!
Thread 31796 finished!
Done.
 
real    0m5.748s
user    0m10.257s
sys    0m0.004s
 
gchen@gchen-desktop:~/Workspace/mutex$ time ./mutex
Creating 2 threads...
Thread 31801 started.
Thread 31802 started.
Thread 31802 wins!
Thread 31802 finished!
Thread 31801 finished!
Done.
 
real    0m4.823s
user    0m4.772s
sys    0m0.032s
 

另外一个值得注意的细节是spin lock耗费了更多的user time。这就是因为两个线程分别运行在两个核上,大部分时间只有一个线程能拿到锁,所以另一个线程就一直在它运行的core上进行忙等待,CPU占用率一直是100%;而mutex则不同,当对锁的请求失败后上下文切换就会发生,这样就能空出一个核来进行别的运算任务了。(其实这种上下文切换对已经拿着锁的那个线程性能也是有影响的,因为当该线程释放该锁时它需要通知操作系统去唤醒那些被阻塞的线程,这也是额外的开销)

总结
(1)Mutex适合对锁操作非常频繁的场景,并且具有更好的适应性。尽管相比spin lock它会花费更多的开销(主要是上下文切换),但是它能适合实际开发中复杂的应用场景,在保证一定性能的前提下提供更大的灵活度。

(2)spin lock的lock/unlock性能更好(花费更少的cpu指令),但是它只适应用于临界区运行时间很短的场景。而在实际软件开发中,除非程序员对自己的程序的锁操作行为非常的了解,否则使用spin lock不是一个好主意(通常一个多线程程序中对锁的操作有数以万次,如果失败的锁操作(contended lock requests)过多的话就会浪费很多的时间进行空等待)。

(3)更保险的方法或许是先(保守的)使用 Mutex,然后如果对性能还有进一步的需求,可以尝试使用spin lock进行调优。毕竟我们的程序不像Linux kernel那样对性能需求那么高(Linux Kernel最常用的锁操作是spin lock和rw lock)。

2010年3月3日补记:这个观点在Oracle的文档中得到了支持:

During configuration, Berkeley DB selects a mutex implementation for the architecture. Berkeley DB normally prefers blocking-mutex implementations over non-blocking ones. For example, Berkeley DB will select POSIX pthread mutex interfaces rather than assembly-code test-and-set spin mutexes because pthread mutexes are usually more efficient and less likely to waste CPU cycles spinning without getting any work accomplished.

p.s.调用syscall(SYS_gettid)和syscall( __NR_gettid )都可以得到当前线程的id:)

 

来自: www.parallellabs.com

分享到:
评论

相关推荐

    并行编程技术(linux)

    由于并行编程能够极大地提升程序的性能,特别是在多核处理器和大型分布式系统中,它已经成为现代软件开发的关键技术之一。然而,相比顺序编程,实现高效的并行程序往往要复杂得多。并行程序中要考虑的问题远多于顺序...

    并行编程原理及程序设计.pdf

    并行编程则是实现并行计算的具体方法之一,它涉及到如何将程序代码按照一定的策略分解成多个可以并行执行的部分。 #### 三、并行编程模型 并行编程模型主要分为两大类:共享内存模型和分布式内存模型。 1. **共享...

    高性能计算导论实验4-基于Pthreads并行实现通用矩阵乘法、数组求和及二次方程组求解

    实验报告.docx文件应包含了实验的详细过程、结果分析以及性能比较,例如与单线程版本的性能提升。Makefile文件用于自动化编译和运行这些程序,而test.sh可能是用于测试和验证代码正确性的脚本。 总的来说,这个实验...

    CPU&GPU的并行编程比较

    CPU与GPU并行编程比较 在现代计算机架构中,并行计算已成为提高性能的重要手段。CPU(中央处理单元)和GPU(图形处理单元)都可以实现并行计算,但它们在软件编码、硬件实现以及操作系统支持方面存在较大差异。本文...

    并行编程原理及程序设计ppt-pdf

    - **《可扩展并行算法的设计与分析》** (李晓梅, 莫则尧等著):专注于可扩展并行算法的设计和分析。 - **《数值并行计算原理与方法》** (张宝琳, 谷同祥等著):讨论了数值计算领域的并行计算原理和方法。 - **《高...

    并行编程模式_ 只有30页 然后要点击链接购买 只提供这30页

    在信息技术领域,特别是编程与软件开发领域,掌握并行编程模式是一项重要的技能。并行编程允许同时执行多个计算任务,从而在多核处理器和分布式系统中提高效率和性能。随着硬件技术的进步,多核处理器变得日益普及,...

    Pthreads_Programming(多线程编程宝典)

    《Pthreads Programming: 多线程编程宝典》是一本专为Linux环境下使用Pthreads库进行多线程编程设计的权威指南。Pthreads是POSIX线程接口的简称,是Unix和类Unix系统中用于创建和管理线程的标准API。在多核处理器...

    推荐完整优质教程课件 高性能科学计算理论和方法 第4章 用Pthreads进行共享式内存编程-课堂练习(共13页).ppt

    《高性能科学计算理论和方法》课程是一门深入探讨并行计算技术的教程,涵盖了从理论到实践的多个方面。在第四章中,重点讲解了如何使用Pthreads进行共享式内存编程,这是一种在单个系统上利用多核处理器提高计算效率...

    pthreads2 源代码

    2. 互斥锁(Mutex):使用`pthread_mutex_init()`初始化互斥锁,`pthread_mutex_lock()`和`pthread_mutex_unlock()`用于控制对共享资源的访问,确保线程安全。 3. 条件变量(Condition Variables):配合互斥锁使用...

    并行程序设计与分析 陈国良.zip

    3. **并行算法分析**:分析并行算法的性能是关键,这通常涉及时间复杂度、空间复杂度和通信开销的评估。书里可能会讲解如何使用效率指标,如速度up、效率和负载平衡,来衡量并行算法的性能。 4. **并行编程模型和...

    mpi与openmp并行程序设计ppt

    MPI(Message Passing Interface)和OpenMP是两种常见的并行编程模型,它们各自有着独特的特性和应用场景。以下是对这两个主题的详细解释以及相关知识点。 MPI(Message Passing Interface)是一种标准化的接口,...

    php线程pthreads扩展

    Pthreads适用于需要并行处理大量数据或长时间运行的任务,如Web爬虫、大规模计算、实时数据分析等。但需要注意的是,线程管理会增加程序复杂性,且可能导致内存占用增加,因此合理使用和控制线程数量至关重要。 8....

    pthreads Windows版 32位+64位

    《pthreads在Windows平台上的应用与理解》 pthreads,全称POSIXThreads,是遵循POSIX标准的一套线程接口,它最初是为类Unix操作系统设计的,但随着跨平台开发的需求增加,pthreads也被引入到了Windows系统中。本文...

    Windows下POSIX多线程编程pthreads库

    互斥量通过`pthread_mutex_lock()`和`pthread_mutex_unlock()`确保同一时间只有一个线程访问资源,而读写锁允许多个读线程同时访问,但写线程独占资源。 3. **线程终止**:`pthread_exit()`函数用于线程的正常退出...

    Linux平台PThreads库多线程编程笔记汇总

    Linux平台上的PThreads库是实现多线程编程的关键工具,它是POSIX标准的一部分,提供了跨平台的线程API。在Linux环境下,多线程编程能够有效地利用系统资源,特别是对于并行计算和并发任务,线程的优势在于它们共享...

    C++并行与分布式编程_肖和平2004译

    《C++并行与分布式编程》是一本由肖和平在2004年翻译的专业书籍,专注于探讨如何在C++编程环境中实现并行和分布式计算。C++是一种强大的、通用的编程语言,以其面向对象的特性、高效性能以及丰富的库支持而闻名。...

    pthread对二分算法进行并行化

    `pthread`库是POSIX线程(也称为Pthreads)在Unix-like系统上的实现,它提供了一种多线程编程接口。在这个场景中,我们将探讨如何使用pthread来并行化经典的二分查找算法,以提升在大型数据集上的搜索性能。 二分...

    pthreads_pthread_

    4. `pthread_mutex_t` 和 `pthread_mutex_init() / pthread_mutex_lock() / pthread_mutex_unlock() / pthread_mutex_destroy()`:互斥锁,用于保证同一时刻只有一个线程访问特定资源,防止数据竞争。 5. `pthread_...

    并行编程提升单芯片多处理性能

    并行编程是提升单芯片多处理性能的关键策略,尤其是在处理器性能优化遭遇物理限制时,例如更快的时钟速度、更深的流水线和更大的缓存可能导致功耗增加和硅片面积增大。并行处理通过将计算任务分解到多个处理器上,...

Global site tag (gtag.js) - Google Analytics