.net конструкция / шаблон для блокировки по сегменту кода, а не по потоку

#c# #.net #multithreading #design-patterns #.net-4.0

#c# #.net #многопоточность #шаблоны проектирования #.net-4.0

Вопрос:

Существует ли в .net конструкция или шаблон, который определяет сегмент кода, к которому могут обращаться несколько потоков, но блокирует, если какой-либо поток находится в каком-либо другом сегменте кода (и наоборот)? Например:

 void SomeOperationA()
{
    Block( B ) 
    {   
        Segment1: 
        ... only executes if no threads are executing in Segment2 ... 
    }    
}

}

void SomeOperationB()
{
    Block( A ) 
    { 
        Segment2: 
        ... only executes if no threads are executing in Segment1 ... 
    }     
}
  

Редактировать
Несколько потоков должны иметь возможность доступа к Segment1 / Segment2 одновременно (одновременно «активен» только один сегмент. Если выполняется сегмент 1, другой поток должен иметь возможность выполнять Сегмент1, но не Сегмент2.

Правка 2

Учитывая все комментарии / ответы и мой реальный сценарий, я понимаю, что требовать, чтобы Segment2 был доступен несколькими потоками, немного безумно.

Комментарии:

1. могут ли несколько потоков выполнять Segment2 одновременно?

2. Звучит как рецепт голодания.

3. @SFun28: каково реальное применение этого?

4. Вот сценарий в реальных терминах: Segment2 сериализует ConcurrentDictionary на диск. Происходит время от времени. При сериализации мы должны убедиться, что словарь стабилен. Segment1 считывает и / или изменяет данные. Это потокобезопасно, поскольку ConcurrentDictionary потокобезопасен. Мы хотим высокую пропускную способность в Segment1, но мы хотим остановить показ при выполнении Segment2. Если мы попытаемся выполнить Segment2, мы хотим придержать строку, пока не закончим выполнение Segment1. надеюсь, это поможет.

5. Чтобы добавить к моему сценарию «реального мира» выше: вызывающий код не будет монополизировать сегмент 1 — вероятно, сегмент 1 будет сильно пострадать в течение некоторого периода времени, затем будет вызван сегмент 2 и так далее. Механизм создан только для мер безопасности.

Ответ №1:

Это довольно необычная модель защиты ресурсов от неправильного одновременного доступа. Сначала я бы подумал, нельзя ли перевести ваш вариант использования в эквивалентный сценарий, где вы можете использовать простые блокировки. Если бы вы могли предоставить более подробную информацию о том, зачем вам нужна такая схема блокировки, возможно, сообщество предложило бы другие идеи.

Чтобы ответить на ваш конкретный вопрос, ничто в .NET или даже Win32 напрямую не поддерживает эту модель блокировки, однако вы можете создать ее из других примитивов. Я бы рассмотрел возможность использования пары ReaderWriterLockSlim экземпляров для защиты каждого ресурса. Когда потоки входят в SegmentA, вы получаете блокировку чтения на запись для A и блокировку записи B … и наоборот для потоков, входящих в SegmentB. Это позволило бы нескольким потокам выполняться в каждом сегменте, но не одновременно.

РЕДАКТИРОВАТЬ: Учитывая ваш ответ в комментариях к вашему вопросу, я более убежден, что вам нужно рассмотреть возможность использования модели блокировки чтения / записи. То, что вы ищете, — это способ защитить ресурс таким образом, чтобы, когда «авторы» выполняют работу (сериализуют словарь), ни читатели, ни другие авторы не могли войти, и когда «читатели» выполняют работу, они не блокируют друг друга, но блокируют всех других авторов. Это классический случай блокировок чтения / записи.

ПРАВКА 2: Теперь, когда у меня появилось больше времени, я думаю, стоит остановиться на одном моменте. Способ думать о блокировках заключается в том, что они защищают ресурсы данных (память, файлы и т.д.), А не области кода. Тот факт, что нам нужно определить критические разделы кода, в которые одновременно может входить только один поток, является деталью реализации, которую не следует путать с тем, как используются общие ресурсы (и которые должны быть защищены). В вашем вопросе акцент на том, как контролировать, какие потоки могут входить в какой раздел кода, отвлекает от реальной проблемы: какие ресурсы данных вы пытаетесь защитить от какого рода изменений. Как только вы посмотрите на проблему с этой точки зрения, становится яснее, какие парадигмы реализации имеют смысл.

Вот несколько хороших ресурсов по моделям блокировки чтения / записи:

http://msdn.microsoft.com/en-us/magazine/cc163599.aspx

http://msdn.microsoft.com/en-us/library/bz6sth95.aspx

http://blogs.msdn.com/b/vancem/archive/2006/03/29/564854.aspx

Комментарии:

1. Я думаю, что необычным это делает многопоточный доступ к Segment2. Если мы скажем, что только один поток может одновременно обращаться к Segment2, но при этом выполнять все остальные требования, я думаю, это просто сводится к ограничению ReaderWriterLockSlim, правильно?

2. Это не будет работать с ReaderWriterLockSlim и вот почему. Вы не можете ввести блокировку записи одновременно. Это приведет к взаимному исключению, и в конечном итоге только один поток сможет выполнить SegmentA

3. Пара ReaderWriterLocks, как описано, не работает. Как только вы перехватываете блокировку записи, вы препятствуете одновременному выполнению этого блока двумя потоками…

4. @SFun28: Да, я считаю, что это правильно. Тем не менее, я бы рекомендовал такого рода сокращение, поскольку оно выводит вас на известную территорию для использования блокировки … а не в глуши.

5. @Reed Copsey: Да, я полагаю, вы правы. Я обновлю свой ответ.

Ответ №2:

Учитывая вашу правку, похоже, что правильным подходом было бы использовать ReaderWriterLockSlim, поскольку вам действительно не следует изменять коллекцию при запуске Segment2, и вы не должны разрешать запускать более 1 Segment2:

 private static ReaderWriterLockSlim readerLock = new ReaderWriterLockSlim();
void SomeOperationA()
{
    try
    {  
        readerLock.EnterReadLock(); 
        // Segment1: 
        // ... only executes if no threads are executing in Segment2 ... 
    }    
    finally
    {
        readerLock.ExitReadLock();
    }
}

void SomeOperationB()
{
    try
    {  
        readerLock.EnterWriteLock(); 
        // Prevents multiple Segment2 from serializing, and prevents all Segment1 threads...
    }    
    finally
    {
        readerLock.ExitWriteLock();
    }
}
  

Комментарии:

1. Он не сказал, что Segment2 не может выполняться одновременно. Простой lock не поможет, если это правда.

2. @Andrey: Да — я отредактировал, чтобы включить это — если это требование, все становится немного сложнее…

3. извините, я хотел включить это. Да, два потока должны иметь возможность выполнять Segment1 / 2 одновременно (но не оба сегмента одновременно!)

4. SomeOperationB() не может выполняться одновременно. Они должны работать одинаково.

5. @Andrey: Учитывая комментарии OPs о цели здесь, я не верю, что это правда…

Ответ №3:

Что-то вроде этого.

     class Segments
    {
        public const int None = 0;
        public const int Segm1 = 1;
        public const int Segm2 = 2;
    }

    int currentSegm = 0;
    int segm1counter = 0;
    int segm2counter = 0;

    object segm1lock = new object();
    object segm2lock = new object();

    void SomeOperationA()
    {
        while (
            Interlocked.CompareExchange(ref currentSegm, Segments.Segm1, Segments.Segm1) != Segments.Segm1
            amp;amp;
            Interlocked.CompareExchange(ref currentSegm, Segments.Segm1, Segments.None) != Segments.None
            )
        {
            Thread.Yield();
        }

        Interlocked.Increment(ref segm1counter);
        try
        {

            //Segment1: 
            //... only executes if no threads are executing in Segment2 ...                 
        }
        finally
        {
            lock (segm1lock)
            {
                if (Interlocked.Decrement(ref segm1counter) == 0)
                    currentSegm = Segments.None;
            }
        }
    }


    void SomeOperationB()
    {
        while (
            Interlocked.CompareExchange(ref currentSegm, Segments.Segm2, Segments.Segm2) != Segments.Segm2
            amp;amp;
            Interlocked.CompareExchange(ref currentSegm, Segments.Segm2, Segments.None) != Segments.None
            )
        {
            Thread.Yield();
        }

        Interlocked.Increment(ref segm2counter);
        try
        {

            //Segment2: 
            //... only executes if no threads are executing in Segment2 ...                 
        }
        finally
        {
            lock (segm2lock)
            {
                if (Interlocked.Decrement(ref segm2counter) == 0)
                    currentSegm = Segments.None;
            }
        }
    }
  

Хорошо, это не будет работать с блокировкой ReaderWriter/

Комментарии:

1. У меня было что-то вроде этого — но блокировки записи предотвращают 2 одновременных потока, что эффективно превращает это в один вызов «блокировки».

2. Большое спасибо за кодирование решения к моему первоначальному сообщению. Теперь я понимаю, что мой первоначальный сценарий довольно сумасшедший.

3. @SFun28 это интересная головоломка. Я не очень вникал в первопричину.