Подтвердить что ты не робот

Почему блокировка происходит намного медленнее, чем 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);;
    }
4b9b3361

Ответ 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 - хороший взгляд на блокировку спина и блокировку в целом.