I have the following code:
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);
}
}
}
TokenHelper.GenerateToken(soloUser.Session) and TokenHelper.GenerateModeratorToken(session); could be hazardous because they may take a moment to generate a token. This will lock all users out for a that moment which could be a problem? Are there any workarounds to this logic so that I can still keep everything threadsafe?
EDIT:
I removed the TokenHelper.GenerateToken(soloUser.Session) and TokenHelper.GenerateModeratorToken(session) because I realized they can happen outside of the lock, but each SoloUser has a property called SessionId and this is generated for each user. the GenerateSession method would also be a method that takes a moment. Each user needs to have one of these SessionIds before being added to the collection
You can move the GenerateSession out of the lock if you can afford to take the lock twice and if it’s ok if occasionally a sessionId is generated but never used.
Something like this:
This basically does a quick lock to see if a new soloUser needs to be constructed and if so, then we need to generate a new session. Generating the new session happens outside the lock. We then reaquire the lock and perform the original set of operations. When constructing a new soloUser, it uses the sessionId that was constructed outside the lock.
This pattern could generate sessionIds that are never used. If two threads execute this function at the same time with the same school and major, both threads will generated session ids, but only one of the threads will successfully create a new soloUser and add it to the collection. The losing thread will find the soloUser in the collection and remove it from the collection – and not use the sessionId it just generated. At this point, both threads will be referring to the same soloUser with the same sessionId, which appears to be the goal.
If sessionIds have resources associated with them (such as an entry in a database) but these resources will be cleaned up when the sessionId ages out, then collisions like this will produce a little extra noise but overall should not impact the system.
If the generated sessionIds have nothing associated with them that would require clean up or aging out, then you might consider losing the first lock in my example and just always generate a sessionId, whether it’s needed or not. This is probably not a likely scenario, but I have used this sort of “promiscuous” trick in specialized cases before to avoid hopping in and out of high traffic locks. If it’s cheap to create and expensive to lock, then create with abandon and be careful with the locks.
Make sure the cost of GenerateSession is high enough to justify this extra run-around. If GenerateSession takes nanoseconds to complete, you don’t need all this – just leave it in the lock as originally written. If GenerateSession takes “a long time” (a second or more? 500ms or more? can’t say), then moving it out of the lock is a good idea to prevent other uses of the shared list from having to wait.