Да, есть общий способ преобразования секции lock
в использование Semaphore
с использованием того же блока try...finally
, которому эквивалентен lock
, с Semaphore
с максимальным счетчиком 1, инициализированным для счетчика 1.
ИЗМЕНИТЬ (11 мая) недавнее исследование показало мне, что моя ссылка на попытку... наконец эквивалентность устарела. В результате приведенные ниже примеры кода необходимо будет соответствующим образом скорректировать. (конец редактирования)
private readonly Semaphore semLock = new Semaphore(1, 1);
void AThread()
{
semLock.WaitOne();
try {
// Protected code
}
finally {
semLock.Release();
}
// Unprotected code
}
Однако вы бы никогда этого не сделали. lock
:
- используется для ограничения доступа к ресурсам для одного потока за раз,
- передает намерение, что ресурсы в этом разделе не могут быть одновременно доступны более чем одному потоку
И наоборот Semaphore
:
- предназначен для управления одновременным доступом к пулу ресурсов с ограничением на одновременный доступ.
- передает намерение либо пула ресурсов, к которому может получить доступ максимальное число потоков, либо управляющего потока, который может освободить некоторое количество потоков для выполнения некоторой работы, когда он будет готов.
- с максимальным количеством 1 будет работать медленнее, чем блокировка.
- может быть выпущена любой веткой, а не только той, которая вошла в раздел (добавлено в редактирование)
Изменить: вы также упомянули EventWaitHandle
в конце своего вопроса. Стоит отметить, что Semaphore
— это WaitHandle
, а не EventWaitHandle
, а также из Документация MSDN для EventWaitHandle.Set:
Нет никакой гарантии, что каждый вызов метода Set освобождает поток от EventWaitHandle, режим сброса которого — EventResetMode.AutoReset. Если два вызова расположены слишком близко друг к другу, так что второй вызов происходит до освобождения потока, освобождается только один поток. Как будто второго звонка не было.
Деталь
Вы спрашивали:
Есть ли общий способ преобразования критической секции в один или несколько семафоров? То есть есть ли какое-то прямое преобразование кода, которое можно сделать для их преобразования?
Учитывая, что:
lock (this) {
// Do protected work
}
//Do unprotected work
эквивалентен (см. ниже ссылку и примечания по этому поводу) к
** РЕДАКТИРОВАТЬ: (11 мая) в соответствии с приведенным выше комментарием этот пример кода необходимо настроить перед использованием в соответствии с эта ссылка
Monitor.Enter(this);
try {
// Protected code
}
finally {
Monitor.Exit(this);
}
// Unprotected code
Вы можете добиться того же, используя Semaphore
, выполнив:
private readonly Semaphore semLock = new Semaphore(1, 1);
void AThread()
{
semLock.WaitOne();
try {
// Protected code
}
finally {
semLock.Release();
}
// Unprotected code
}
Вы также спросили:
Например, если у меня есть два потока, выполняющих защищенную и незащищенную работу, как показано ниже. Могу ли я преобразовать их в семафоры, которые можно сигнализировать, очищать и ждать?
Это вопрос, который я изо всех сил пытался понять, поэтому я извиняюсь. В вашем примере вы называете свой метод AThread. Для меня это не совсем AThread, это AMethodToBeRunByManyThreads!!
private readonly Semaphore semLock = new Semaphore(1, 1);
void MainMethod() {
Thread t1 = new Thread(AMethodToBeRunByManyThreads);
Thread t2 = new Thread(AMethodToBeRunByManyThreads);
t1.Start();
t2.Start();
// Now wait for them to finish - but how?
}
void AMethodToBeRunByManyThreads() { ... }
Таким образом, semLock = new Semaphore(1, 1);
защитит ваш "защищенный код", но lock
больше подходит для этого использования. Разница в том, что семафор позволяет подключиться третьему потоку:
private readonly Semaphore semLock = new Semaphore(0, 2);
private readonly object _lockObject = new object();
private int counter = 0;
void MainMethod()
{
Thread t1 = new Thread(AMethodToBeRunByManyThreads);
Thread t2 = new Thread(AMethodToBeRunByManyThreads);
t1.Start();
t2.Start();
// Now wait for them to finish
semLock.WaitOne();
semLock.WaitOne();
lock (_lockObject)
{
// uses lock to enforce a memory barrier to ensure we read the right value of counter
Console.WriteLine("done: {0}", counter);
}
}
void AMethodToBeRunByManyThreads()
{
lock (_lockObject) {
counter++;
Console.WriteLine("one");
Thread.Sleep(1000);
}
semLock.Release();
}
Однако в .NET 4.5 для этого можно использовать Tasks и управлять синхронизацией основного потока.
Вот несколько мыслей:
lock(x) и Monitor.Enter — эквивалентность
Приведенное выше утверждение об эквивалентности не совсем точно. Фактически:
«[lock] точно эквивалентен [Monitor.Enter try ... finally] за исключением того, что x оценивается только один раз [by lock]» (ссылка: Спецификация языка C#)
Это мелочь, и, вероятно, не имеет значения для нас.
Возможно, вам придется быть осторожным с барьерами памяти и увеличивать поля, подобные счетчикам, поэтому, если вы используете семафор, вам все равно может понадобиться блокировка или блокировка, если вы уверены в ее использовании.
Остерегайтесь lock(this) и взаимоблокировок
Моим исходным источником для этого была бы статья Джеффри Рихтера "Безопасная синхронизация потоков". Это и общая лучшая практика:
- Не блокируйте
this
, вместо этого создайте поле object
в своем классе при создании экземпляра класса (не используйте тип значения, так как он все равно будет упакован)
- Сделайте поле
object
доступным только для чтения (личное предпочтение, но оно не только передает намерение, но и предотвращает изменение вашего объекта блокировки другими участниками кода и т. д.)
Последствий много, но чтобы облегчить командную работу, следовать рекомендациям по инкапсуляции и избежать неприятных ошибок в крайних случаях, которые трудно обнаружить тестами, лучше следовать приведенным выше правилам.
Таким образом, ваш исходный код станет:
private readonly object m_lockObject = new object();
void AThread()
{
lock (m_lockObject) {
// Do protected work
}
//Do unprotected work
}
(Примечание: обычно Visual Studio помогает вам в своих фрагментах, используя SyncRoot в качестве имени объекта блокировки)
Семафор и блокировка предназначены для разных целей
lock
предоставляет потокам место в «очереди готовности» на основе FIFO (ref. Threading in C# — Джозеф Альбахари, часть 2: базовая синхронизация, раздел: блокировка). Когда кто-нибудь видит lock
, он знает, что обычно внутри этого раздела находится общий ресурс, такой как поле класса, который должен изменяться только одним потоком за раз.
Семафор — это элемент управления, не относящийся к FIFO, для раздела кода. Он отлично подходит для сценариев издатель-подписчик (межпотоковое взаимодействие). Свобода, связанная с тем, что разные потоки могут передавать семафор тем, кто его приобрел, очень эффективна. Семантически это не обязательно означает, что «только один поток обращается к ресурсам внутри этого раздела», в отличие от lock
.
Пример: чтобы увеличить счетчик класса, вы можете использовать lock
, но не Semaphore
.
lock (_lockObject) {
counter++;
}
Но для увеличения только после того, как другой поток сказал, что это можно сделать, вы можете использовать Semaphore
, а не lock
, где поток A выполняет приращение, когда у него есть раздел семафора:.
semLock.WaitOne();
counter++;
return;
И поток B освобождает семафор, когда он готов разрешить приращение:
// when I'm ready in thread B
semLock.Release();
(Обратите внимание, что это принудительно, в этом примере может быть более подходящим WaitHandle, такой как ManualResetEvent).
Производительность
С точки зрения производительности, запустив простую программу, приведенную ниже, на небольшой многопоточной виртуальной машине, блокировка значительно превосходит Semaphore, хотя временные рамки все еще очень быстрые и достаточны для всего, кроме программного обеспечения с высокой пропускной способностью. Обратите внимание, что это ранжирование в целом было таким же при запуске теста с двумя параллельными потоками, обращающимися к блокировке.
Время для 100 итераций в тиках на маленькой ВМ (чем меньше, тем лучше):
- 291.334 (Семафор)
- 44.075 (Семафор Тонкий)
- 4.510 (Монитор.Ввод)
- 6,991 (замок)
Тактов в миллисекунду: 10000
class Program
{
static void Main(string[] args)
{
Program p = new Program();
Console.WriteLine("100 iterations in ticks");
p.TimeMethod("Semaphore", p.AThreadSemaphore);
p.TimeMethod("SemaphoreSlim", p.AThreadSemaphoreSlim);
p.TimeMethod("Monitor.Enter", p.AThreadMonitorEnter);
p.TimeMethod("Lock", p.AThreadLock);
Console.WriteLine("Ticks per millisecond: {0}", TimeSpan.TicksPerMillisecond);
}
private readonly Semaphore semLock = new Semaphore(1, 1);
private readonly SemaphoreSlim semSlimLock = new SemaphoreSlim(1, 1);
private readonly object _lockObject = new object();
const int Iterations = (int)1E6;
int sharedResource = 0;
void TimeMethod(string description, Action a)
{
sharedResource = 0;
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < Iterations; i++)
{
a();
}
sw.Stop();
Console.WriteLine("{0:0.000} ({1})", (double)sw.ElapsedTicks * 100d / (double)Iterations, description);
}
void TimeMethod2Threads(string description, Action a)
{
sharedResource = 0;
Stopwatch sw = new Stopwatch();
using (Task t1 = new Task(() => IterateAction(a, Iterations / 2)))
using (Task t2 = new Task(() => IterateAction(a, Iterations / 2)))
{
sw.Start();
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
sw.Stop();
}
Console.WriteLine("{0:0.000} ({1})", (double)sw.ElapsedTicks * (double)100 / (double)Iterations, description);
}
private static void IterateAction(Action a, int iterations)
{
for (int i = 0; i < iterations; i++)
{
a();
}
}
void AThreadSemaphore()
{
semLock.WaitOne();
try {
sharedResource++;
}
finally {
semLock.Release();
}
}
void AThreadSemaphoreSlim()
{
semSlimLock.Wait();
try
{
sharedResource++;
}
finally
{
semSlimLock.Release();
}
}
void AThreadMonitorEnter()
{
Monitor.Enter(_lockObject);
try
{
sharedResource++;
}
finally
{
Monitor.Exit(_lockObject);
}
}
void AThreadLock()
{
lock (_lockObject)
{
sharedResource++;
}
}
}
person
Andy Brown
schedule
06.05.2013
EventWaitHandle
. Наконец, есть ли особая причина, по которой вы хотите избегатьlock
? - person Jim Mischel   schedule 01.05.2013