Используя заявление замка в петле в C#

Позволяет посещают типовой урок SomeThread, где мы пытаемся препятствовать тому, чтобы методы DoSomething были названы после того, как Бегущая собственность установлена в ложный, и Расположите, назван классом OtherThread, потому что, если бы их называют после того, как Расположить метод - мир, закончился бы, поскольку мы знаем это.

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

Я смотрел на способы обращаться с этим, используя петлю в простом легком, чтобы вести метод. Для отчета я сделал считал Проверку Двойного замка скопированной, однако это, кажется, не, рекомендуют для C#.

Предупреждение: Это - упрощенный пример, чтобы попытаться помочь сосредоточиться на проблеме с петлей и захватывающий в одной. Если я не разрабатывал достаточно некоторое место, пожалуйста, сообщите мне, и я приложу все усилия, чтобы заполнить любые детали.

public class SomeThread : IDisposable
{
    private object locker = new object();
    private bool running = false;

    public bool Running 
    { 
        get
        {
            lock(locker)
            {
                return running;
            }
        }
        set
        {
            lock(locker)
            {
                running = value;
            }
        }
    }

    public void Run()
    {
        while (Running)
        {
            lock(locker)
            {
                DoSomething1();
                DoSomething2();
            }
        }
    }

    private void DoSomething1()
    {
        // something awesome happens here
    }

    private void DoSomething2()
    {
        // something more awesome happens here
    }

    public void Dispose()
    {
        lock (locker)
        {   
            Dispose1();
            Dispose2();
        }
    }

    private void Dispose1()
    {
        // something awesome happens here
    }

    private void Dispose2()
    {
        // something more awesome happens here
    }

}

public class OtherThread
{
    SomeThread st = new SomeThread();

    public void OnQuit()
    {
        st.Running = false;
        st.Dispose();

        Exit();
    }
}
14
задан Rodney S. Foley 21 January 2010 в 21:59
поделиться

3 ответа

Сделайте шаг назад.

Начните с указания всех желательных и нежелательных характеристик перед тем, как вы начнете писать решение. Некоторые сразу приходят на ум:

  • «Работа» выполняется в потоке W. «Пользовательский интерфейс» выполняется в потоке U.
  • Работа выполняется в «единицах работы». Каждая единица работы «короткая» по продолжительности, для некоторого определения «краткости». Назовем метод, выполняющий работу, M ().
  • Работа выполняется W непрерывно в цикле, пока U не прикажет ему остановиться.
  • U вызывает метод очистки, D (), когда вся работа сделана.
  • D () никогда не должен выполняться до или во время выполнения M ().
  • Exit () должен вызываться после D () в потоке U.
  • U никогда не должен блокироваться на «долгое» время; это приемлемо для его блокировки на «короткое» время.
  • Никаких тупиков и т. Д.

Суммирует ли это проблемное пространство?

Прежде всего, отмечу, что на первый взгляд кажется , что проблема в том, что U должен быть вызывающим D (). Если бы W вызывала D (), вам не о чем было бы беспокоиться; вы просто сигнализируете W о выходе из цикла, а затем W вызовет D () после цикла. Но это просто меняет одну проблему на другую; предположительно в этом сценарии U должен дождаться, пока W вызовет D (), прежде чем U вызовет Exit (). Таким образом, перемещение вызова D () с U на W на самом деле не упрощает проблему.

Вы сказали, что не хотите использовать блокировку с двойной проверкой. Вы должны знать, что, начиная с CLR v2, шаблон блокировки с двойной проверкой считается безопасным. Гарантии модели памяти были усилены в v2. Таким образом, для вас, вероятно, будет безопасно использовать блокировку с двойной проверкой.

ОБНОВЛЕНИЕ: Вы запросили информацию о (1), почему блокировка с двойной проверкой безопасна в версии 2, а не в версии 1? и (2) почему я использовал ласковое слово «вероятно»?

Чтобы понять, почему блокировка с двойной проверкой небезопасна в модели памяти CLR v1, но безопасна в модели памяти CLR v2, прочтите это:

