Почему блокировка происходит намного медленнее, чем Monitor.TryEnter?
Результаты
Блокировка: 85,3 микросекунды
Monitor.TryEnter: 11.0 микросекунд
Разве блокировка не распространяется на один и тот же код?
Изменить: результаты с 1000 итерациями:
Блокировка: 103,3 микросекунды
Monitor.TryEnter: 20,2 микросекунды
Код ниже. Благодаря
[Test]
public void Lock_Performance_Test()
{
const int lockIterations = 100;
Stopwatch csLock = Stopwatch.StartNew();
for (int i = 0; i < lockIterations; )
{
lock (object1)
{
i++;
}
}
csLock.Stop();
Stopwatch csMonitor = Stopwatch.StartNew();
for (int i = 0; i < lockIterations; )
{
if (Monitor.TryEnter(object1, TimeSpan.FromSeconds(10)))
{
try
{
i++;
}
finally
{
Monitor.Exit(object1);
}
}
}
csMonitor.Stop();
Console.WriteLine("Lock: {0:f1} microseconds", csLock.Elapsed.Ticks / 10M);
Console.WriteLine("Monitor.TryEnter: {0:f1} microseconds", csMonitor.Elapsed.Ticks / 10M);;
}
Ответы
Ответ 1
Я действительно не знаю ответа, но считаю важным отметить, что lock
и Monitor.TryEnter
функционально эквивалентны не. Из документация MSDN на Monitor.TryEnter
:
В случае успеха этот метод исключительная блокировка по параметру obj. Этот метод немедленно возвращается, независимо от того, доступен ли замок.
Оператор lock
аналогичен Monitor.Enter
, который потенциально блокируется. Конечно, в вашем примере кода не должно быть никаких проблем с блокировкой; но я бы сказал, что, поскольку lock
обеспечивает блокировку, он делает немного больше работы (потенциально), чем TryEnter
.
Для чего это стоит, я просто попробовал свой код на своей машине и получил полностью разные результаты:
100 итераций:
lock
: 4,4 микросекунды
Monitor.TryEnter
: 16,1 микросекунды
Monitor.Enter
: 3,9 микросекунды
100000 итераций:
lock
: 2872,5 микросекунд
Monitor.TryEnter
: 5226,6 микросекунд
Monitor.Enter
: 2432,9 микросекунды
Это серьезно подрывает мое первоначальное предположение и показывает, что в моей системе lock
(который работает примерно так же, как Monitor.Enter
) на самом деле значительно превосходит Monitor.TryEnter
.
В самом деле, я попытался это в VS 2010, ориентированном на .NET 3.5 и .NET 4.0, и хотя результаты были разными, в каждом случае lock
действительно превосходил Monitor.TryEnter
:
Версия выполнения: 2.0.50727.3603
Ran 100 раз, 100000 итераций каждый раз:
Блокировка: 279736,4 микросекунды
Monitor.TryEnter: 1366751.5 микросекунды
Monitor.TryEnter(без таймаута): 475107,3 микросекунды
Monitor.Enter: 332334.1 микросекунды
Версия выполнения: 4.0.30128.1
Ran 100 раз, 100000 итераций каждый раз:
Блокировка: 334273.7 микросекунды
Monitor.TryEnter: 1671363.4 микросекунды
Monitor.TryEnter(без таймаута): 531451.8 микросекунд
Monitor.Enter: 316693.1 микросекунды
(Заметьте, я также тестировал Monitor.TryEnter
без таймаута, так как я согласился с Марком, что вызов TimeSpan.FromSeconds
почти наверняка замедляет ваше время для Monitor.TryEnter
- и эти тесты поддерживают это, хотя это странно, поскольку в вашем случае, очевидно, lock
все еще значительно медленнее.)
Исходя из этих результатов, я очень склонен полагать, что ваше измеренное время выполнения каким-то образом повлияло на запуск этого кода с помощью атрибута Test
. Либо этот, либо этот код намного более зависим от машины, чем я ожидал.
Ответ 2
100 слишком мало, и работа в тестовой структуре может искажать вещи. Также возможно (см. Комментарии), связанные с любой дополнительной стоимостью, связанной с первым блокированием объекта; попробуйте:
- блокировка за пределами цикла сначала
- делает больше итераций
- в консоли exe, в командной строке, в режиме выпуска
Также обратите внимание, что в 4.0 lock
не Monitor.Enter(object)
- так что ожидайте разные результаты в 4.0.
Но я получаю:
lock: 3548ms
Monitor.TryEnter: 7008ms
Monitor.TryEnter (2): 2947ms
Monitor.Enter: 2906ms
Из испытательной установки:
using System;
using System.Diagnostics;
using System.Threading;
static class Program {
static void Main()
{
const int lockIterations = 50000000;
object object1 = new object();
lock (object1) { Console.WriteLine("First one has to pay an extra toll"); }
Stopwatch csLock = Stopwatch.StartNew();
for (int i = 0; i < lockIterations; ) {
lock (object1) { i++; }
}
csLock.Stop();
Console.WriteLine("lock: " + csLock.ElapsedMilliseconds + "ms");
Stopwatch csMonitorTryEnter = Stopwatch.StartNew();
for (int i = 0; i < lockIterations; ) {
if (Monitor.TryEnter(object1, TimeSpan.FromSeconds(10))) {
try { i++; } finally { Monitor.Exit(object1); }
}
}
csMonitorTryEnter.Stop();
Console.WriteLine("Monitor.TryEnter: " + csMonitorTryEnter.ElapsedMilliseconds + "ms");
csMonitorTryEnter = Stopwatch.StartNew();
for (int i = 0; i < lockIterations; ) {
if (Monitor.TryEnter(object1, 10000)) {
try { i++; } finally { Monitor.Exit(object1); }
}
}
csMonitorTryEnter.Stop();
Console.WriteLine("Monitor.TryEnter (2): " + csMonitorTryEnter.ElapsedMilliseconds + "ms");
Stopwatch csMonitorEnter = Stopwatch.StartNew();
for (int i = 0; i < lockIterations; ) {
Monitor.Enter(object1);
try { i++; } finally { Monitor.Exit(object1); }
}
csMonitorEnter.Stop();
Console.WriteLine("Monitor.Enter: " + csMonitorEnter.ElapsedMilliseconds + "ms");
}
}
Ответ 3
может быть, потому что lock является Monitor.Enter, а не Monitor.TryEnter?
Ответ 4
Вы можете использовать отражатель .NET для проверки сгенерированного ИЛ. Ключевое слово lock
использует Monitor.Enter
вместо Monitor.TryEnter
- вот короткий ответ на ваш вопрос. Вот как выглядит ваш код при его дизассемблировании и переходе на С#:
public void Lock_Performance_Test()
{
Stopwatch csLock = Stopwatch.StartNew();
int i = 0;
while (i < 100)
{
object CS$2$0000;
bool <>s__LockTaken0 = false;
try
{
Monitor.Enter(CS$2$0000 = this.object1, ref <>s__LockTaken0);
i++;
}
finally
{
if (<>s__LockTaken0)
{
Monitor.Exit(CS$2$0000);
}
}
}
csLock.Stop();
Stopwatch csMonitor = Stopwatch.StartNew();
i = 0;
while (i < 100)
{
if (Monitor.TryEnter(this.object1, TimeSpan.FromSeconds(10.0)))
{
try
{
i++;
}
finally
{
Monitor.Exit(this.object1);
}
}
}
csMonitor.Stop();
Console.WriteLine("Lock: {0:f1} microseconds", csLock.Elapsed.Ticks / 10M);
Console.WriteLine("Monitor.TryEnter: {0:f1} microseconds", csMonitor.Elapsed.Ticks / 10M);
}
Ответ 5
Если вам нужна скорость, то SpinLock - это гораздо лучший выбор в моем опыте.
public class DisposableSpinLock : IDisposable {
private SpinLock mylock;
private bool isLocked;
public DisposableSpinLock( SpinLock thelock ) {
this.mylock = thelock;
mylock.Enter( ref isLocked );
}
public DisposableSpinLock( SpinLock thelock, bool tryLock) {
this.mylock = thelock;
if( tryLock ) {
mylock.TryEnter( ref isLocked );
} else {
mylock.Enter( ref isLocked );
}
}
public bool IsLocked { get { return isLocked; } }
public void Dispose() {
Dispose( true );
GC.SuppressFinalize( this );
}
protected virtual void Dispose( bool disposing ) {
if( disposing ) {
if( isLocked ) {
mylock.Exit();
}
}
}
}
Хороший полезный способ заставить вещи работать "автоматически" в случаях прерывания и исключения.
Вы можете просто создать SpinLock вместо объекта "lock", а затем использовать:
using( new DisposableSpinLock( myLock ) ) {
// Under lock and key...
}
Это позволяет вам получить одну и ту же единицу кода, которую предоставляет функция lock(), а также иметь дело с требуемым поведением {} finally {} и иметь немного больше контроля над тем, что происходит при очистке объекта.
У меня также есть поддержка случая "try", который будет написан с использованием блоков кода с дополнительным if-выражением внутри:
using( theLock = new DisposableSpinLock( myLock ) ) {
if( theLock.IsLocked ) {
// Under Lock and Key
}
}
SpinLock не является совместимым с процессором для высококонфликтных блокировок из-за того, что в этой ситуации добавлено использование процессора SpinLock, но для блокировок, которые в значительной степени синхронизированы, и просто нужно время от времени блокировать внешние ссылки или случайный доступ к второму потоку, это большая победа.
Да, это не великолепно, но для меня SpinLocks сделали все, что у меня для слабоспорных замков, намного более результативны.
http://www.adammil.net/blog/v111_Creating_High-Performance_Locks_and_Lock-free_Code_for_NET_.html - хороший взгляд на блокировку спина и блокировку в целом.