在多核并发的编程中,我们经常要用锁来解决同步和互斥的问题,避免由于多个线程对变量、资源或代码的抢占而引起的各种问题,提高代码执行的正确性和稳定性。我想和大家在这里一起探讨一下:如果你的代码遇到要加锁的情况,可以用哪些办法来提高代码的性能。我将总结出几条结论,可以运用于平时的编程。
最后,作为抛砖引玉,我将向大家展示在一个既解决同步互斥问题,又显著提高并行执行效率的实际设计案例(乍看上去,这个案例明显违反了编程规范)。
多核系统中比较常用的锁包括了读写锁和自旋锁。那么哪些因素决定了加锁代码的执行效率呢?
首先我们都知道,如果一个线程“锁”住了一段资源或代码,而其他线程也要执行这段资源或代码,那么这些线程就要等待,因此锁的冲突概率对锁的性能影响很大。其次,如果一个线程“锁”代码的时间很长,那么其他线程可能等待的时间就会变长,因此加解锁之间代码的执行时间也影响了锁的性能。
因此,我们得出结论:加锁代码的性能取决于两点:
1、锁的冲突概率;
2、加解锁之间代码的执行时间。
既然加锁代码的性能的决定要素是冲突概率和执行时间,那么我们要想提高加锁代码的性能,着眼点就是在保证功能正确和稳定性的基础上考虑两点:
1、降低锁的冲突概率;
2、减少加解锁之间代码的执行时间。
降低锁的冲突概率
为了降低锁的冲突概率,我们可以考虑以下几个办法:
1、 考虑能否不用锁;
2、 自旋锁改读写锁;
3、 大锁变小锁。
依次举例说明。
1、考虑不用锁。
大家可以考虑这样一种情形,多个线程共用同一个内存g_stDomain处理报文,此时必须加锁。
RawSpinLock;
MemCpy(g_stDomain
, …);
RawSpinUnlock;
如果考虑给每个线程分配一个内存,就不需要锁了。
可以定义
g_stDomain[
MAX_CUPS_NR]
可以这样编码:
ulCpuIndex = GetGlobalThreadID();
MemCpy(g_stDomain[ulCpuIndex], …);
每个线程都有自己的内存,不用加锁,也就不会有冲突了。
2、自旋锁改读写锁。
这点比较好理解,常见的情形是在数据平面读配置,偶尔控制平面会写配置。在这种情形下,读数据占了绝大多数情况,由于读锁和读锁之间并不互斥,因此在采用读写锁避免了绝大多数的冲突。
3、大锁变小锁。
这种方法适用最多的情形是操作链表。
(1)其中一种情形是对Hash链表操作。
HLIST_HEAD_S *pstIpHlistHead =
DataGlobalMalloc(HASHNUM*sizeof(HLIST_HEAD_S)...);
如果定义RAW_RWLOCK_S stRwLock;
保护这个HASH链表的插入删除操作,会引起大量的冲突。假设有N次操作,那么冲突次数是N。
可以改进一下,定义RAW_RWLOCK_S
stRwLock[HASHNUM],考虑到HASH链表头是固定的,每个锁保护每个HASH链表的子链表,会大大减小冲突概率。假设有N次操作,那么冲突概率只有(N/
HASHNUM)。
(2)另外一种情形是对链表和节点的操作,假设一个单链表需要保护插入删除节点的操作,每个节点也可能会修改。
定义节点数据结构:
struct stNode;
{
。。。。。。
ULONG ulValue;
LIST_HEAD_S stListNode;
};
定义链表:
LIST_HEAD_S g_stListHead;
如果定义一个锁管理保护链表操作和节点的修改,冲突会非常多。
SPINLOCK_S g_stLock;
SpinLock(&g_stLock);
ListDel(pstNode->
stListNode);
pstNode->ulValue =
1;
SpinUnlock(&g_stLock);
假设分别有N次删除节点操作和M个节点各修改1次内容的操作,那么冲突就有N+M次。
这时可以定义两个锁,一个专门操作链表,一个专门操作节点内容修改。
struct stNode;
{
。。。。。。
ULONG ulValue;
LIST_HEAD_S stListNode;
SPINLOCK_S stNodeLock;
};
SPINLOCK_S g_stListLock;
SpinLock(&g_stListLock);
SpinLock(&pstNode->stNodeLock);
ListDel(pstNode->
stListNode);
pstNode->ulValue =
1;
SpinUnlock(&g_stListLock);
SpinUnlock(&pstNode->
stNodeLock);
修改后,假设分别有N次删除节点操作和M个节点各修改1次内容的操作,那么g_stListLock的冲突只有N次,而每个节点的stNodeLock冲突只有1次。在对链表和节点修改频繁的情况下,降低了线程等待的概率。
减少加解锁之间代码的执行时间
为了减少加解锁之间代码的执行时间,我们可以考虑以下几个办法:
1、 将锁变为原子变量;
2、 尽量减少加减锁之间的代码操作。
依次举例说明。
1、将锁变为原子变量。
如果你要保护的数据结构中只有一个变量可能会有冲突,那么修改为原子变量是更好的选择。
继续考虑上文“大锁变小锁”中第二种情形。
数据结构stNode中实际上只有一个ulValue的修改会有冲突,那么修改为原子变量,可以进一步减少执行时间,编码也更简单。
struct
stNode;
struct stNode;
{
{
。。。。。。
。。。。。。
ULONG
ulValue;
——》
ATOMIC_S atValue;
LIST_HEAD_S
stListNode;
LIST_HEAD_S stListNode;
SPINLOCK_S
stNodeLock;
};
};
2、尽量减少加减锁之间的代码操作。
我们可以考虑定时器的处理。
定时器也操作了一个链表,定义了g_stTimerLock来保护链表的操作。
添加定时器时:
SpinLock(&g_stTimerLock);
ListAdd;
SpinUnlock(&g_stTimerLock);
而在删除定时器时,在锁之间只操作了ListDel,没有对超时处理进行具体的操作。
SpinLock(&g_stTimerLock);
ListDel;
SpinUnlock(&g_stTimerLock);
处理Timerout;
——————————不放在锁之间处理,减少锁的执行时间,提高性能。
总结
1、 加锁代码的性能取决于两点:1、锁的冲突概率;2、加解锁之间代码的执行时间。
2、
提高锁的性能可以考虑以下几个办法:
(1)考虑不用锁,典型情况是给每个CPU分配内存;
(2)自旋锁改读写锁,典型情况是数据平面读配置,控制平面偶尔改配置;
(3)大锁变小锁,典型情况是对链表的操作;
(4)将锁变为原子变量,典型情况是保护数据只是几个ULONG;
(5)尽量减少加减锁之间的代码操作。
案例
抗攻击需要统计攻击报文的具体流量(单位是字节),具体方式是对抗攻击丢弃的每个报文计数并统计类型。定时器定时一段时间(比如10秒)采集一次数据,上报数据平面。
在这个需求中,考虑到数据量大,在攻击情形下几乎所有报文都要统计,可能造成很大的冲突,影响性能,而且还要考虑数据越界的情况。
第一次设计的数据结构如下:
typedef struct ANTIATTACK_Statis_st
{
ULLONG ullAntiAttackOctet [STATIS_TYPENUM];
RAW_RWLOCK_S stStatisRwLock [STATIS_TYPENUM];
}ANTIATTACK_STATIS_S;
这种数据结构的设计方式下,假设有N个udpflood攻击报文,冲突的概率是N。在100万pps的攻击测试下,性能下降接近30%,这个性能值无法接受。
考虑上述结论中的第(1)和第(4)个办法,将锁变为原子变量,并给每个CPU分配内存。
重新设计数据结构如下:
typedef struct
ANTIATTACK_Statis_st
{
ATOMIC_S atAntiAttackOctet [ MAX_CUPS_NR] [STATIS_TYPENUM];
}ANTIATTACK_STATIS_S;
此时,假设有N个udpflood攻击报文,冲突的概率是N/
MAX_CUPS_NR。在100万pps的攻击测试下,性能下降大约8%左右,有了很大的提高,但是可能有数据越界的风险。
还有没有办法继续改进?我注意到在统计过程中,绝大多数是写操作,但是可以通过给每个CPU分配内存的方式避免;但是由于定时器和每个数据平面CPU不在一起,读操作(虽然非常少)还是可能会有冲突。要是可以让数据平面写数据的时候不冲突,只在读数据的时候上锁就最好了。
重新设计数据结构:
typedef struct ANTIATTACK_Statis_st
{
ULLONG ullAntiAttackOctet [ MAX_CUPS_NR] [STATIS_TYPENUM];
RAW_RWLOCK_S stStatisRwLock [ MAX_CUPS_NR] [STATIS_TYPENUM];
}ANTIATTACK_STATIS_S;
重新设计后的数据结构,由于给每个CPU分配了内存,读锁可以并行操作,因此数据平面写数据没有冲突;而只有定时器读数据时用了写锁,此时数据被保护,不会出错。不论用哪种类型的攻击报文,冲突的概率都趋近于零。
在100万pps的攻击测试下,性能下降幅度不到1%。
在这个案例中,数据结构的设计改造看上去违反了编程规范(读用写锁,写用读锁),灵活运用读写锁和给每个数据平面分配内存的方式避免冲突,既保证了正确性,又达到了性能优化的目的。
阅读(3482) | 评论(0) | 转发(0) |