Как измерить интервал между двумя вызовами метода без блокировки?

У меня есть функция, которая должна возвращать точное количество времени, прошедшее с момента последнего вызова. В настоящее время это реализовано так:

    public TimeSpan Span()
    {
        lock (this)
        {
            var now = DateTime.UtcNow;
            var r = now - lastCallTime;
            lastCallTime = now;
            return r;
        }
    }

Моя проблема с этим методом заключается в том, что он использует блокировку, которая может существенно повлиять на производительность.

Есть ли способ реализовать это вообще без использования блокировок?

DateTime имеет точность около 50 мс, так что в любом случае этого будет недостаточно. Вы должны использовать Stopwatch.
Matthew Watson 12.09.2018 14:36

@MatthewWatson отметил, спасибо!

Arsen Zahray 12.09.2018 14:37

@mjwills Я использую это в сочетании с рандомизацией, чтобы заставить программное обеспечение выполнять различные действия с заданной периодичностью. Реализация этого позволяет мне не хранить информацию о состоянии. Чем менее точен интервал, тем больше то, что делает программа, будет отклоняться от того, что я ей говорю.

Arsen Zahray 12.09.2018 14:40

Как часто lock будет сталкиваться с разногласиями? В 1% случаев? 70%?

mjwills 12.09.2018 14:41

@mjwills - это часть библиотеки, которую я буду использовать снова и снова. Если есть способ реализовать это без блокировки, я бы хотел это сделать

Arsen Zahray 12.09.2018 14:44
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
5
81
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Я бы рекомендовал использовать:

public long lastTimestamp = Stopwatch.GetTimestamp();

public TimeSpan Span()
{
    do
    {
        long oldValue = lastTimestamp;
        long currentTimestamp = Stopwatch.GetTimestamp();

        var previous = Interlocked.CompareExchange(ref lastTimestamp, currentTimestamp, oldValue);

        if (previous == oldValue)
        {
            // We effectively 'got the lock'
            var ticks = (currentTimestamp - oldValue) * 10_000_000 / Stopwatch.Frequency;
            return new TimeSpan(ticks);
        }
    } while (true);

    // Will never reach here
    // return new TimeSpan(0);
}

Это будет потокобезопасным без необходимости явного lock. И если есть разногласия по lastTimestamp, тогда код будет зацикливаться, пока не заработает. Это означает, что несколько вызовов Span могут не «завершиться» в том же порядке, в котором они «начали».

Более простой подход для рассмотрения (но см. Предупреждение ниже):

public long lastTimestamp = Stopwatch.GetTimestamp();

public TimeSpan Span()
{
    long currentTimestamp = Stopwatch.GetTimestamp();

    var previous = Interlocked.Exchange(ref lastTimestamp, currentTimestamp);

    var ticks = (currentTimestamp - previous) * 10_000_000 / Stopwatch.Frequency;

    return new TimeSpan(ticks);
}

Это будет потокобезопасным без необходимости явного lock. Interlocked.Exchangeв целомпревосходитlock.

Согласно документы, Interlocked.Exchange:

Sets a 64-bit signed integer to a specified value and returns the original value, as an atomic operation.

Этот код проще, но из-за того, как работает Interlocked.Exchange (см. Отличный ответ Мэтью Ватсона), возвращаемый TimeSpan может быть отрицательный в сценариях с высокой конкуренцией. Этого не произойдет с первым решением, но первое решение будет работать медленнее с высокой конкуренцией.

Для полноты картины я хочу показать вам, как более простой код (второе решение) из принятого ответа мог возвращает отрицательное значение, как указано в этом ответе.

В этом мысленном эксперименте используются две нити, обозначенные T1 и T2. Я добавляю к переменным стека префиксы T1 и T2, чтобы вы могли различить их (ниже).

Предположим, что lastTimeStamp начинается с 900, а текущее время - 1000.

Теперь рассмотрим следующие операции с чересстрочным потоком:

T1: long currentTimestamp = Stopwatch.GetTimestamp(); 
    => T1:currentTimeStamp = 1000
T2: long currentTimestamp = Stopwatch.GetTimestamp(); 
    => T2:currentTimeStamp = 1010
T2: var previous = Interlocked.Exchange(ref lastTimestamp, T2:currentTimestamp);
    => T2:previous = 900, lastTimestamp = 1010
T1: var previous = Interlocked.Exchange(ref lastTimestamp, T1:currentTimestamp);
    => T1:previous = 1010, lastTimestamp = 1000
T1: var ticks = (T1:currentTimestamp - T1:previous)
    => ticks = 1000 - 1010 = -10
T2: var ticks = (T2:currentTimestamp - T2:previous)
    => ticks = 1010 - 900 = 110

Как видите, поток T1 вернет -10.


[Приложение]

Вот мой взгляд на это - я не пытаюсь преобразовать временную метку секундомера в TimeSpan; Я просто оставляю его в единицах, возвращенных из Stopwatch.GetTimestamp(), для краткости (и это будет немного быстрее):

public static long Span()
{
    long previous;
    long current;

    do
    {
        previous = lastTimestamp;
        current = Stopwatch.GetTimestamp();
    }
    while (previous != Interlocked.CompareExchange(ref lastTimestamp, current, previous));

    return current - previous;
}

static long lastTimestamp = Stopwatch.GetTimestamp();

Это то же решение, что и принятый ответ выше, только немного по-другому.

Другие вопросы по теме