У меня есть функция, которая должна возвращать точное количество времени, прошедшее с момента последнего вызова. В настоящее время это реализовано так:
public TimeSpan Span()
{
lock (this)
{
var now = DateTime.UtcNow;
var r = now - lastCallTime;
lastCallTime = now;
return r;
}
}
Моя проблема с этим методом заключается в том, что он использует блокировку, которая может существенно повлиять на производительность.
Есть ли способ реализовать это вообще без использования блокировок?
@MatthewWatson отметил, спасибо!
@mjwills Я использую это в сочетании с рандомизацией, чтобы заставить программное обеспечение выполнять различные действия с заданной периодичностью. Реализация этого позволяет мне не хранить информацию о состоянии. Чем менее точен интервал, тем больше то, что делает программа, будет отклоняться от того, что я ей говорю.
Как часто lock
будет сталкиваться с разногласиями? В 1% случаев? 70%?
@mjwills - это часть библиотеки, которую я буду использовать снова и снова. Если есть способ реализовать это без блокировки, я бы хотел это сделать
Я бы рекомендовал использовать:
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();
Это то же решение, что и принятый ответ выше, только немного по-другому.
DateTime
имеет точность около 50 мс, так что в любом случае этого будет недостаточно. Вы должны использоватьStopwatch
.