http : //web.archive.org/web/20150326171404/https: //msdn.microsoft.com/en-us/magazine/cc163715.aspx

Я сказал «вероятно», потому что, как мудро сказал Джо Даффи:

как только вы отваживаетесь хотя бы немного выйти за пределы немногих "благословенных" безблокировочных практик [...], вы открываете себя для худшего {{ 1}} условий гонки.

Я не знаю, планируете ли вы правильно использовать блокировку с двойной проверкой или планируете написать свой собственный умный, неработающий вариант блокировки с двойной проверкой, который на самом деле ужасно умирает на машинах IA64. Следовательно, он , вероятно, у вас сработает, если ваша проблема действительно поддается блокировке с двойной проверкой и , вы напишете код правильно.

Если вам это небезразлично, прочтите статьи Джо Даффи:

http://www.bluebytesoftware.com/blog/2006/01/26/BrokenVariantsOnDoublecheckedLocking.aspx

и

http: // www.bluebytesoftware.com/blog/2007/02/19/RevisitedBrokenVariantsOnDoubleCheckedLocking.aspx

И этот вопрос SO имеет хорошее обсуждение:

Необходимость в модификаторе volatile в блокировке с двойной проверкой в ​​.NET

Вероятно, это Лучше всего найти какой-нибудь другой механизм, кроме блокировки с двойной проверкой.

Существует механизм ожидания завершения завершения работы одного потока - thread.Join.Вы можете присоединиться из потока пользовательского интерфейса к рабочему потоку; когда рабочий поток завершается, поток пользовательского интерфейса снова просыпается и выполняет удаление.

ОБНОВЛЕНИЕ: Добавлена ​​некоторая информация о присоединении.

«Присоединиться» в основном означает «поток U сообщает потоку W о завершении работы, а U переходит в спящий режим, пока это не произойдет». Краткий набросок метода выхода:

// do this in a thread-safe manner of your choosing
running = false; 
// wait for worker thread to come to a halt
workerThread.Join(); 
// Now we know that worker thread is done, so we can 
// clean up and exit
Dispose(); 
Exit();   

Предположим, вы по какой-то причине не захотели использовать «Соединение». (Возможно, рабочему потоку нужно продолжать работать, чтобы делать что-то еще, но вам все равно нужно знать, когда это будет сделано с помощью объектов.) Мы можем создать наш собственный механизм, который работает как Join, используя дескрипторы ожидания. Что вам теперь нужно, так это два механизма блокировки: один, который позволяет U послать сигнал W, который говорит «прекратить работу сейчас», а другой, который ждет , пока W завершает последний вызов M ().

В этой ситуации я бы сделал следующее:

  • сделал бы поточно-ориентированный флаг "работающим". Используйте любой удобный для вас механизм, чтобы сделать его потокобезопасным. Я бы лично начал с посвященного ему замка; если позже вы решите, что можете использовать на нем операции без блокировки, то вы всегда сможете сделать это позже.
  • создают AutoResetEvent для работы в качестве шлюза при удалении.

Итак, краткий набросок:

Поток пользовательского интерфейса, логика запуска:

running = true
waithandle = new AutoResetEvent(false)
start up worker thread

Поток пользовательского интерфейса, логика выхода:

running = false; // do this in a thread-safe manner of your choosing
waithandle.WaitOne(); 

// WaitOne is robust in the face of race conditions; if the worker thread
// calls Set *before* WaitOne is called, WaitOne will be a no-op.  (However,
// if there are *multiple* threads all trying to "wake up" a gate that is
// waiting on WaitOne, the multiple wakeups will be lost. WaitOne is named
// WaitOne because it WAITS for ONE wakeup. If you need to wait for multiple
// wakeups, don't use WaitOne.

Dispose();
waithandle.Close();
Exit();    

Рабочий поток:

while(running) // make thread-safe access to "running"
    M();
waithandle.Set(); // Tell waiting UI thread it is safe to dispose

