Суть проблемы (в чём баг)
При закрытии плеера на ОС Windows 10 (через крестик главного окна или контекстное меню) приложение визуально исчезает с экрана, иконка в трее пропадает, но процесс LMP.exe не завершается.
В диспетчере задач процесс переходит в состояние «призрака»:
- Потребление процессора падает практически до нуля (~0.1% CPU).
- Аномалия: Если в момент закрытия играл трек, музыка может продолжать воспроизводиться ещё некоторое время (или бесконечно), несмотря на то, что графическая оболочка плеера полностью уничтожена.
- Процесс невозможно завершить стандартными средствами ОС, кроме принудительного снятия через диспетчер задач (
Taskkill).
Такое поведение указывает на классический Deadlock (взаимную блокировку) нативного аудио-драйвера Windows (winmm.dll / COM-сервисов) и фонового потока заполнения буфера при некорректной последовательности уничтожения объектов (Shutdown Sequence).
Как воспроизвести (Steps to Reproduce)
- Запустить плеер на Windows 10.
- Включить любой онлайн-трек (чтобы пошла активная буферизация и работа фонового потока
AudioFillBuffer).
- Нажать кнопку закрытия окна плеера.
- Интерфейс закрывается, иконка из трея пропадает.
- Открыть Диспетчер задач Windows -> Найти процесс
LMP.exe (он продолжает висеть в фоновых процессах с загрузкой CPU ~0.1%, звук может продолжать играть).
Возможное техническое решение (теория и архитектура)
Проблема вызвана двумя взаимосвязанными архитектурными ошибками:
Ошибка А: async void в обработчике завершения (App.axaml.cs)
desktop.ShutdownRequested += async (_, _) => { ... };
Событие ShutdownRequested в Avalonia возвращает void. Использование async void лямбды приводит к тому, что Avalonia уничтожает графическое окно, закрывает цикл сообщений (Message Loop) и даёт ОС сигнал на закрытие процесса до того, как завершатся асинхронные вызовы DisposeAsync внутри лямбды. В итоге AudioEngine и AudioPlayer вообще не успевают финализироваться.
Ошибка Б: Дедлок при финализации WaveOutEvent в NAudio
Если процесс начинает аварийно завершаться на уровне CLR, срабатывают деструкторы (Finalizers). Нативный драйвер winmm.dll пытается освободить буферы WaveOutEvent.
В этот момент наш фоновый поток AudioFillBuffer в NAudioBackend.cs может стоять на блокировке lock-объекта _stateLock или быть заблокированным внутри _provider.AddSamples() (который ждёт освобождения ресурсов драйвером). Возникает перекрёстный дедлок:
- Поток UI/Finalizer ждёт завершения
_fillThread.Join().
- Поток
_fillThread заблокирован в нативном вызове, ожидающем ответа от аудио-драйвера, который заблокирован потоком UI.
Шаги к решению:
- Сделать Shutdown синхронным и гарантированным:
В App.axaml.cs в обработчике ShutdownRequested отказаться от async void. Нам нужно принудительно и строго синхронно (блокируя поток закрытия на долю секунды) вызвать Dispose на всей цепочке:
AudioEngine.Dispose() -> AudioPlayer.Dispose() -> NAudioBackend.Dispose().
- Безопасный выход из цикла заполнения (NAudioBackend.cs):
В методе Dispose() бэкенда сначала вызывать деактивацию и отмену токена _cts.Cancel(), будить поток через _fillWakeup.Set(), дожидаться гарантированного завершения _fillThread.Join() и только после этого вызывать _waveOut.Stop() и _waveOut.Dispose().
Это разорвёт петлю дедлока: на момент уничтожения аудио-устройства наш фоновый поток гарантированно прекратит писать данные в буферы.
Окружение (Environment)
- Версия LMP: 220+
- Версия ОС: Windows 10 (Build 19045+)
- Остальные настройки вроде не влияют.
Суть проблемы (в чём баг)
При закрытии плеера на ОС Windows 10 (через крестик главного окна или контекстное меню) приложение визуально исчезает с экрана, иконка в трее пропадает, но процесс
LMP.exeне завершается.В диспетчере задач процесс переходит в состояние «призрака»:
Taskkill).Такое поведение указывает на классический Deadlock (взаимную блокировку) нативного аудио-драйвера Windows (
winmm.dll/ COM-сервисов) и фонового потока заполнения буфера при некорректной последовательности уничтожения объектов (Shutdown Sequence).Как воспроизвести (Steps to Reproduce)
AudioFillBuffer).LMP.exe(он продолжает висеть в фоновых процессах с загрузкой CPU ~0.1%, звук может продолжать играть).Возможное техническое решение (теория и архитектура)
Проблема вызвана двумя взаимосвязанными архитектурными ошибками:
Ошибка А: async void в обработчике завершения (App.axaml.cs)
Событие
ShutdownRequestedв Avalonia возвращаетvoid. Использованиеasync voidлямбды приводит к тому, что Avalonia уничтожает графическое окно, закрывает цикл сообщений (Message Loop) и даёт ОС сигнал на закрытие процесса до того, как завершатся асинхронные вызовыDisposeAsyncвнутри лямбды. В итогеAudioEngineиAudioPlayerвообще не успевают финализироваться.Ошибка Б: Дедлок при финализации WaveOutEvent в NAudio
Если процесс начинает аварийно завершаться на уровне CLR, срабатывают деструкторы (Finalizers). Нативный драйвер
winmm.dllпытается освободить буферыWaveOutEvent.В этот момент наш фоновый поток
AudioFillBufferвNAudioBackend.csможет стоять на блокировке lock-объекта_stateLockили быть заблокированным внутри_provider.AddSamples()(который ждёт освобождения ресурсов драйвером). Возникает перекрёстный дедлок:_fillThread.Join()._fillThreadзаблокирован в нативном вызове, ожидающем ответа от аудио-драйвера, который заблокирован потоком UI.Шаги к решению:
В
App.axaml.csв обработчикеShutdownRequestedотказаться отasync void. Нам нужно принудительно и строго синхронно (блокируя поток закрытия на долю секунды) вызватьDisposeна всей цепочке:AudioEngine.Dispose()->AudioPlayer.Dispose()->NAudioBackend.Dispose().В методе
Dispose()бэкенда сначала вызывать деактивацию и отмену токена_cts.Cancel(), будить поток через_fillWakeup.Set(), дожидаться гарантированного завершения_fillThread.Join()и только после этого вызывать_waveOut.Stop()и_waveOut.Dispose().Это разорвёт петлю дедлока: на момент уничтожения аудио-устройства наш фоновый поток гарантированно прекратит писать данные в буферы.
Окружение (Environment)