I want to lock on two objects at the same time. Why can't I write like such code?
lock (obj1, obj2)
Should I always write like that?
lock (obj1)
{
lock (obj2)
开发者_StackOverflow中文版 {
}
}
Probably this could be made simpler? Likely it would be better to introduce special private object, and use it for a lock...
Well, this question is way too old but, here is a compact one I figured out, both codes will end up to the same compiled statements (this and the one in the question description):
lock (obj1) lock (obj2)
{
// your code
}
That is the correct way to lock on multiple objects, yes.
My guess is that the reason for only allowing a single argument to the lock statement is to make the order in which locks are taken as clear as possible.
Note that you must see to it that the two locks are taken in the same order everywhere in your code, or you have a potential for deadlocks.
You could also, as you suggest, introduce a single dedicated lock object, but that would make your locking more coarse. It all depends on your needs. If you sometimes only need one of the locks, you should keep them separate (but make sure to preserve lock ordering, as mentioned above).
If you write code like this, you need to make sure, that you always lock those two objects in this order. Otherwise, you might run into deadlocks.
I have encountered the same kind of problem, and wrote this snippet that might help you out, even though it is far from perfect :
private void MultiLock(object[] locks, WaitCallback pFunc, int index = 0)
{
if (index < locks.Count())
{
lock (locks[index])
{
MultiLock(locks, pFunc, index + 1);
}
}
else
{
ThreadPool.QueueUserWorkItem(pFunc);
}
}
And then, just call this method like this :
public object LockedObject1 = new Object();
public object LockedObject2 = new Object();
public void MyFunction(object arg)
{
WaitCallback pFunc = delegate
{
// Operations on locked objects
}
MultiLock(new object[] {LockedObject1, LockedObject2}, pFunc);
}
The reason you have to do it as you wrote it, is because you can't lock two objects in the same time; You lock them one after the other (and it is very important to keep the order of the lock, otherwise you might run in to deadlocks), and it's better to be as explicit as you can with these things.
Do something like
internal static void DuoEnter(object po1, object po2, int pnTimeOutMs = 1000)
{
if ((po1 == null) && (po2 == null))
return;
int nMaxLoops = 100 * pnTimeOutMs;
bool lOneProcessor = Environment.ProcessorCount < 2;
for (int nLoops = 0; nLoops < nMaxLoops; nLoops++)
{
if ((po1 == null) || (po2 == null) || (po1 == po2))
{
if (Monitor.TryEnter(po1 ?? po2))
return;
}
else
{
if (Monitor.TryEnter(po1))
if (Monitor.TryEnter(po2))
return;
else
Monitor.Exit(po1);
}
if (lOneProcessor || (nLoops % 100) == 99)
Thread.Sleep(1); // Never use Thread.Sleep(0)
else
Thread.SpinWait(20);
}
throw new TimeoutException(
"Waited more than 1000 mS trying to obtain locks on po1 and po2");
}
internal static void DuoExit(object po1, object po2)
{
if ((po1 == null) && (po2 == null))
return;
if (po1 == null || po2 == null || po1 == po2)
Monitor.Exit(po2 ?? po1);
else
{
Monitor.Exit(po2);
Monitor.Exit(po1);
}
}
Short answer:
So in terms of engineering the difference is between when you want to make object to count as a single lock and only for that, and you do not want to allow other programmers execute any code between the lock happens, you should use in-line locking, it will prevent to put the code between locks. On the other side if you want to achieve opposite effect and allow to modify collections or execute something between the locks, then you should use nested locks. But in technical terms the will compile to the same IL code. So, the difference exist, but it isn't technical. It's placed on the precompile time, when you are writing the code.
Detailed answer:
As far as I know inside the lock it works like reference equality. If references are equal than it means it locked on the same object. That's why value types doesn't allowed to be locked as well as dynamic object (because they may change what they are referencing to and you will lost your lock).
On the other hand the underlying mechanism behind the compilation for 2021th is the same, so it's only a sugar of how you would like to use it, but each sugar has it's own cost and this isn't an exclusion.
I would like to share a little snippet of my code to improve the understand of underlying locking mechanics
private class Tes
{
private object lock1 = new object();
private object lock2 = new object();
public void Test()
{
lock(lock2) lock (this.lock1)
{
Console.WriteLine("lol");
}
lock(lock2)
{
lock(lock1)
{
Console.WriteLine("lol2");
}
}
}
}
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
var k = new Tes();
k.Test();
}
}
Under the hood it's compiled to IL (as known as CIL - common interpreted language) which is compiled to machine instructions, so we assume if IL snippets are match than execution is the same.
This is how it is compiled. The first block of locks
lock(lock2) lock (this.lock1)
{
Console.WriteLine("lol");
}
Compiled into
IL_0001: ldarg.0 // this
IL_0002: ldfld object ConsoleApp1.Program/Tes::lock2
IL_0007: stloc.0 // V_0
IL_0008: ldc.i4.0
IL_0009: stloc.1 // V_1
.try
{
IL_000a: ldloc.0 // V_0
IL_000b: ldloca.s V_1
IL_000d: call void [System.Threading]System.Threading.Monitor::Enter(object, bool&)
IL_0012: nop
// [13 17 - 13 34]
IL_0013: ldarg.0 // this
IL_0014: ldfld object ConsoleApp1.Program/Tes::lock1
IL_0019: stloc.2 // V_2
IL_001a: ldc.i4.0
IL_001b: stloc.3 // V_3
.try
{
IL_001c: ldloc.2 // V_2
IL_001d: ldloca.s V_3
IL_001f: call void [System.Threading]System.Threading.Monitor::Enter(object, bool&)
IL_0024: nop
// [14 5 - 14 6]
IL_0025: nop
// [15 6 - 15 31]
IL_0026: ldstr "lol"
IL_002b: call void [System.Console]System.Console::WriteLine(string)
IL_0030: nop
// [16 5 - 16 6]
IL_0031: nop
IL_0032: leave.s IL_003f
} // end of .try
finally
{
IL_0034: ldloc.3 // V_3
IL_0035: brfalse.s IL_003e
IL_0037: ldloc.2 // V_2
IL_0038: call void [System.Threading]System.Threading.Monitor::Exit(object)
IL_003d: nop
IL_003e: endfinally
} // end of finally
IL_003f: leave.s IL_004c
} // end of .try
finally
{
IL_0041: ldloc.1 // V_1
IL_0042: brfalse.s IL_004b
IL_0044: ldloc.0 // V_0
IL_0045: call void [System.Threading]System.Threading.Monitor::Exit(object)
IL_004a: nop
IL_004b: endfinally
} // end of finally
And the second block of c# code
lock(lock2)
{
lock(lock1)
{
Console.WriteLine("lol2");
}
}
Compiled into same IL (compare it to ensure, I have compared, but it will give you a deeper understanding of what's going on)
// [17 5 - 17 16]
IL_004c: ldarg.0 // this
IL_004d: ldfld object ConsoleApp1.Program/Tes::lock2
IL_0052: stloc.s V_4
IL_0054: ldc.i4.0
IL_0055: stloc.s V_5
.try
{
IL_0057: ldloc.s V_4
IL_0059: ldloca.s V_5
IL_005b: call void [System.Threading]System.Threading.Monitor::Enter(object, bool&)
IL_0060: nop
// [18 5 - 18 6]
IL_0061: nop
// [19 6 - 19 17]
IL_0062: ldarg.0 // this
IL_0063: ldfld object ConsoleApp1.Program/Tes::lock1
IL_0068: stloc.s V_6
IL_006a: ldc.i4.0
IL_006b: stloc.s V_7
.try
{
IL_006d: ldloc.s V_6
IL_006f: ldloca.s V_7
IL_0071: call void [System.Threading]System.Threading.Monitor::Enter(object, bool&)
IL_0076: nop
// [20 6 - 20 7]
IL_0077: nop
// [21 7 - 21 33]
IL_0078: ldstr "lol2"
IL_007d: call void [System.Console]System.Console::WriteLine(string)
IL_0082: nop
// [22 6 - 22 7]
IL_0083: nop
IL_0084: leave.s IL_0093
} // end of .try
finally
{
IL_0086: ldloc.s V_7
IL_0088: brfalse.s IL_0092
IL_008a: ldloc.s V_6
IL_008c: call void [System.Threading]System.Threading.Monitor::Exit(object)
IL_0091: nop
IL_0092: endfinally
} // end of finally
// [23 5 - 23 6]
IL_0093: nop
IL_0094: leave.s IL_00a3
} // end of .try
finally
{
IL_0096: ldloc.s V_5
IL_0098: brfalse.s IL_00a2
IL_009a: ldloc.s V_4
IL_009c: call void [System.Threading]System.Threading.Monitor::Exit(object)
IL_00a1: nop
IL_00a2: endfinally
} // end of finally
The only difference is inside the
IL_0007: stloc.0
function, but this is only a getter from top of the stack, this is based on where the code is placed, since I have moved all my code into one class and executed it synchronously - it's all on the stack.
But the meaning of technical matching doesn't mean it is the same in the practice, because you are unable to put the logs between single line locks, so you cannot ensure where you are right know
lock(obj1) lock(obj2) { Console.WriteLine(""); } //you are sure after both, but //you are unable to catch the space between them
And another way to handle middle space between the locks
lock(obj1){
Console.WriteLine("do something after first lock");
lock(obj2) {
//you are clearly know when the first lock and the second lock appers
}
}
Instead of locking the objects themselves, you create a dedicated object called PadLock or similar and only lock that one where it is needed.
Locking here doesn't mean that for duration of the lock no other code on other thread can access or modify the object. If you lock an object any other thread can modify the object at the same time. What lock code block allows you to do is to make the code inside the lock block to be single entry i.e only one thread can execute the lock code block once and other threads who tries to execute the same code block will have to wait till the owner thread is done with executing the code block. So basically you really don't need to lock 2 or more objects in usual cases. By locking your purpose is to make the code block single entry
精彩评论