Наконец-то вышла RTM версия .NET 4 и Visual Studio 2010. Заключительные оптимизации в финальной версии платформы проведены, и можно смело подвергнуть её тестам.
Ни для кого не секрет, что одним из значительных нововведений .NET 4 является Parallel Extensions – набор средств для облегчения распараллеливания кода и работы в многопоточной среде. В числе прочих инструментов этого набора есть и примитивы синхронизации, которые также подверглись переработке.
В частности, появился модифицированный вариант весьма популярного примитива ManualResetEvent. Для тех, кому не слишком знакомо это средство: с его помощью вы можете синхронизировать выполнение участков кода, работающих в разных потоках. Объект может находиться в 2 состояниях – установленном и неустановленном. Переход из одного в другое выполняется с помощью методов Set() и Reset(). В двух словах как это работает (здесь mre – это экземпляр типа ManualResetEvent):
Улучшенная версия этого примитива из .NET 4 называется ManualResetEventSlim. Основная идея заключается в том, чтобы снизить накладные расходы в случае, если к примитиву обращается только 1 поток. Используется т.н. “гибридная схема”, которая может быть реализована так:
Это пример из книги Рихтера “CLR via C#”, 3rd edition. Примитив SimpleHybridLock имеет пару открытых методов Enter() и Leave(). Вызовами этих методов стоит обрамить критическую секцию нашего кода, которую мы хотим выполнять всегда только в одном потоке. Код класса довольно прозрачный: первый же поток, вызвавший Enter(), увеличивает внутренний счётчик на 1. Второй поток также увеличивает счётчик, при этом блокируется до тех пор, пока кто-нибудь не вызовет Set() у объекта m_waiterLock. Т.о. если не будет конкурентного доступа к примитиву, не будут вызваны весьма “тяжелые” с точки зрения производительности методы WaitOne() и Set(). Это может положительно сказаться на скорости работы кода.
По похожему принципу построен и ManualResetEventSlim. Думаю, там предусмотрены более умные механизмы, например контроль рекурсивных вызовов и т.д. Меня как конечного пользователя платформы заинтересовала реальная разница в производительности между ManualResetEvent и его*-Slim версией. Чтобы её найти, я подготовил небольшой “бенчмарк”. Это консольное приложение такого вида:
В методе Main() создаём экземпляры примитивов и промоделируем доступ к ним из 2 потоков – основного и потока пула. При этом поток пула будет в цикле устанавливать состояние, а основной поток – сбрасывать. Повторим эксперимент COUNT раз и выведем среднее значение на экран. Вот что получилось на моём ноутбуке (2хядерный CPU T7250, Win 7 x64):
Разница очевидна и довольно существенна – примерно в 10 раз.
Т.о. предпочтительным является использование ManualResetEventSlim, поскольку не всегда при вызове Set() и Reset() будет происходить долгое обращение к объектам ядра Windows и можно выиграть “копеечку” в скорости работы ;)
Ни для кого не секрет, что одним из значительных нововведений .NET 4 является Parallel Extensions – набор средств для облегчения распараллеливания кода и работы в многопоточной среде. В числе прочих инструментов этого набора есть и примитивы синхронизации, которые также подверглись переработке.
В частности, появился модифицированный вариант весьма популярного примитива ManualResetEvent. Для тех, кому не слишком знакомо это средство: с его помощью вы можете синхронизировать выполнение участков кода, работающих в разных потоках. Объект может находиться в 2 состояниях – установленном и неустановленном. Переход из одного в другое выполняется с помощью методов Set() и Reset(). В двух словах как это работает (здесь mre – это экземпляр типа ManualResetEvent):
Поток 1 | Поток 2 | Время |
---|---|---|
mre.Reset(); mre.WaitOne(); |
//выполнение кода | 0 |
//ожидание | //выполнение кода | 1 |
//ожидание | //выполнение кода | 2 |
//ожидание | //выполнение кода | 3 |
//ожидание | mre.Set(); | 4 |
//выполнение кода | //… | 5 |
Улучшенная версия этого примитива из .NET 4 называется ManualResetEventSlim. Основная идея заключается в том, чтобы снизить накладные расходы в случае, если к примитиву обращается только 1 поток. Используется т.н. “гибридная схема”, которая может быть реализована так:
internal sealed class SimpleHybridLock : IDisposable
{
private Int32 m_waiters = 0;
private AutoResetEvent m_waiterLock = new AutoResetEvent(false);
public void Enter()
{
if (Interlocked.Increment(ref m_waiters) == 1)
return;
m_waiterLock.WaitOne();
}
public void Leave()
{
if (Interlocked.Decrement(ref m_waiters) == 0)
return;
m_waiterLock.Set();
}
public void Dispose()
{
m_waiterLock.Dispose();
}
}
* This source code was highlighted with Source Code Highlighter.
Это пример из книги Рихтера “CLR via C#”, 3rd edition. Примитив SimpleHybridLock имеет пару открытых методов Enter() и Leave(). Вызовами этих методов стоит обрамить критическую секцию нашего кода, которую мы хотим выполнять всегда только в одном потоке. Код класса довольно прозрачный: первый же поток, вызвавший Enter(), увеличивает внутренний счётчик на 1. Второй поток также увеличивает счётчик, при этом блокируется до тех пор, пока кто-нибудь не вызовет Set() у объекта m_waiterLock. Т.о. если не будет конкурентного доступа к примитиву, не будут вызваны весьма “тяжелые” с точки зрения производительности методы WaitOne() и Set(). Это может положительно сказаться на скорости работы кода.
По похожему принципу построен и ManualResetEventSlim. Думаю, там предусмотрены более умные механизмы, например контроль рекурсивных вызовов и т.д. Меня как конечного пользователя платформы заинтересовала реальная разница в производительности между ManualResetEvent и его*-Slim версией. Чтобы её найти, я подготовил небольшой “бенчмарк”. Это консольное приложение такого вида:
static void Main(string[] args)
{
ManualResetEventSlim mres = new ManualResetEventSlim(false);
ManualResetEventSlim mres2 = new ManualResetEventSlim(false);
ManualResetEvent mre = new ManualResetEvent(false);
long total = 0;
int COUNT = 50;
for (int i = 0; i < COUNT; i++)
{
mres2.Reset();
//счётчик затраченного времени
Stopwatch sw = Stopwatch.StartNew();
//запускаем установку в потоке пула
ThreadPool.QueueUserWorkItem((obj) =>
{
//Method(mres, true);
Method2(mre, true);
mres2.Set();
});
//запускаем сброс в основном потоке
//Method(mres, false);
Method2(mre, false);
//Ждём, пока выполнится поток пула
mres2.Wait();
sw.Stop();
Console.WriteLine("Pass {0}: {1} ms", i, sw.ElapsedMilliseconds);
total += sw.ElapsedMilliseconds;
}
Console.WriteLine();
Console.WriteLine("===============================");
Console.WriteLine("Done in average=" + total / (double)COUNT);
Console.ReadLine();
}
// работаем с ManualResetEventSlim
private static void Method(ManualResetEventSlim mre, bool value)
{
//в цикле повторяем действие достаточно большое число раз
for (int i = 0; i < 9000000; i++)
{
if (value)
{
mre.Set();
}
else
{
mre.Reset();
}
}
}
// работаем с классическим ManualResetEvent
private static void Method2(ManualResetEvent mre, bool value)
{
//в цикле повторяем действие достаточно большое число раз
for (int i = 0; i < 9000000; i++)
{
if (value)
{
mre.Set();
}
else
{
mre.Reset();
}
}
}
}
* This source code was highlighted with Source Code Highlighter.
В методе Main() создаём экземпляры примитивов и промоделируем доступ к ним из 2 потоков – основного и потока пула. При этом поток пула будет в цикле устанавливать состояние, а основной поток – сбрасывать. Повторим эксперимент COUNT раз и выведем среднее значение на экран. Вот что получилось на моём ноутбуке (2хядерный CPU T7250, Win 7 x64):
ManualResetEvent | ManualResetEventSlim |
![]() |
![]() |
Разница очевидна и довольно существенна – примерно в 10 раз.
Т.о. предпочтительным является использование ManualResetEventSlim, поскольку не всегда при вызове Set() и Reset() будет происходить долгое обращение к объектам ядра Windows и можно выиграть “копеечку” в скорости работы ;)