首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

运行时 使进程和线程同步 Linux 和 Windows 上的高性能编程技术

运行时 使进程和线程同步 Linux 和 Windows 上的高性能编程技术

进程和线程同步本文中,我要集中探讨实现进程间和线程间同步的许多方法。通过同步,可以进行受控访问。例如,如果两个进程(或者线程)希望更新同一个共享内存计数器,那么这个计数器必须由这两个进程分别作原子更新。为实现这一点,进程必须控制计数器足够长的时间以便从内存中读取计数器,对它进行增量操作,然后将它返回内存。在大多数计算机上,这个操作由多条机器指令构成。在多指令更新过程中,为使操作系统避免发生意外的上下文环境切换,可以使用进程同步原语。
进程同步需要相互协作。每个进程必须同意遵守同步的规则。一旦达成协议,则有许多机制可用于同步。一些机制只适合于线程,一些机制适合于进程,还有一些甚至运用于网络上进行进程或线程间的同步。本文将研究专为同步而设计的计算机内的原语:
  • 信号量(Semaphores)
  • 互斥锁(Mutexes)
  • 临界段(Critical sections)
让我们从信号量开始。它可以由不同的线程或者不同的进程使用。信号量作为计数器来实现。为获得独占控制,线程必须“获得”信号量。一个“获得”转化成从信号量值中减 1。如果信号量的当前值为 0,那么进程就阻塞,直到该值减 1 后的结果大于或等于 0 为止。操作系统保证在其它线程或者进程试图进行相同的减法和 0 测试操作中,这个操作是一个原子操作。因此,为获取信号量,进程就尝试减 1 操作。如果这个操作所得到的结果是个负值,进程就阻塞。
在 Windows 上,信号量操作是                 ReleaseSemaphore() 和                 WaitForSingleObject() 。ReleaseSemaphore() 对应于给信号量的值加 1。                 WaitForSingleObject() 对应于给信号量的值减 1。            
在 Linux 上,有两类信号量。第一类是由 semget/semop/semctl API 定义的信号量的 SVR4(System V Release 4)版本。第二类是由 sem_init/sem_wait/sem_post/interfaces 定义的 POSIX 接口。 它们具有相同的功能,但接口不同。我写了一个程序,用来测试这些接口并对它们计时。这是一个单线程程序。(实际上,存在两个线程,但同步原语都以单线程方式执行。)
在我们查看代码之前,先扯开来谈些别的。编写计时循环涉及到猜测适当最大值的循环计数器来产生足够多的执行次数。在所讨论接口的基本计时已知之前,通过计时循环挑选合适的循环次数是个纯粹猜测的工作。通过在一段固定的时间内运行循环,本文程序中消除了这个问题。不是计算执行一百万次操作需要的时间,而是在一段固定的时间内,对执行的次数进行计数。 我选择 2 秒作为缺省值;本文中的所有计时都指两秒的执行周期。
计时器循环计时器循环在一段固定的时间后简单地将单个全局标志的值更改为 0。当全局标志的值为 1 时,实际的执行循环对执行的次数进行计数。当全局标志的值变为 0 时,循环终止并且提交计时报告。 为实现计时器,                 starttimedtest() 例程创建了一个线程,它只负责休眠几个纳秒后,将标志值更改为 0,并退出。计时测试实用程序显示如下。            
计时测试实用程序
        volatile int run_count = 0;    int startTimedTest(volatile int *flg)    {        static int first = 1;        if(first) {    #ifdef _WIN32            InitializeCriticalSection(&lock_run_count);    #else            (void)pthread_mutex_init(&lock_run_count,NULL);    #endif            first = 0;        }        run_count = 0;    #ifdef _WIN32        th1 = CreateThread(NULL, 4096,timerloop,(char *)flg,NULL,&timerId);        if(th1 == NULL) {            printf("CreateThread FAILED: err=%d\n",errno);            return 1;        }    #else    #   define DEC (void *(*)(void *))        if(pthread_create(&tA,NULL,DEC timerloop,(void *)&timedtestflag)) {            printf("pthread_create FAILED: err=%d\n", errno);            return 1;        }    #endif        while(run_count != 1)            YIELD;        return 0;    }    unsigned long WINDEC timerloop(void *v)    {        int *flg = (int *)v;        LOCK(&lock_run_count);        run_count++;        UNLOCK(&lock_run_count);        *flg = 1;        SLEEP(nseconds);        *flg = 0;        return 0;    }    void endTimedTest()    {    #ifdef _WIN32        (void)WaitForSingleObject(&th1, INFINITE);    #else        if(pthread_join(tA, (void **)&threadreturn)) {            printf("pthread_join FAILED: err=%d\n",errno);            return;        }    #endif    }注意                 starttimedtest() 的                 volatile int *flg 自变量。为了解释 C 或者 C++ 中的关键字                 volatile 或者                 const ,只需从右到左阅读声明。传递了一个 volatile 的整数指针作为                 starttimedtest() 其参数。“Volatile”指的是不允许编译器优化对                 *flg 表达式指定内存的间接引用。            
使用计时实用程序以及 timedtest 接口,Windows 信号量计时循环程序显示如下。
计时 Windows 信号量操作
        semaA = CreateSemaphore(NULL, 0, 1, "semaABC");    if(semaA == NULL) {        printf("CreateSemaphore \"semaABC\" failed ERROR=%d\n", GetLastError());        return 1;    }    count = 0;    timedtestflag  = 0;    if(startTimedTest(&timedtestflag))        return 1;    tstart();    while(timedtestflag) {        count++;        //        // Increment        //        if(!ReleaseSemaphore(semaA,1,0)) {            printf("ReleaseSema failed: error=%d\n",GetLastError());            return 1;        }        //        // Decrement        //        if(WaitForSingleObject(semaA, INFINITE) == WAIT_FAILED) {            printf("Wait in ALREADY_EXISTS child failed err=%d\n",                GetLastError());            return 1;        }    }    tend();    endTimedTest();    t = tval(); 计时测试实用程序为指定时间内运行所有我们的测试提供了一个简单的框架。只需要启动计时器,对执行的次数进行计数,直到标志值回到 0 为止。这些实用程序本可以与作为工作线程的附加线程一起编写。那样有利于向                 starttimedtest() 例程(真正用来计时的函数)提供一个自变量。我选择了在另一个线程中执行计时器,在只用于测试代码可视性的主线程中加入待测代码。 信号量的文档存放在 Windows Platform SDK(信号量对象)和 Linux 帮助页(输入                 man semop 可找到有关 System V 信号量的帮助页面,输入                 man sem_init 可找到有关 POSIX 信号量的页面)中。            
互斥锁互斥锁是那些值只能为 0 或 1 的信号量。一个互斥锁可以作为一个入口,每次只让一个线程或进程访问一个资源。互斥锁表示                互相排斥线程的访问;在给定的时间内,只有一个线程可以“拥有”一个互斥锁。互斥锁与信号量很相似并且它们可以在支持信号量的同一操作系统代码下实现。事实上,从下面的计时评测中可以看出,可能 Windows 正好是这样做的。UNIX 和 Linux 中的信号量先于 POSIX 线程支持的互斥锁出现。            
互斥锁的文档存放于 Windows Platform SDK(互斥锁对象)和 Linux 帮助页(                 man pthread_mutex_lock )中。            
最后,Windows 有一种称为临界段(Critical Section)的方法。                临界段为相互排斥提供了最低开销机制,但是只能由单个进程中的线程使用。它们的行为与互斥锁很相似;但开销却相当少。它们的文档存放于 Platform SDK(临界段对象)中。            
我写了一个单个程序,用来练习加锁/解锁接口。它基于前面提到的计时测试实用程序。在 Windows 2000 和 Windows XP 上运行 sync6.cpp 的结果如图 1 所示。

图 2 显示了在 Red Hat 7.1 Linux(其内核版本是 Linux 2.4.2)上运行的结果。

图 1 和图 2 出自表 1 和表 2,两表明确显示了 sync6.cpp 程序评测的时间。
接口Win2KWinXPMutex2.6292.191Sema2.5552.149CriticalSection0.0460.129
接口Linux 2.4.2

SVR5_Semaphores1.828POSIX_Semaphores0.487pthread_mutex0.262表 1 Windows 同步原语(usec/call-pair)
表 2 Linux 同步原语(usec/call-pair)
在第一张图上可以看到,Windows XP 信号量和互斥锁的执行次数改进为只有 Windows 2000 的 83%。CriticalSection API 看起来在执行时间上提高了 280%。CriticalSection API 应该取决于硬件内存互锁指令,所以为什么 Windows XP 中的 CriticalSection API 比 Windows 2000 中的慢得多,这一点不是很清楚。
Linux 接口显示了传统 SVR5 信号量是最慢的执行者,而 pthread 互斥锁是最快的执行者。尽管 Windows XP CriticalSection API 的速度降低了下来,但它们仍然比 Linux pthread 互斥锁快 2 倍。
也可能使用其它机制来处理进程或者线程同步。例如,块通信信道可以用于同步。块信道包括管道、套接字、串行线和红外信道。在这个星期的测试中,最长的时间是 Windows 2000 互斥锁,为 2.62 微秒/调用对。如果使用 Windows 上的命名管道,那么我们可以希望得到的最好的一点是移动最少数据量所涉及的开销。那将是一个单字节。在我 中公布的电子表格上可以得出,使用一个 1 字节大小的块产生的结果是每秒钟移动 144,000 个字节。那相当于每秒写/读 144,000 次,或者 6.944 微秒/调用对。 这要比这周测试中所记录的对 Windows 上任何原语的测试结果都要糟糕。除非有其它原因,否则将管道用于同步机制是个很差的性能选择。            
Linux 管道的速度比 Windows 管道的速度快。Linux 上移动一个一字节大小的块,其结果是每秒 500,000 字节或者每秒 500,000 个调用对或者每个调用对 2.0 微秒。管道几乎与 SVR4 信号量一样好(1.828 微秒),但是比 POSIX 信号量(0.487 微秒)或者 pthread 互斥对象(0.262 微秒)差了许多。除非要考虑其它事项,否则管道对于 Linux 上的进程同步也不是一个好的选择。
结束语我写了一个程序                 sync6.cpp  来演示 Windows 和 Linux 上各种进程和线程同步原语的用途并评测了它们的性能。 我们发现其中最快的原语是 Windows 临界段。我们还看到 Windows 2000 与 Windows XP 的信号量性能一起得到了改进,但是 Windows XP 的 CriticalSection API 减慢了。
返回列表