我们在使用ConcurrentDictionary的时候,可能会有类似下面的代码:
// users的key是用户名(字符串),value是该用户的复杂信息对象。
// 其中用户名如果以"vip:"开头,则是vip用户。
// 于是我们可以用这样的代码获得所有vip用户的用户名。
var vipUserNames = from userName in users.Keys
where userName.StartsWith("vip:")
select userName.Substring(4);
在上面的这段代码里面,Keys属性的访问会导致并发性能问题。该属性是通过调用一个GetKeys()函数来获得的,而GetKeys函数则会首先通过AcquireAllLocks来锁定内部的所有锁,然后在这一锁的保护下,取出所有的Key值。正是这一个操作导致了一定程度的互相竞争。要阐明该问题及其原因,需要首先简单的说一下ConcurrentDictionary的并发设计思路。
一般而言,如果我们自己写并发字典,可能会尝试通过简单的使用lock来在读写的时候进行同步锁定。然而这种实施方式会导致在大并发的时候出现互相等待的情况,甚至最终退化成单线程处理的状态。稍微好一点的ReaderWriterLockSlim,在读操作的时候共享读阻塞写,而写操作则阻塞所有操作。这种设计在大量读少量写的时候也不失为一种不错的设计。但是不可避免的是,每一个线程的操作都会导致一次锁请求,而且说不定就会被阻塞。甚至面对大量写操作的场景,则会ReaderWriterLockSlim对写操作的排他性而互相等待,最终退化成单线程处理的状态。那么ConcurrentDictionary又有什么高招呢?
ConcurrentDictionary是这么设计的:通过将桶进行分组,每一组使用一个单独的锁对象。这样,在大量的写操作面前,也会因为写操作对象的不同而进入不同的组,互相之间被锁定的可能性就大为减少了。另一方面,其读操作被设计成为多线程操作下无需锁同步也是安全的,因此读写之间不需要互相阻塞,也不需要获取任何的锁。于是,其吞吐量通常会较一般设计要好得多。
然而,Keys属性的设计却导致并发性能的降低。因为AcquireAllLocks会锁定所有的锁,这个时候所有的写操作都会被阻塞。可是为什么要AcquireAllLocks呢?于是有人会猜测:之所以需要锁定,是因为任何的写操作都可能修改内容,导致最终结果和那一瞬间的情况不一致。然而这类的想法其实是错误的,因为:
1、线程安全问题是指,在多个线程同时操作的时候,程序不能出错(死机死循环或者什么也不干),数据一致性不违反你的期望(比如你的操作是增加,但实际上没有,甚至变成清除所有);
2、在高并发的情况下,“某一瞬间”的定义还真不好说清楚,因此仍然只能够从逻辑上来说是否违反你的预期。
如果你仍然无法接受上面那两句话,那可以继续考察一下ConcurrentDictionary的枚举操作,这一操作依赖于GetEnumerator返回的枚举器。如果你真的去看,就会发现ConcurrentDictionary所返回的枚举器并不会加锁。它实际上和普通的TryGet没有什么两样,是一种不需要锁的线程安全写法。于是,最开始的那一段代码,用下面的方式来写,就不会出问题了:
// users的key是用户名(字符串),value是该用户的复杂信息对象。
// 其中用户名如果以"vip:"开头,则是vip用户。
// 于是我们可以用这样的代码获得所有vip用户的用户名。
var vipUserNames = from userItem in users
let userName = userItem.Key
where userName.StartsWith("vip:")
select userName.Substring(4);
嗯……真的没有问题了?其实还是会有的,比如说,如果我需要按照用户名来进行排序:
// users的key是用户名(字符串),value是该用户的复杂信息对象。
// 其中用户名如果以"vip:"开头,则是vip用户。
// 于是我们可以用这样的代码获得所有vip用户的用户名。
var orderedUserNames = from userItem in users
orderby userItem.Key
select userItem.Key;
你猜现在会调用AcquireAllLocks吗?看上去会使用ConcurrentDictionary.GetEnumerator,实际上不然。其实orderby会根据待排序的对象是否为一个ICollection而区别对待,如是,则调用ICollection.CopyTo(array, 0),否,则使用IEnumerable.GetEnumerator()然后MoveNext()。如果你直接orderby,由于ConcurrentDictionary存在ICollection接口,于是会调用CopyTo,而CopyTo也和Keys对象一样,会存在一个AcquireAllLocks的调用,类似的地方还有Values属性。
除了上面的这些问题外,ConcurrentDictionary还有另一个会造成死循环的场景,不过这不是本文的主题了,留待以后再谈。
没有评论:
发表评论