我有以下代码:
private static HashSet<SoloUser> soloUsers = new HashSet<SoloUser>();
public void findNewPartner(string School, string Major)
{
lock (soloUsers)
{
SoloUser soloUser = soloUsers.FirstOrDefault(s => (s.School == School) && (s.Major == Major));
MatchConnection matchConn;
if (soloUser != null)
{
if (soloUser.ConnectionId != Context.ConnectionId)
{
soloUsers.Remove(soloUser);
}
}
else
{ string sessionId = TokenHelper.GenerateSession();
soloUser = new SoloUser
{
Major = Major,
School = School,
SessionId = sessionId,
ConnectionId = Context.ConnectionId
};
soloUsers.Add(soloUser);
}
}
}
CCD_ 1和CCD_ 2可能是危险的,因为它们可能需要一段时间来生成令牌。这会将所有用户锁定一段时间,这可能是个问题吗?这个逻辑有什么变通办法吗,这样我仍然可以保证一切都是线程安全的?
编辑:我删除了TokenHelper.GenerateToken(soloUser.Session)
和TokenHelper.GenerateModeratorToken(session)
,因为我意识到它们可以在锁之外发生,但每个SoloUser
都有一个名为SessionId
的属性,这是为每个用户生成的。CCD_ 7方法也将是花费一段时间的方法。每个用户在添加到集合之前都需要拥有其中一个SessionId
如果您能够承受两次锁,并且如果偶尔生成一个sessionId但从未使用过,则可以将GenerateSession移出锁。
类似这样的东西:
public void findNewPartner(string School, string Major)
{
SoloUser soloUser = null;
lock (soloUsers)
{
soloUser = soloUsers.FirstOrDefault(s => (s.School == School) && (s.Major == Major));
}
string sessionId = null;
// will we be creating a new soloUser?
if (soloUser == null)
{
// then we'll need a new session for that new user
sessionId = TokenHelper.GenerateSession();
}
lock (soloUsers)
{
soloUser = soloUsers.FirstOrDefault(s => (s.School == School) && (s.Major == Major));
if (soloUser != null)
{
// woops! Guess we don't need that sessionId after all. Oh well! Carry on...
if (soloUser.ConnectionId != Context.ConnectionId)
{
soloUsers.Remove(soloUser);
}
}
else
{
// use the sessionid computed earlier
soloUser = new SoloUser
{
Major = Major,
School = School,
SessionId = sessionId,
ConnectionId = Context.ConnectionId
};
soloUsers.Add(soloUser);
}
}
这基本上是一个快速锁定,看看是否需要构建一个新的soloUser,如果需要,那么我们需要生成一个新会话。生成新会话发生在锁之外。然后,我们重新获取锁并执行原始操作集。在构造新的soloUser时,它使用在锁外构造的sessionId。
此模式可能生成从未使用过的sessionId。如果两个线程同时使用相同的学校和专业执行此函数,两个线程都将生成会话ID,但只有其中一个线程将成功创建新的soloUser并将其添加到集合中。丢失的线程将在集合中找到soloUser并将其从集合中删除,而不使用它刚刚生成的sessionId。此时,两个线程将引用具有相同sessionId的同一soloUser,这似乎是目标。
如果sessionId有与其相关联的资源(例如数据库中的条目),但当sessionId老化时,这些资源会被清理掉,那么像这样的冲突会产生一些额外的噪音,但总体上不会影响系统。
如果生成的sessionId没有任何需要清理或老化的关联,那么在我的示例中,您可能会考虑丢失第一个锁,并始终生成sessionId,无论是否需要。这可能不是一种可能的情况,但我以前在特殊情况下使用过这种"滥交"技巧,以避免在高流量锁中进进出出。如果创建成本低,锁定成本高,那么就尽情创建,小心使用锁定。
确保GenerateSession的成本足够高,足以证明这种额外的跑路是合理的。如果GenerateSession需要纳秒才能完成,则不需要所有这些,只需将其保留在最初编写的锁中即可。如果GenerateSession需要"很长时间"(一秒钟或更长时间?500毫秒或更长时间,不能说),那么将其移出锁是一个好主意,可以防止共享列表的其他用途不得不等待。
最好的解决方案可能是使用ConcurrentBag<T>
。
请参阅http://msdn.microsoft.com/en-us/library/dd381779
我假设您使用的是.NET 4。
请注意,这是一个袋子,而不是一套。因此,您必须自己围绕"无重复"进行编码,并以线程安全的方式进行编码。