如何修复异步 WMI select/PerformanceCounter 上的 UI 死锁以获取远程计算机 LastBootUpTime

How to fix UI deadlock on async WMI select/PerformanceCounter for getting remote machine LastBootUpTime

我正在创建用于控制我们公司网络上的远程工作站和服务器的表单应用程序,我必须创建 "Wait on restart of remote machine" 函数。这个功能没问题,但我需要它 运行 asynchoniously,这是我的问题...... 该函数首先检查 online/offline 状态以确定重新启动,然后检查远程机器的新 LastBootUpTime 值以确保它确实重新启动而不仅仅是网络问题。 当我 运行 此异步检查时,ManagementObjectSearcher 在使用它的 .Get() 方法时触发死锁。当我改用 PerformanceCounter 时遇到同样的问题。

为此有 3 个主要对象: 1) 表格 class 2) Relation class (归Form所有) 3) RestartChecker class(由 Relation 拥有)

当 RestartChecker 获取重启已完成的信息时,通过事件将此信息发送到 Relation。关系使用它自己的事件将其发送到 UI 上的表单和表单更改图标。

这是我来自 RestartChecker 的代码(重要部分):

此方法在关系 class 中,它启动了 RestartChecker。此 Relation 方法是从 Form class.

调用的
    public void StartRestartMonitoring()
    {
        restartChecker = new RestartChecker(machine.Name, machine.OperatingSystem.lastBootUpTime.Value, wmiSuccess);

        //WasRestarted property calls event on value change to true. That event change icons on Form
        restartChecker.RestartWasMade += new Action(() => { WasRestarted = true; }); 

        restartChecker.Start();
    }

该方法启动检测重启功能

Task checker;
CancellationTokenSource tokenSource;   

    public void Start()
    {
        tokenSource = new CancellationTokenSource();
        CancellationToken token = tokenSource.Token;

        checker = CheckActionAsync(token);
        running = true;
    }

这是更重要的部分=>任务方法应该运行异步

    private async Task CheckActionAsync(CancellationToken ct)
    {
        bool isOnline = await RemoteTask.PingAsync(target, PING_TIMEOUT_SECONDS);
        int onlineState = (isOnline) ? 0 : 1;

        try
        {
            lastKnownBootUpTime = (isOnline) ? (GetLastBootUpTime(target, useWMI) ?? lastKnownBootUpTime) : lastKnownBootUpTime;
        }
        catch (Exception ex)
        {
            //Logs to File
            EventNotifier.Log(ex,....);
        }

        //This part looks OK...
            while (onlineState < 2)
            {
                if (ct.IsCancellationRequested) { return; }

                bool actualOnlineState = await RemoteTask.PingAsync(target, PING_TIMEOUT_SECONDS);
                onlineState += (actualOnlineState == isOnline) ? 0 : 1;

                await Task.Delay(CHECK_INTERVAL);
            }

        while (!ct.IsCancellationRequested)
        {
            if (ct.IsCancellationRequested) { return; }

            //Here, until I get properly value for LastBootUpTime of remote machine, I'm still trying again and again (beacause first try is cannot be OK => machine is Online, but services for WMI is not ready yet, so there is exception on first try)
            while (newBootUpTime == null)
            {
                try
                {
                    newBootUpTime = GetLastBootUpTime(target, useWMI);
                }
                catch (Exception ex)
                {
                    //Some reactions to exception including logging to File
                }

                await Task.Delay(INTERVAL);
            }

            //This part looks ok too..
            newBootUpTime = newBootUpTime.Value.AddTicks(-newBootUpTime.Value.Ticks % TimeSpan.TicksPerSecond);
            lastKnownBootUpTime = lastKnownBootUpTime.Value.AddTicks(-lastKnownBootUpTime.Value.Ticks % TimeSpan.TicksPerSecond);

            if (newBootUpTime.Value > lastKnownBootUpTime.Value)
            {
                RestartWasMade?.Invoke();
                return;
            }

            await Task.Delay(CHECK_INTERVAL);
        }
    }

GetLastBoostUpTime 方法

    private static DateTime? GetLastBootUpTime(string target, bool useWMI)
    {
        DateTime? lastBootUpTime = null;

        if (useWMI)
        {
            //wmiBootUpTime is SelectQuery
            string dateInString = RemoteTask.SelectStringsFromWMI(wmiBootUpTime, new ManagementScope(string.Format("\\{0}\root\cimv2", target))).First()[wmiBootUpTime.SelectedProperties[0].ToString()];
            lastBootUpTime = (string.IsNullOrEmpty(dateInString)) ? null : (DateTime?)ManagementDateTimeConverter.ToDateTime(dateInString);
        }
        else
        {
            TimeSpan? osRunningTime = RemoteTask.GetUpTime(target);
            lastBootUpTime = (osRunningTime == null) ? null : (DateTime?)DateTime.Now.Subtract(osRunningTime.Value);
        }

        return lastBootUpTime;
    }

用于获取数据的WMI方法:

    public static List<Dictionary<string, string>> SelectStringsFromWMI(SelectQuery select, ManagementScope wmiScope)
    {
        List<Dictionary<string, string>> result = new List<Dictionary<string, string>>();
        using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(wmiScope, select))
        {
            //This line is deadlock-maker... Because remote machine  services is not ready yet, searcher.Get() is trying
            //until reach it's timeout (by default it is 30s) and that's my deadlock. For the time of running searcher.Get()
            //there is 30s deadlock. Where is the mistake I've made? I supposed that this can not confront my UI thread
            using (ManagementObjectCollection objectCollection = searcher.Get())
            {
                foreach (ManagementObject managementObject in objectCollection)
                {
                    result.Add(new Dictionary<string, string>());
                    foreach (PropertyData property in managementObject.Properties)
                    {
                        result.Last().Add(property.Name, property.Value?.ToString());
                    }
                }

                return result;
            }
        }
    }

用于获取数据的 PerformanceCounte 方法:

    public static TimeSpan? GetUpTime(string remoteMachine = null)
    {
        try
        {
            using (PerformanceCounter upTime = (string.IsNullOrWhiteSpace(remoteMachine))
                ? new PerformanceCounter("System", "System Up Time")
                : new PerformanceCounter("System", "System Up Time", null, remoteMachine))
            {
                upTime.NextValue();
                return TimeSpan.FromSeconds(upTime.NextValue());
            }
        }
        catch
        {
            return null;
        }
    }

异步ping方法

    public async static Task<bool> PingAsync(string target, int pingTimeOut)
    {
        bool result = false;
        Exception error = null;

        using (Ping pinger = new Ping())
        {
            try
            {
                PingReply replay = await pinger.SendPingAsync(target, pingTimeOut * 1000);
                result = (replay.Status == IPStatus.Success) ? true : false;
            }
            catch (Exception ex)
            {
                error = ex;
            }
        }

        if (error != null) { throw error; }

        return result;
    }

我在这里没有看到死锁,但我看到你用同步调用阻止了异步方法

newBootUpTime = GetLastBootUpTime(target, useWMI);

我认为你应该在单独的线程中异步调用它,或者使 GetLastBootUpTime 方法异步

newBootUpTime = await Task.Run(() => GetLastBootUpTime(target, useWMI));

您还应该使用上述方法从您的异步方法中删除所有其他同步阻塞调用..

只有调用

才可能导致死锁

checker.Wait(); 在您创建 Task checker 的线程中的某处(可能是 UI 线程)

你在做这个吗?

您还可以在此处了解什么是死锁以及如何避免死锁

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html