Обратите внимание, что это зависит от того факта, что M () является коротким. Если M () занимает много времени, вы можете долго ждать, чтобы выйти из приложения, что кажется плохим.

Есть ли в этом смысл?

На самом деле, тебе не следует этого делать.Если вы хотите дождаться завершения рабочего потока, прежде чем удалять объект, который он использует, просто присоединитесь к нему.

ОБНОВЛЕНИЕ: Подняты некоторые дополнительные вопросы:

стоит ли ждать без тайм-аута?

В самом деле, обратите внимание, что в моем примере с Join и моем примере с WaitOne я не использую варианты на них которые ждут определенное время, прежде чем сдаться. Скорее,Я заявляю, что мое предположение состоит в том, что рабочий поток завершается чисто и быстро. Это правильно?

Это зависит от обстоятельств! Это зависит от того, насколько плохо ведет себя рабочий поток и что он делает, когда плохо себя ведет.

Если вы можете гарантировать, что работа будет непродолжительной, независимо от того, что «короткое» для вас означает, то вам не нужен тайм-аут. Если вы не можете этого гарантировать, я бы посоветовал сначала переписать код, чтобы вы могли гарантировать это; жизнь становится намного проще, если вы знаете, что код быстро завершится, когда вы его попросите.

Если не можете, что делать правильно? Предположение этого сценария состоит в том, что работник плохо себя ведет и не увольняется вовремя, когда его об этом просят. Итак, теперь мы должны спросить себя: «Является ли рабочий медленным по дизайну , глючным или враждебным ?»)

В первом сценарии рабочий просто делает что-то, что занимает много времени и по какой-либо причине не может быть прервано. Что здесь нужно делать? Не имею представления. Это ужасная ситуация. Предположительно рабочий не выключается быстро, потому что это опасно или невозможно. В таком случае, что вы собираетесь делать, когда истечет время ожидания ??? У вас есть что-то, что опасно или невозможно выключить, и что оно не выключается своевременно. Ваш выбор выглядит следующим образом: (1) ничего не делать, (2) сделать что-то опасное или (3) сделать что-то невозможное. Третий вариант, вероятно, отсутствует. Первый вариант равносилен вечному ожиданию, от которого мы уже отказались.Остается «сделать что-нибудь опасное».

Знание того, что делать правильно, чтобы минимизировать вред пользовательским данным, зависит от конкретных обстоятельств, которые вызывают опасность; тщательно проанализируйте его, разберитесь со всеми сценариями и определите, что нужно делать.

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

Если код враждебный и активно сопротивляется отключению , то вы уже проиграли. Вы не можете остановить поток обычными средствами и даже не можете его прервать. Нет никакой гарантии, что прерывание враждебного потока действительно завершит его; владелец враждебного кода, который вы по глупости запустили в своем процессе, может выполнять всю свою работу в блоке finally или другой ограниченной области, которая предотвращает исключения прерывания потока.

Лучшее, что можно сделать, - это вообще никогда не попадать в такую ​​ситуацию; если у вас есть код, который вы считаете враждебным, либо не запускайте его вообще, либо запускайте в отдельном процессе и завершайте процесс , а не поток , когда дела идут плохо .

Короче говоря, нет хорошего ответа на вопрос «что мне делать, если это займет слишком много времени?» Если это произойдет, вы окажетесь в ужасной ситуации, и на нее нет простого ответа. Лучше всего усердно работать, чтобы убедиться, что вы не вникнете в это вообще; запускайте только совместный, мягкий и безопасный код, который всегда отключается чисто и быстро, когда его об этом просят.

Что, если рабочий вызовет исключение?

Хорошо, а что, если это произойдет? Опять же, лучше вообще не попадать в эту ситуацию; напишите рабочий код, чтобы он не закидывал. Если вы не можете этого сделать, у вас есть два варианта: обрабатывать исключение или не обрабатывать исключение.

