Ответ 1
Ключевое слово lock
- это просто синтаксический сахар для Monitor.Enter и Monitor.Exit:
Monitor.Enter(o);
try
{
//put your code here
}
finally
{
Monitor.Exit(o);
}
совпадает с
lock(o)
{
//put your code here
}
У меня есть функция в С#, которая может быть вызвана несколько раз из нескольких потоков, и я хочу, чтобы это было сделано только один раз, поэтому я подумал об этом:
class MyClass
{
bool done = false;
public void DoSomething()
{
lock(this)
if(!done)
{
done = true;
_DoSomething();
}
}
}
Проблема _DoSomething
занимает много времени, и я не хочу, чтобы многие потоки ожили, когда они могут просто увидеть, что done
является истинным.
Что-то вроде этого может быть обходным путем:
class MyClass
{
bool done = false;
public void DoSomething()
{
bool doIt = false;
lock(this)
if(!done)
doIt = done = true;
if(doIt)
_DoSomething();
}
}
Но просто делать блокировку и разблокировку вручную будет намного лучше.
Как я могу вручную заблокировать и разблокировать, как это делает lock(object)
? Мне нужно использовать тот же интерфейс, что и lock
, чтобы этот ручной способ и lock
блокировали друг друга (для более сложных случаев).
Ключевое слово lock
- это просто синтаксический сахар для Monitor.Enter и Monitor.Exit:
Monitor.Enter(o);
try
{
//put your code here
}
finally
{
Monitor.Exit(o);
}
совпадает с
lock(o)
{
//put your code here
}
Томас предлагает двойной контроль в своем ответе. Это проблематично. Во-первых, вам не следует использовать методы с низким уровнем блокировки, если вы не продемонстрировали, что у вас есть реальная проблема с производительностью, которая решается методом низкой блокировки. Методы с низким уровнем блокировки безумно трудны для правильного выбора.
Во-вторых, это проблематично, потому что мы не знаем, что делает "_DoSomething" или какие последствия его действий мы будем полагаться.
В-третьих, как я уже указывал в комментарии выше, кажется безумным вернуться к тому, что _DoSomething "сделано" , когда другой поток на самом деле все еще находится в процессе его выполнения. Я не понимаю, почему у вас есть это требование, и я собираюсь предположить, что это ошибка. Проблемы с этим шаблоном все еще существуют, даже если мы устанавливаем "done" после того, как "_DoSomething" делает свою вещь.
Рассмотрим следующее:
class MyClass
{
readonly object locker = new object();
bool done = false;
public void DoSomething()
{
if (!done)
{
lock(locker)
{
if(!done)
{
ReallyDoSomething();
done = true;
}
}
}
}
int x;
void ReallyDoSomething()
{
x = 123;
}
void DoIt()
{
DoSomething();
int y = x;
Debug.Assert(y == 123); // Can this fire?
}
Является ли это потокобезопасным во всех возможных реализациях С#? Я не думаю, что это так. Помните, что энергонезависимые чтения могут перемещаться во времени кэшем процессора. Язык С# гарантирует, что волатильные чтения последовательно упорядочиваются в отношении критических точек выполнения, таких как блокировки, и гарантирует, что неизменяемые чтения согласуются в одном потоке выполнения, но это не гарантирует, что неизменяемые чтения будут согласованы в любых путь через потоки выполнения.
Посмотрим на пример.
Предположим, что есть два потока: Alpha и Bravo. Оба называют DoIt в новом экземпляре MyClass. Что происходит?
В потоке Bravo кэш процессора используется для выполнения (энергонезависимой!) выборки памяти для x, которая содержит нуль. "done" происходит на другой странице памяти, которая еще не загружена в кеш.
В потоке Alpha в то же время на другом процессоре DoI вызывает DoSomething. Thread Alpha теперь запускает все. Когда поток Alpha выполняется, работа выполнена верно, а x - 123 на процессоре Alpha. Thread Alpha-процессор очищает эти факты от основной памяти.
В настоящее время программа dragvo запускает DoSomething. Он считывает страницу основной памяти, содержащую "сделанную" в кэш процессора, и видит, что это правда.
Итак, теперь "сделано" верно, но "x" по-прежнему равен нулю в кэше процессора для потока Bravo. Thread Bravo не требуется для того, чтобы аннулировать часть кеша, содержащего "x", равную нулю, потому что в потоке Bravo ни чтение "сделано" , ни "x" не было изменчивым.
Предлагаемая версия блокировки с двойной проверкой фактически не является двойной блокировкой. Когда вы меняете двойной шаблон блокировки, вам нужно начинать заново с нуля и повторно анализировать все.
Как правильно сделать эту версию шаблона, нужно сделать, по крайней мере, первое чтение "сделано" в изменчивом чтении. Тогда чтение "x" не будет разрешено перемещать "впереди" волатильного чтения в "сделано" .
Вы можете проверить значение done
до и после блокировки:
if (!done)
{
lock(this)
{
if(!done)
{
done = true;
_DoSomething();
}
}
}
Таким образом, вы не войдете в блокировку, если done
- true. Вторая проверка внутри замка заключается в том, чтобы справиться с условиями гонки, если два потока одновременно входят в первый if
.
BTW, вы не должны блокировать this
, потому что это может вызвать взаимоблокировки. Вместо этого заблокируйте личное поле (например, private readonly object _syncLock = new object()
)
Ключевое слово lock
- это просто синтаксический сахар для класса Monitor
. Также вы можете вызвать Monitor.Enter()
, Monitor.Exit()
.
Но сам класс Monitor имеет также функции TryEnter()
и Wait()
, которые могут помочь в вашей ситуации.
Я знаю, что этот ответ наступает несколько лет назад, но ни один из текущих ответов, похоже, не затрагивает ваш фактический сценарий, который стал очевидным только после comment:
В других потоках не нужно использовать какую-либо информацию, созданную ReallyDoSomething.
Если другим потокам не нужно ждать завершения операции, второй фрагмент кода в вашем вопросе будет работать нормально. Вы можете оптимизировать его дальше, полностью устранив блокировку и используя атомную операцию:
private int done = 0;
public void DoSomething()
{
if (Interlocked.Exchange(ref done, 1) == 0) // only evaluates to true ONCE
_DoSomething();
}
Кроме того, если ваш _DoSomething()
является операцией "огонь и забухание", вам может не понадобиться даже первый поток, чтобы дождаться его, позволяя ему асинхронно запускать задачу в пуле потоков:
int done = 0;
public void DoSomething()
{
if (Interlocked.Exchange(ref done, 1) == 0)
Task.Factory.StartNew(_DoSomething);
}