Один из распространенных способов - если у вас есть вложенные блокировки, которые не получены в том же порядке. Поток 1 может получить блокировку A, а поток 2 может получить блокировку B, и они заблокируются.
var a = new object();
var b = new object();
lock(a) {
lock(b) {
}
}
// other thread
lock (b) {
lock(a) {
}
}
edit: неблокирующий пример .. с использованием ручек ожидания. Предположим, у Сократа и Декарта есть стейки, и они оба, будучи воспитанными философами, нуждаются в вилке и ноже, чтобы есть. Тем не менее, у них есть только один набор столового серебра, поэтому каждый может взять одну посуду и затем ждать вечно, пока другая сдаст свою посуду.
См. Обеденная философская проблема
WaitHandle fork = new AutoResetEvent(), knife = new AutoResetEvent();
while(Socrates.IsHungry) {
fork.WaitOne();
knife.WaitOne();
Eat();
fork.Set();
knife.Set();
}
// other thread
while(Descartes.IsHungry) {
knife.WaitOne();
fork.WaitOne();
Eat();
knife.Set();
fork.Set();
}