Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1772478
  • 博文数量: 272
  • 博客积分: 1272
  • 博客等级: 少尉
  • 技术积分: 1866
  • 用 户 组: 普通用户
  • 注册时间: 2011-03-09 15:51
文章分类

全部博文(272)

文章存档

2016年(16)

2015年(28)

2014年(97)

2013年(59)

2012年(25)

2011年(47)

分类: LINUX

2013-06-07 17:13:02

    在多核并发的编程中,我们经常要用锁来解决同步和互斥的问题,避免由于多个线程对变量、资源或代码的抢占而引起的各种问题,提高代码执行的正确性和稳定性。我想和大家在这里一起探讨一下:如果你的代码遇到要加锁的情况,可以用哪些办法来提高代码的性能。我将总结出几条结论,可以运用于平时的编程。
    最后,作为抛砖引玉,我将向大家展示在一个既解决同步互斥问题,又显著提高并行执行效率的实际设计案例(乍看上去,这个案例明显违反了编程规范)。
    多核系统中比较常用的锁包括了读写锁和自旋锁。那么哪些因素决定了加锁代码的执行效率呢?
    首先我们都知道,如果一个线程“锁”住了一段资源或代码,而其他线程也要执行这段资源或代码,那么这些线程就要等待,因此锁的冲突概率对锁的性能影响很大。其次,如果一个线程“锁”代码的时间很长,那么其他线程可能等待的时间就会变长,因此加解锁之间代码的执行时间也影响了锁的性能。
    因此,我们得出结论:加锁代码的性能取决于两点:
    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%。 在这个案例中,数据结构的设计改造看上去违反了编程规范(读用写锁,写用读锁),灵活运用读写锁和给每个数据平面分配内存的方式避免冲突,既保证了正确性,又达到了性能优化的目的。
阅读(3417) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~