和操作不保证是原子的n++
。n--
每个操作分为三个阶段:
- 从内存中读取当前值
- 修改值(递增/递减)
- 将值写入内存
由于您的两个线程都在重复执行此操作,并且您无法控制线程的调度,因此您将遇到以下情况:
- 线程 1:获取
n
(值 = 0)
- Thread1:增量(值 = 1)
- 线程 2:获取
n
(值 = 0)
- 线程1:写
n
(n == 1)
- Thread2:递减(值 = -1)
- 线程 1:获取
n
(值 = 1)
- 线程2:写
n
(n == -1)
等等。
这就是为什么锁定对共享数据的访问总是很重要的原因。
- 代码:
static void Main(string[] args)
{
int n = 0;
object lck = new object();
var up = new Thread(() =>
{
for (int i = 0; i < 1000000; i++)
{
lock (lck)
n++;
}
});
up.Start();
for (int i = 0; i < 1000000; i++)
{
lock (lck)
n--;
}
up.Join();
Console.WriteLine(n);
Console.ReadLine();
}
- 编辑:更多关于如何lock
工作......
当您使用该lock
语句时,它会尝试获取您提供的对象的锁定 -lck
我上面代码中的对象。如果该对象已被锁定,则该lock
语句将导致您的代码在继续之前等待锁定被释放。
C#lock
语句实际上与Critical Section相同。实际上,它类似于以下 C++ 代码:
// declare and initialize the critical section (analog to 'object lck' in code above)
CRITICAL_SECTION lck;
InitializeCriticalSection(&lck);
// Lock critical section (same as 'lock (lck) { ...code... }')
EnterCriticalSection(&lck);
__try
{
// '...code...' goes here
n++;
}
__finally
{
LeaveCriticalSection(&lck);
}
C#lock
语句将大部分内容抽象出来,这意味着我们更难进入临界区(获取锁)而忘记离开它。
重要的是,只有您的锁定对象受到影响,并且仅与其他线程试图获取同一对象的锁定有关。没有什么能阻止您编写代码来修改锁定对象本身或访问任何其他对象。 您有责任确保您的代码尊重锁,并在写入共享对象时始终获取锁。
否则,您将得到一个不确定的结果,就像您在这段代码中看到的那样,或者规范编写者喜欢称之为“未定义的行为”。Here Be Dragons(以错误的形式出现,您将遇到无穷无尽的麻烦)。