Глава.29 .Примитивные.конструкции.синхронизации.потоков
С помощью события с автосбросом можно легко реализовать блокировку
в рамках синхронизации потоков, поведение которого сходно с поведением ранее
показанного класса
SimpleSpinLock
:
internal sealed class SimpleWaitLock : IDisposable {
private readonly AutoResetEvent m_available;
public SimpleWaitLock() {
m_available = new AutoResetEvent(true); // Изначально свободен
}
public void Enter() {
// Блокирование на уровне ядра до освобождения ресурса
m_available.WaitOne();
}
public void Leave() {
// Позволяем другому потоку обратиться к ресурсу
m_available.Set();
}
public void Dispose() { m_available.Dispose(); }
}
Класс
SimpleWaitLock
применяется так же, как мы использовали бы класс
SimpleSpinLock
. Более того, внешне он ведет себя совершенно так же; а вот произ-
водительность двух вариантов блокировки отличается кардинальным образом. При
отсутствии конкуренции за блокировку класс
SimpleWaitLock
работает намного
медленнее класса
SimpleSpinLock
, поскольку каждый вызов его методов
Enter
и
Leave
заставляет поток совершить переход из управляемого кода в ядро и обратно.
Тем не менее при наличии конкуренции проигравший поток блокируется ядром
и не зацикливается, не давая впустую тратить ресурсы процессора. Имейте в виду,
что переходами из управляемого кода в ядро и обратно сопровождается также соз-
дание объекта
AutoResetEvent
и вызов для него метода
Dispose
, что отрицательно
сказывается на производительности. Впрочем, эти вызовы совершаются редко, так
что не стоит слишком сильно беспокоиться по этому поводу.
Чтобы продемонстрировать разницу в производительности, я написал следую-
щий код:
public static void Main() {
Int32 x = 0;
const Int32 iterations = 10000000; // 10 миллионов
// Сколько времени займет инкремент x 10 миллионов раз?
Stopwatch sw = Stopwatch.StartNew();
for (Int32 i = 0; i < iterations; i++) {
x++;
}
Console.WriteLine("Incrementing x: {0:N0}", sw.ElapsedMilliseconds);
// Сколько времени займет инкремент x 10 миллионов раз, если
849
Конструкции.режима.ядра
// добавить вызов ничего не делающего метода?
sw.Restart();
for (Int32 i = 0; i < iterations; i++) {
M(); x++; M();
}
Console.WriteLine("Incrementing x in M: {0:N0}", sw.ElapsedMilliseconds);
// Сколько времени займет инкремент x 10 миллионов раз, если
// добавить вызов неконкурирующего объекта SimpleSpinLock?
SpinLock sl = new SpinLock(false);
sw.Restart();
for (Int32 i = 0; i < iterations; i++) {
Boolean taken = false; sl.Enter(ref taken); x++; sl.Exit();
}
Console.WriteLine("Incrementing x in SpinLock: {0:N0}",
sw.ElapsedMilliseconds);
// Сколько времени займет инкремент x 10 миллионов раз, если
// добавить вызов неконкурирующего объекта SimpleWaitLock?
using (SimpleWaitLock swl = new SimpleWaitLock()) {
sw.Restart();
for (Int32 i = 0; i < iterations; i++) {
swl.Enter(); x++; swl.Leave();
}
Console.WriteLine(
"Incrementing x in SimpleWaitLock: {0:N0}", sw.ElapsedMilliseconds);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private st
atic void M() { /* Этот метод только возвращает управление */ }
Запустив этот код, я получил следующий результат:
Incrementing x:
8 Самый быстрый
Incrementing x in M: 69 Медленнее ~9 раз
Incrementing x in SpinLock: 164 Медленнее ~21 раз
Incrementing x i
n SimpleWaitLock: 8,854 Медленнее ~1107 раз
Как легко заметить, простой инкремент
x
занимает всего 8 мс. Простой вызов
метода до и после инкремента увеличивает время выполнения в 9 раз! Выполне-
ние кода в методе, который использует конструкции пользовательского режима,
заставило код работать в 21 (164/8) раз медленней. А теперь обратите внимание,
на сколько замедлилась программа при вставке в нее конструкций режима ядра.
Результат достигается в 1107 (8854/8) раз медленней! Поэтому если можете избе-
жать синхронизации потоков, избегайте ее. Если без нее не обойтись, задействуйте
конструкции пользовательского режима. Конструкции режима ядра следует ис-
пользовать лишь в самом крайнем случае.
Do'stlaringiz bilan baham: |