C # - проблемы с блокировкой с Mutex - PullRequest
3 голосов
/ 16 января 2009

У меня есть веб-приложение, которое контролирует, какие веб-приложения получают трафик от нашего балансировщика нагрузки. Веб-приложение работает на каждом отдельном сервере.

Он отслеживает состояние «включено или выключено» для каждого приложения в объекте в состоянии приложения ASP.NET, и объект сериализуется в файл на диске при каждом изменении состояния. Состояние десериализуется из файла при запуске веб-приложения.

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

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

Тем не менее код, который я использую для контроля доступа к файлу, выглядит следующим образом:

internal static Mutex _lock = null;
/// <summary>Executes the specified <see cref="Func{FileStream, Object}" /> delegate on 
/// the filesystem copy of the <see cref="ServerState" />.
/// The work done on the file is wrapped in a lock statement to ensure there are no 
/// locking collisions caused by attempting to save and load the file simultaneously 
/// from separate requests.
/// </summary>
/// <param name="action">The logic to be executed on the 
/// <see cref="ServerState" /> file.</param>
/// <returns>An object containing any result data returned by <param name="func" />. 
///</returns>
private static Boolean InvokeOnFile(Func<FileStream, Object> func, out Object result)
{
    var l = new Logger();
    if (ServerState._lock.WaitOne(1500, false))
    {
        l.LogInformation( "Got lock to read/write file-based server state."
                        , (Int32)VipEvent.GotStateLock);
        var fileStream = File.Open( ServerState.PATH, FileMode.OpenOrCreate 
                                  , FileAccess.ReadWrite, FileShare.None);                
        result = func.Invoke(fileStream);                
        fileStream.Close();
        fileStream.Dispose();
        fileStream = null;
        ServerState._lock.ReleaseMutex();
        l.LogInformation( "Released state file lock."
                        , (Int32)VipEvent.ReleasedStateLock);
        return true;
    }
    else
    {
        l.LogWarning( "Could not get a lock to access the file-based server state."
                    , (Int32)VipEvent.CouldNotGetStateLock);
        result = null;
        return false;
    }
}

Этот обычно работает, но иногда я не могу получить доступ к мьютексу (я вижу событие "Не удалось получить блокировку" в журнале). Я не могу воспроизвести это локально - это происходит только на моих производственных серверах (Win Server 2k3 / IIS 6). Если я удаляю тайм-аут, приложение зависает на неопределенное время (состояние гонки ??), в том числе при последующих запросах.

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

Мьютекс создается в событии Application_Start. Я получаю те же результаты, когда он статически создается в объявлении.

Извините, извините: многопоточность / блокировка - это не моя удача, так как мне, как правило, об этом не нужно беспокоиться.

Какие-либо предложения относительно того, почему он случайно не сможет получить сигнал?


Обновление:

Я добавил правильную обработку ошибок (как неловко!), Но я все еще получаю те же ошибки - и для записи, необработанные исключения никогда не были проблемой.

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

@ mmr: Чем отличается использование Monitor от Mutex? Основываясь на документации MSDN, похоже, что он фактически делает то же самое - если и я не могу получить блокировку с помощью моего Mutex, он действительно терпит неудачу, просто возвращая false.

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


Обновление 2:

Эта блокировка не используется для любых других вызовов. Единственный раз, когда на _lock ссылаются вне метода InvokeOnFile, - это когда он создается.

Вызываемый Func либо читает файл и десериализует его в объект, либо сериализует объект и записывает его в файл. Ни одна операция не выполняется в отдельном потоке.

ServerState.PATH - это статическое поле только для чтения, которое, как я ожидаю, не вызовет проблем с параллелизмом.

Я также хотел бы повторить мою прежнюю мысль о том, что я не могу воспроизвести это локально (в Кассини).


Извлеченные уроки:

  • Используйте правильную обработку ошибок (дух!)
  • Используйте правильный инструмент для работы (и имейте общее представление о том, что / как этот инструмент делает). Как указывает самбо, использование Mutex, по-видимому, связано с большими накладными расходами, что вызывало проблемы в моем приложении, тогда как Monitor разработан специально для .NET.

Ответы [ 3 ]

15 голосов
/ 16 января 2009

Вы должны использовать мьютексы, только если вам нужна межпроцессная синхронизация .

Хотя мьютекс может использоваться для внутрипроцессная синхронизация потоков, использование монитора обычно предпочтительнее, потому что мониторы были разработаны специально для .NET Framework и, следовательно, лучше использовать Ресурсы. В отличие от Mutex класс является оберткой для Win32 построить. Хотя это мощнее чем монитор, мьютекс требует переходы взаимодействия, которые являются более вычислительно дороже, чем те требуется классом Monitor.

Если вам нужна поддержка межпроцессного блокирования, вам необходим Global mutex .

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

Кроме того, если ваша файловая операция когда-либо длится дольше 1,5 секунд, есть вероятность, что параллельные мьютексы не смогут ее захватить. Я бы порекомендовал получить правильную блокировку и избежать тайм-аута.

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

Это и короче, и намного безопаснее:

