因为最近研究单位项目的代码,看到里面用到了一种自己实现的读写锁,我对那把锁的性能没有什么概念,打算研究一下。所以有了一个念头,先把几种常用的同步机制实证一下性能,做一下技术准备。
测试思路
启动一组线程,在线程内做一个循环,每次循环都对一组全局 counter 累加。最后等所有的线程运行结束后,检查counter的正确性,并统计执行的时间。
测试环境
AMD 羿龙II 955x4,8G 内存,Win 7 64位,VS2010,编译平台x64.
首先定义一组全局 counter,三个counter相互独立。线程要分别对这三个累加,全局变量,初始值都是0,期待的正确结果是这三个 counter 累加后的数值依然保持一致:
1 volatile long g_counter_1; 2 volatile long g_counter_2; 3 volatile long g_counter_3;
一共会启动5个线程,每个线程的循环次数是 1000000,这样上面的三个counter加完后应该都等于 5000000
#define WORK_STEPS 1000000 #define WORKER_COUNT 5
首先看一个没有任何锁保护的线程:
1 DWORD WINAPI raw_worker_thread(void*) 2 { 3 for (int i = 0; i < WORK_STEPS; i++) 4 { 5 g_counter_1++; 6 g_counter_2++; 7 g_counter_3++; 8 } 9 return 0; 10 }
非优化编译下,生成的代码还是带循环的,加了优化会把循环约去,直接变成一具赋值语句。所以我们测试的时候都要关闭优化。
运行的结果:
counters: 1763635, 2277816, 1453466 Time cost 63 ticks. |
可以看到,运行时间很快,但是结果谬以千里。
再看一个 InterlockedExchange 版本的,这是CPU提供的一种机制,指令有 lock前缀,在更新内存的时候锁住内存总线,防止其他CPU同时也修改同一块内存。
1 DWORD WINAPI exchg_worker_thread(void*) 2 { 3 for (int i = 0; i < WORK_STEPS; i++) 4 { 5 InterlockedExchangeAdd(&g_counter_1, 1); 6 InterlockedExchangeAdd(&g_counter_2, 1); 7 InterlockedExchangeAdd(&g_counter_3, 1); 8 } 9 return 0; 10 }
这样就可以保证多线程累加得到正确的结果:
counters: 5000000, 5000000, 5000000 |
耗时略有增加。
接下来是CriticalSection版本的:
1 CRITICAL_SECTION g_critical_section; 2 3 #define init_critical_section() InitializeCriticalSection(&g_critical_section) 4 #define delete_critical_section() DeleteCriticalSection(&g_critical_section) 5 6 #define enter_critical_section() EnterCriticalSection(&g_critical_section) 7 #define leave_critical_section() LeaveCriticalSection(&g_critical_section) 8 9 DWORD WINAPI critical_section_worker_thread(void*) 10 { 11 for (int i = 0; i < WORK_STEPS; i++) 12 { 13 enter_critical_section(); 14 15 g_counter_1++; 16 g_counter_2++; 17 g_counter_3++; 18 19 leave_critical_section(); 20 } 21 return 0; 22 }
测试程序需要在线程外调用init_critical_section() 和 delete_critical_section()
测试结果如下:
counters: 5000000, 5000000, 5000000 |
可以看到,critical_section 比 InterlockedExchange 只慢了一点点。Critical Section 是一种用户态的同步机制,不用创建内核对象,所以性能会比较好。因为这里更新的是三组变量,interlocked指令要执行3次,这种指令会锁住内存总线,还会设置 memory barrier,导致CPU同步缓存,造成性能大幅下降,因此这个操作不易频繁使用。只用了3个,就几乎抵消了 interlocked 指令相对 critical section 的性能优势。
CriticalSection 还提供了自旋锁的功能,我也实现了一版带自旋的CriticalSection:
1 // use spin critical section, the spin count is adjustable. 2 CRITICAL_SECTION g_spin_critical_section; 3 4 #define SPIN_COUNT 10 5 6 #define init_spin_critical_section() InitializeCriticalSectionAndSpinCount(&g_spin_critical_section, SPIN_COUNT) 7 #define delete_spin_critical_section() DeleteCriticalSection(&g_spin_critical_section) 8 9 #define enter_spin_critical_section() EnterCriticalSection(&g_spin_critical_section) 10 #define leave_spin_critical_section() LeaveCriticalSection(&g_spin_critical_section) 11 12 DWORD WINAPI spin_critical_section_worker_thread(void*) 13 { 14 for (int i = 0; i < WORK_STEPS; i++) 15 { 16 enter_spin_critical_section(); 17 18 g_counter_1++; 19 g_counter_2++; 20 g_counter_3++; 21 22 leave_spin_critical_section(); 23 } 24 return 0; 25 }
这里的自旋数设为10,因为我发现这个数字设的越大性能越差,无论如何设置性能都不会超越不带自旋的 CriticalSection。所以我很怀疑这个自旋的实现......
counters: 5000000, 5000000, 5000000 |
再看 Mutex 版的
1 // use mutex, slowest way. 2 HANDLE g_mutex; 3 4 #define create_mutex() g_mutex = CreateMutex(NULL, FALSE, NULL); 5 #define delete_mutex() CloseHandle(g_mutex); 6 7 #define wait_mutex() WaitForSingleObject(g_mutex, INFINITE); 8 #define release_mutex() ReleaseMutex(g_mutex); 9 10 DWORD WINAPI mutex_worker_thread(void*) 11 { 12 for (int i = 0; i < WORK_STEPS; i++) 13 { 14 wait_mutex(); 15 16 g_counter_1++; 17 g_counter_2++; 18 g_counter_3++; 19 20 release_mutex(); 21 } 22 return 0; 23 }
这是性能最差的一版。因为Mutex 是操作系统的内核对象,因此成本会大增。<简直弱爆了 :-) >
counters: 5000000, 5000000, 5000000 |
最后是我自己照着 linux 实现的一个自旋锁:
1 // a spin lock 2 volatile long g_spin_lock = 0; 3 4 #define enter_spin_lock()\ 5 _GET_LOCK:\ 6 if (1 == InterlockedCompareExchange(&g_spin_lock, 1, 0))\ 7 {\ 8 while (g_spin_lock == 1);\ 9 goto _GET_LOCK;\ 10 } 11 12 #define leave_spin_lock()\ 13 g_spin_lock = 0; 14 15 DWORD WINAPI spin_lock_worker_thread(void*) 16 { 17 for (int i = 0; i < WORK_STEPS; i++) 18 { 19 enter_spin_lock(); 20 21 g_counter_1++; 22 g_counter_2++; 23 g_counter_3++; 24 25 leave_spin_lock(); 26 } 27 return 0; 28 }
本以为性能会超过 CriticalSection,结果却出乎意料:
counters: 5000000, 5000000, 5000000 |
性能真的很一般,是我哪儿没实现对吗?
总结
本文在Win7 64位+AMD CPU上实证了几种同步机制,依照性能从好到差排序,依次是:
InterlockedExchange, CriticalSection, SpinCriticalSection, SpinLock, Mutex
平时使用时,尽量使用InterlockedExchange,但也不要低估 CriticalSection 的性能。也不要迷信Spinlock。
如果不是进程间的同步,不要用Mutex,太慢了。