Как избежать гонки на очистке RCW

У меня есть приложение графического интерфейса, которое периодически показывает загрузку процессора. Загрузка читается классом StateReader:

public class StateReader
{
    ManagementObjectSearcher searcher;

    public StateReader()
    {
        ManagementScope scope = new ManagementScope("\\\\localhost\\root\\cimv2");
        ObjectQuery query = new ObjectQuery("select Name,PercentProcessorTime from Win32_PerfFormattedData_PerfOS_Processor where not Name='_Total'");
        searcher = new ManagementObjectSearcher(scope, query);
    }

    // give the maximum load over all cores
    public UInt64 CPULoad()
    {
        List<UInt64> list = new List<UInt64>();
        ManagementObjectCollection results = searcher.Get();
        foreach (ManagementObject result in results)
        {
            list.Add((UInt64)result.Properties["PercentProcessorTime"].Value); 
        }
        return list.Max();
    }
}

GUI обновляется с использованием реактивных расширений:

var gui = new GUI();
var reader = new StateReader();

var sub = Observable.Interval(TimeSpan.FromSeconds(0.5))
                    .Select(_ => reader.CPULoad())
                    .ObserveOn(gui)
                    .Subscribe(gui.ShowCPUState);

Application.Run(gui);
sub.Dispose();

Теперь при выходе из приложения появляется сообщение об ошибке

RaceOnRCWCleanup was detected. 
An attempt has been mad to free an RCW that is in use. The RCW is use on the 
active thread or another thread. Attempting to free an in-use RCW can cause 
corruption or data loss.

Эта ошибка не появляется, если я не читаю загрузку процессора, а просто предоставляю какое-то случайное значение, поэтому ошибка каким-то образом связана с чтением загрузки. Также, если я поставлю точку останова после Application.Run(gui) и подождите немного, ошибка, кажется, не так часто.

Исходя из этого и из моего поиска в Google, я думаю, что использование классов в пространстве имен управления создает фоновый поток, который ссылается на COM-объект, обернутый в Runtime Callable Wrapper, и когда я выхожу из приложения, этот поток не успевает правильно закрыть RCW, что привело к моей ошибке. Это правильно, и как я могу решить эту проблему?


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

  • StateReader является Disposable, удаляет его ManagementObjectSearcher в методе Dispose, и я вызываю Dispose для объекта StateReader после Application.Run в моем основном методе
  • В CPULoad я располагаю коллекцией ManagementCollection и каждым объектом ManagementObject.
  • В моем основном методе я избавляюсь от объекта подписки в обработчике событий в FormClosing
    в графическом интерфейсе Это должно гарантировать, что никакие события не генерируются для графического интерфейса после его закрытия.

Соответствующие части кода теперь в StateReader:

// give the maximum load over all cores
public UInt64 CPULoad()
{
    List<UInt64> list = new List<UInt64>();
    using (ManagementObjectCollection results = searcher.Get())
    {
        foreach (ManagementObject result in results)
        {
            list.Add((UInt64)result.Properties["PercentProcessorTime"].Value); 
            result.Dispose();
        }
    }
    return list.Max();
}

public void Dispose()
{
    searcher.Dispose();
}

И по моему главное:

gui.FormClosing += (a1, a2) => sub.Dispose();

Application.Run(gui);
reader.Dispose();

Могу ли я сделать что-нибудь еще, чтобы избежать ошибки, которую я получил?

2 ответа

Я думаю, вам нужно сделать StateReader одноразовые и утилизируйте их перед выходом из приложения. StateReader должен распоряжаться searcher, Тем не менее, я думаю, что реальная проблема в том, что вы не избавляетесь ManagementObject в CPULoad, Если GC работает после CPULoad RCW будут освобождены. Однако, если вы выйдете до GC, это может вызвать исключение, которое вы видите.

Я думаю, что использование классов в пространстве имен управления создает фоновый поток, который ссылается на COM-объект, завернутый в Runtime Callable Wrapper

Observable.Interval создает фоновый поток и CPULoad выполняется в этом потоке.

Не позволяйте приложению закрываться, пока работает фоновый поток CPULoad чтобы избежать этого.

Самое простое решение, которое я могу придумать, - это получить нагрузку на процессор из нового потока переднего плана, а затем присоединиться к потокам.

public UInt64 CPULoad()
{
    List<UInt64> list = new List<UInt64>();
    Thread thread = new Thread(() =>
    {
        ManagementObjectCollection results = searcher.Get();
        foreach (ManagementObject result in results)
        {
            list.Add((UInt64)result.Properties["PercentProcessorTime"].Value);
        }
    });
    thread.Start();
    thread.Join();
    return list.Max();
}

Затраты на запуск нового потока каждый раз незначительны по сравнению с медленными вызовами WMI.

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

Другие вопросы по тегам