// if you want timeout support use 
// try{var success=Monitor.TryEnter(m_syncObj, 2000);}
// finally{Monitor.Exit(m_syncObj)}
lock(m_syncObj)
{
    l.LogInformation( "Got lock to read/write file-based server state."
                    , (Int32)VipEvent.GotStateLock);
    using (var fileStream = File.Open( ServerState.PATH, FileMode.OpenOrCreate
                                     , FileAccess.ReadWrite, FileShare.None))
    {
        // the line below is risky, what will happen if the call to invoke
        // never returns? 
        result = func.Invoke(fileStream);
    }
}

l.LogInformation("Released state file lock.", (Int32)VipEvent.ReleasedStateLock);
return true;

// note exceptions may leak out of this method. either handle them here.
// or in the calling method. 
// For example the file access may fail of func.Invoke may fail
2 голосов
/ 16 января 2009

Если некоторые файловые операции завершатся неудачно, блокировка не будет снята. Скорее всего, это так. Поместите файловые операции в блок try / catch и снимите блокировку в блоке finally.

В любом случае, если вы прочитаете файл в своем методе Global.asax Application_Start, это будет гарантировать, что никто не будет работать над ним (вы сказали, что файл читается при запуске приложения, верно?). Чтобы избежать коллизий при перезапуске пула приложений и т. Д., Вы можете просто попытаться прочитать файл (при условии, что операция записи принимает монопольную блокировку), а затем подождать 1 секунду и повторить попытку при возникновении исключения.

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

0 голосов
/ 16 января 2009

Я вижу здесь несколько потенциальных проблем.

Редактировать для обновления 2: Если функция представляет собой простую комбинацию сериализации / десериализации, я бы разделил две функции на две разные: одну в функцию «сериализации», а другую в функцию «десериализации». Это действительно две разные задачи. После этого вы можете выполнять различные задачи, связанные с блокировкой. Invoke - отличный способ, но я сам столкнулся с большими трудностями, пытаясь "изящный" вместо "работать".

1) Блокируется ли ваша функция LogInformation? Потому что сначала вы вызываете его внутри мьютекса, а затем, как только вы отпускаете мьютекс. Так что, если есть блокировка для записи в файл / структуру журнала, тогда вы можете в итоге получить состояние вашей гонки. Чтобы избежать этого, поместите журнал в замок.

2) Проверьте с помощью класса Monitor, который, как я знаю, работает в C #, и я предполагаю, что он работает в ASP.NET. Для этого вы можете просто попытаться получить блокировку и в противном случае потерпеть неудачу. Один из способов использовать это - просто продолжать пытаться получить блокировку. (Отредактируйте почему: см. здесь ; по сути, мьютекс находится между процессами, монитор находится в одном процессе, но был разработан для .NET и поэтому предпочтителен. Никаких других реальных объяснений в документации .)

3) Что произойдет, если открытие потока файлов не удастся, потому что кто-то еще имеет блокировку? Это вызовет исключение, и это может привести к тому, что этот код будет вести себя плохо (т. Е. Блокировка все еще удерживается потоком, в котором есть исключение, и другой поток может получить его).

4) А как насчет самого функционала? Это запускает другой поток, или это полностью внутри одного потока? Как насчет доступа к ServerState.PATH?

5) Какие другие функции могут получить доступ к ServerState._lock? Я предпочитаю, чтобы каждая функция, требующая блокировки, получала свою собственную блокировку, чтобы избежать условий гонки / тупика. Если у вас много потоков, и каждый из них пытается заблокировать один и тот же объект, но для совершенно разных задач, вы можете получить тупики и гонки без какой-либо действительно понятной причины. Я изменил код, чтобы отразить эту идею, вместо того, чтобы использовать глобальную блокировку. (Я понимаю, что другие люди предлагают глобальную блокировку; мне действительно не нравится эта идея, потому что другие вещи могут ее захватить для какой-то задачи, которая не является этой задачей).

    Object MyLock = new Object();
    private static Boolean InvokeOnFile(Func<FileStream, Object> func, out Object result)
{
    var l = null;
    var filestream = null;
    Boolean success = false;
    if (Monitor.TryEnter(MyLock, 1500))
        try {
            l = new Logger();
            l.LogInformation("Got lock to read/write file-based server state.", (Int32)VipEvent.GotStateLock);
            using (fileStream = File.Open(ServerState.PATH, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)){                
                result = func.Invoke(fileStream); 
            }    //'using' means avoiding the dispose/close requirements
            success = true;
         }
         catch {//your filestream access failed

            l.LogInformation("File access failed.", (Int32)VipEvent.ReleasedStateLock);
         } finally {
            l.LogInformation("About to released state file lock.", (Int32)VipEvent.ReleasedStateLock);
            Monitor.Exit(MyLock);//gets you out of the lock you've got
        }
    } else {
         result = null;
         //l.LogWarning("Could not get a lock to access the file-based server state.", (Int32)VipEvent.CouldNotGetStateLock);//if the lock doesn't show in the log, then it wasn't gotten; again, if your logger is locking, then you could have some issues here
    }
  return Success;
}
...