问题
Is there any way to lock on an integer in C#? Integers can not be used with lock because they are boxed (and lock only locks on references).
The scenario is as follows: I have a forum based website with a moderation feature. What I want to do is make sure that no more than one moderator can moderate a post at any given time. To achieve this, I want to lock on the ID of the post.
I've had a couple of ideas so far (e.g. using a dictionary<int, object>), but I'm looking for a better and cleaner way.
Any suggestions?
回答1:
I like doing it like this
public class Synchronizer {
private Dictionary<int, object> locks;
private object myLock;
public Synchronizer() {
locks = new Dictionary<int, object>();
myLock = new object();
}
public object this[int index] {
get {
lock (myLock) {
object result;
if (locks.TryGetValue(index, out result))
return result;
result = new object();
locks[index] = result;
return result;
}
}
}
}
Then, to lock on an int you simply (using the same synchronizer every time)
lock (sync[15]) { ... }
This class returns the same lock object when given the same index twice. When a new index comes, it create an object, returning it, and stores it in the dictionary for next times.
It can easily be changed to work generically with any struct
or value type, or to be static
so that the synchronizer object does not have to be passed around.
回答2:
If it's a website then using an in-process lock probably isn't the best approach as if you need to scale the site out onto multiple servers, or add another site hosting an API (or anything else that would require another process accessing the same data to exist) then all your locking strategies are immediately ineffective.
I'd be inclined to look into database-based locking for this. The simplest approach is to use optimistic locking with something like a timestamp of when the post was last updated, and to reject updates made to a post unless the timestamps match.
回答3:
I've read a lot of comments mentioning that locking isn't safe for web applications, but, other than web farms, I haven't seen any explanations of why. I would be interested in hearing the arguments against it.
I have a similar need, though I'm caching re-sized images on the hard drive (which is obviously a local action so a web farm scenario isn't an issue).
Here is a redone version of what @Configurator posted. It includes a couple features that @Configurator didn't include:
- Unlocking: Ensures the list doesn't grow unreasonably large (we have millions of photos and we can have many different sizes for each).
- Generic: Allows locking based on different data types (such as int or string).
Here's the code...
/// <summary>
/// Provides a way to lock a resource based on a value (such as an ID or path).
/// </summary>
public class Synchronizer<T>
{
private Dictionary<T, SyncLock> mLocks = new Dictionary<T, SyncLock>();
private object mLock = new object();
/// <summary>
/// Returns an object that can be used in a lock statement. Ex: lock(MySync.Lock(MyValue)) { ... }
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public SyncLock Lock(T value)
{
lock (mLock)
{
SyncLock theLock;
if (mLocks.TryGetValue(value, out theLock))
return theLock;
theLock = new SyncLock(value, this);
mLocks.Add(value, theLock);
return theLock;
}
}
/// <summary>
/// Unlocks the object. Called from Lock.Dispose.
/// </summary>
/// <param name="theLock"></param>
public void Unlock(SyncLock theLock)
{
mLocks.Remove(theLock.Value);
}
/// <summary>
/// Represents a lock for the Synchronizer class.
/// </summary>
public class SyncLock
: IDisposable
{
/// <summary>
/// This class should only be instantiated from the Synchronizer class.
/// </summary>
/// <param name="value"></param>
/// <param name="sync"></param>
internal SyncLock(T value, Synchronizer<T> sync)
{
Value = value;
Sync = sync;
}
/// <summary>
/// Makes sure the lock is removed.
/// </summary>
public void Dispose()
{
Sync.Unlock(this);
}
/// <summary>
/// Gets the value that this lock is based on.
/// </summary>
public T Value { get; private set; }
/// <summary>
/// Gets the synchronizer this lock was created from.
/// </summary>
private Synchronizer<T> Sync { get; set; }
}
}
Here's how you can use it...
public static readonly Synchronizer<int> sPostSync = new Synchronizer<int>();
....
using(var theLock = sPostSync.Lock(myID))
lock (theLock)
{
...
}
回答4:
I would personally go with either Greg's or Konrad's approach.
If you really do want to lock
against the post ID itself (and assuming that your code will only ever be running in a single process) then something like this isn't too dirty:
public class ModeratorUtils
{
private static readonly HashSet<int> _LockedPosts = new HashSet<int>();
public void ModeratePost(int postId)
{
bool lockedByMe = false;
try
{
lock (_LockedPosts)
{
lockedByMe = _LockedPosts.Add(postId);
}
if (lockedByMe)
{
// do your editing
}
else
{
// sorry, can't edit at this time
}
}
finally
{
if (lockedByMe)
{
lock (_LockedPosts)
{
_LockedPosts.Remove(postId);
}
}
}
}
}
回答5:
This option builds on the good answer provided by configurator with the following modifications:
- Prevents the size of the dictionary from growing uncontrollably. Since, new posts will get new ids, your dictionary of locks will grow indefinitely. The solution is to mod the id against a maximum dictionary size. This does mean that some ids will have the same lock (and have to wait when they would otherwise not have to), but this will be acceptable for some dictionary size.
- Uses ConcurrentDictionary so there is no need for a separate dictionary lock.
The code:
internal class IdLock
{
internal int LockDictionarySize
{
get { return m_lockDictionarySize; }
}
const int m_lockDictionarySize = 1000;
ConcurrentDictionary<int, object> m_locks = new ConcurrentDictionary<int, object>();
internal object this[ int id ]
{
get
{
object lockObject = new object();
int mapValue = id % m_lockDictionarySize;
lockObject = m_locks.GetOrAdd( mapValue, lockObject );
return lockObject;
}
}
}
Also, just for completeness, there is the alternative of string interning: -
- Mod the id against the maximum number of interned id strings you will allow.
- Convert this modded value to a string.
- Concatenate the modded string with a GUID or namespace name for name collision safety.
- Intern this string.
- lock on the interned string. See this answer for some information:
The only benefit of the string interning approach is that you don't need to manage a dictionary. I prefer the dictionary of locks approach as the intern approach makes a lot of assumptions about how string interning works and that it will continue to work in this way. It also uses interning for something it was never meant / designed to do.
回答6:
Why don't you lock on the whole posting instead just on its ID?
回答7:
Coresystem at codeplex has two class for thread synchronization based on value types, for details see http://codestand.feedbook.org/2012/06/lock-on-integer-in-c.html
回答8:
I doubt you should use a database or O/S level feature such as locks for a business level decision. Locks incur significant overheads when held for long times (and in these contexts, anything beyond a couple of hundred milliseconds is an eternity).
Add a status field to the post. If you deal with several therads directly, then you can use O/S level locks -- to set the flag.
回答9:
You need a whole different approach to this.
Remember that with a website, you don't actually have a live running application on the other side that responds to what the user does.
You basically start a mini-app, which returns the web-page, and then the server is done. That the user ends up sending some data back is a by-product, not a guarantee.
So, you need to lock to persist after the application has returned the moderation page back to the moderator, and then release it when the moderator is done.
And you need to handle some kind of timeout, what if the moderator closes his browser after getting the moderation page back, and thus never communicates back with the server that he/she is done with the moderation process for that post.
回答10:
Ideally you can avoid all the complex and brittle C# locking and replace it with database locking, if your transactions are designed correctly then you should be able to get by with DB transactions only.
回答11:
Two boxed integers that happen to have the same value are completely indepent objects. So if you wanted to do this, your idea of Dictionary would probably be the way to go. You'd need to synchronize access to the dictionary to make sure you are always getting the same instance. And you'd have the problem of the dictionary growing in size.
回答12:
C# locking is for thread safety and doesn't work the way you want it to for web applications.
The simplest solution is adding a column to the table that you want to lock and when somone locks it write to the db that that column is locked.
Dont let anyone open a post in edit mode if the column is locked for editing.
Otherwise maintain a static list of locked entry Ids and compare to that before allowing an edit.
回答13:
You want to make sure that a delete doesn't happen twice?
CREATE PROCEDURE RemovePost( @postID int )
AS
if exists(select postID from Posts where postID = @postID)
BEGIN
DELETE FROM Posts where postID = @postID
-- Do other stuff
END
This is pretty much SQL server syntax, I'm not familiar with MyISAM. But it allows stored procedures. I'm guessing you can mock up a similar procedure.
Anyhow, this will work for the majority of cases. The only time it will fail is if two moderators submit at almost exactly the same time, and the exists() function passes on one request just before the DELETE statement executes on another request. I would happily use this for a small site. You could take it a step further and check that the delete actually deleted a row before continuing with the rest, which would guarantee the atomicity of it all.
Trying to create a lock in code, for this use case, I consider very impractical. You lose nothing by having two moderators attempting to delete a post, with one succeeding, and the other having no effect.
回答14:
You should use a sync object like this:
public class YourForm
{
private static object syncObject = new object();
public void Moderate()
{
lock(syncObject)
{
// do your business
}
}
}
But this approach shouldn't be used in a web app scenario.
来源:https://stackoverflow.com/questions/781189/how-to-lock-on-an-integer-in-c