Предположим, вы не обрабатываете исключение. На мой взгляд, CLR v2, необработанное исключение в рабочем потоке завершает работу всего приложения. Причина в том, что в прошлом происходило то, что вы запускали кучу рабочих потоков, все они генерировали исключения, и в итоге вы получали бы работающее приложение, в котором не осталось рабочих потоков, не выполняющих никакой работы и не сообщая об этом пользователю. Лучше заставить автора кода обрабатывать ситуацию, когда рабочий поток выходит из строя из-за исключения; старый способ эффективно скрывает ошибки и упрощает написание уязвимых приложений.

Предположим, вы обрабатываете исключение.Что теперь? Что-то вызвало исключение, которое по определению является непредвиденной ошибкой. Теперь вы не имеете ни малейшего представления о том, что какие-либо из ваших данных согласованы или какие-либо инварианты вашей программы поддерживаются в любой из ваших подсистем. Итак, что ты собираешься делать? На данный момент вы вряд ли сможете сделать что-нибудь безопасное.

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

Или вполне возможно, что лучше всего будет приложить добросовестные усилия для сохранения данных пользователя, привести в порядок как можно больше состояния и завершить работу как можно более обычным образом.

По сути, оба ваших вопроса: «Что мне делать, если мои подсистемы не ведут себя должным образом?» Если ваши подсистемы ненадежны, либо сделайте их надежными , либо разработайте политику работы с ненадежной подсистемой и реализуйте эту политику . Я знаю это расплывчатый ответ, но дело в том, что работа с ненадежной подсистемой по своей сути является ужасной ситуацией. То, как вы с ней справляетесь, зависит от характера ее ненадежности и последствий этой ненадежности для ценных данных пользователя.

46
ответ дан 1 December 2019 в 06:12
поделиться

Проверьте Опять снова внутри блокировки:

while (Running)
{
    lock(locker)
    {
        if(Running) {
            DoSomething1();
            DoSomething2();
        }
    }
}

Вы можете даже переписать это как , пока (True) ... Break , что, вероятно, будет предпочтительным.

6
ответ дан 1 December 2019 в 06:12
поделиться

Вместо использования bool для Running, почему бы не использовать Enum со состояниями Stopped, Starting, Running и Stopping?

Таким образом, вы выходите из шлейфа, когда Running устанавливается на Stopping, и выполняете утилизацию. После этого Выполнение устанавливается на Остановка . Когда OnQuit() увидит, что Running установлено на Stopped, он выйдет вперед и выполнит удаление.

Правка: Вот код, быстрый и грязный, не протестированный и т.д.

public class SomeThread : IDisposable 
{ 
    private object locker = new object(); 
    private RunState running = RunState.Stopped;

    public enum RunState
    {
        Stopped,
        Starting,
        Running,
        Stopping,
    }

    public RunState Running  
    {  
        get 
        { 
            lock(locker) 
            { 
                return running; 
            } 
        } 
        set 
        { 
            lock(locker) 
            { 
                running = value; 
            } 
        } 
    } 

    public void Run() 
    { 
        while (Running == RunState.Running) 
        { 
            lock(locker) 
            { 
                DoSomething1(); 
                DoSomething2(); 
            } 
        } 

        Dispose();

    } 

    private void DoSomething1() 
    { 
        // something awesome happens here 
    } 

    private void DoSomething2() 
    { 
        // something more awesome happens here 
    } 

    public void Dispose() 
    { 
        lock (locker) 
        {    
            Dispose1(); 
            Dispose2(); 
        } 
        Running = RunState.Stopped;
    } 

    private void Dispose1() 
    { 
        // something awesome happens here 
    } 

    private void Dispose2() 
    { 
        // something more awesome happens here 
    } 

} 

public class OtherThread 
{ 
    SomeThread st = new SomeThread(); 

    public void OnQuit() 
    { 
        st.Running = SomeThread.RunState.Stopping; 
        while (st.Running == SomeThread.RunState.Stopping) 
        {                 
            // Do something while waiting for the other thread.
        }  

        Exit(); 
    } 
} 
0
ответ дан 1 December 2019 в 06:12
поделиться
Другие вопросы по тегам:

Похожие вопросы: