C# AutoResetEvent 未释放

C# AutoResetEvent not releasing

使用 Xamarin.Forms(对于 iOS)我尝试实现等待用户确认已设置 GeoLocation 权限的功能,然后再继续。

我尝试实现此目的的方法是让线程等待,直到使用 AutoResetEvent.

触发事件

主要问题(我相信)位于以下代码中:

manager.AuthorizationChanged += (object sender, CLAuthorizationChangedEventArgs args) => {

    Console.WriteLine ("Authorization changed to: {0}", args.Status);

    if (UIDevice.CurrentDevice.CheckSystemVersion (8, 0)) {
        tcs.SetResult (args.Status == CLAuthorizationStatus.AuthorizedAlways || args.Status == CLAuthorizationStatus.AuthorizedWhenInUse);
    } else {
        tcs.SetResult (args.Status == CLAuthorizationStatus.Authorized);
    }

    _waitHandle.Set ();
};

manager.Failed += (object sender, Foundation.NSErrorEventArgs e) => {

    Console.WriteLine ("Authorization failed");

    tcs.SetResult (false);

    _waitHandle.Set ();
};

if (UIDevice.CurrentDevice.CheckSystemVersion (8, 0)) {
    manager.RequestWhenInUseAuthorization ();
}

_waitHandle.WaitOne ();

您可以在下面找到完整的 class:

public class LocationManager : ILocationManager
{
    static EventWaitHandle _waitHandle = new AutoResetEvent (false);

    private TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

    public LocationManager ()
    {
    }

    public Task<bool> IsGeolocationEnabledAsync()
    {
        Console.WriteLine (String.Format("Avaible on device: {0}", CLLocationManager.LocationServicesEnabled));
        Console.WriteLine (String.Format("Permission on device: {0}", CLLocationManager.Status));

        if (!CLLocationManager.LocationServicesEnabled) {
            tcs.SetResult (false);
        } else if (CLLocationManager.Status == CLAuthorizationStatus.Denied || CLLocationManager.Status == CLAuthorizationStatus.Restricted) {
            tcs.SetResult (false);
        } else if (CLLocationManager.Status == CLAuthorizationStatus.NotDetermined) {

            Console.WriteLine ("Waiting for authorisation");

            CLLocationManager manager = new CLLocationManager ();

            manager.AuthorizationChanged += (object sender, CLAuthorizationChangedEventArgs args) => {

                Console.WriteLine ("Authorization changed to: {0}", args.Status);

                if (UIDevice.CurrentDevice.CheckSystemVersion (8, 0)) {
                    tcs.SetResult (args.Status == CLAuthorizationStatus.AuthorizedAlways || args.Status == CLAuthorizationStatus.AuthorizedWhenInUse);
                } else {
                    tcs.SetResult (args.Status == CLAuthorizationStatus.Authorized);
                }

                _waitHandle.Set ();
            };

            manager.Failed += (object sender, Foundation.NSErrorEventArgs e) => {

                Console.WriteLine ("Authorization failed");

                tcs.SetResult (false);

                _waitHandle.Set ();
            };

            if (UIDevice.CurrentDevice.CheckSystemVersion (8, 0)) {
                manager.RequestWhenInUseAuthorization ();
            }

            _waitHandle.WaitOne ();

            Console.WriteLine (String.Format ("Auth complete: {0}", tcs.Task.Result));

        } else {
            if (UIDevice.CurrentDevice.CheckSystemVersion (8, 0)) {
                tcs.SetResult (CLLocationManager.Status == CLAuthorizationStatus.AuthorizedAlways || CLLocationManager.Status == CLAuthorizationStatus.AuthorizedWhenInUse);
            } else {
                tcs.SetResult (CLLocationManager.Status == CLAuthorizationStatus.Authorized);
            }
        }

        return tcs.Task;
    }
}

它工作正常,除了我无法弄清楚为什么 manager.AuthorizationChangedmanager.Failed 事件似乎永远不会被触发,因此当状态未确定时线程永远不会释放。

非常感谢任何帮助或指点。

如果没有 a good, minimal, complete code example 可靠地重现问题,就不可能确定问题出在哪里。但是您的代码肯定存在明显的设计缺陷,我希望通过解决该缺陷来解决您的问题。

有什么缺陷?您正在等待 任何东西。您编写了一个显然应该表示异步操作的方法 — 它的名称中包含 "Async" 并且 return 是一个 Task<bool> 而不是 bool — 然后您编写了returned 的 Task<bool> 将始终完成的方法,无论采用哪种代码路径。

为什么这么糟糕?好吧,除了它完全无法利用您正在实现的接口的异步方面这一简单事实之外,您正在使用的 CLLocationManager class 很可能期望能够运行 在调用 IsGeolocationEnabledAsync() 方法的同一线程中。由于此方法在引发事件之前不会 returning,并且由于在方法 return 之前无法引发事件,因此您遇到了死锁。

恕我直言,这就是您的 class 的实施方式:

public class LocationManager : ILocationManager
{
    public async Task<bool> IsGeolocationEnabledAsync()
    {
        bool result;

        Console.WriteLine (String.Format("Avaible on device: {0}", CLLocationManager.LocationServicesEnabled));
        Console.WriteLine (String.Format("Permission on device: {0}", CLLocationManager.Status));

        if (!CLLocationManager.LocationServicesEnabled) {
            result = false;
        } else if (CLLocationManager.Status == CLAuthorizationStatus.Denied || CLLocationManager.Status == CLAuthorizationStatus.Restricted) {
            result = false;
        } else if (CLLocationManager.Status == CLAuthorizationStatus.NotDetermined) {
            TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

            Console.WriteLine ("Waiting for authorisation");

            CLLocationManager manager = new CLLocationManager ();

            manager.AuthorizationChanged += (object sender, CLAuthorizationChangedEventArgs args) => {

                Console.WriteLine ("Authorization changed to: {0}", args.Status);

                if (UIDevice.CurrentDevice.CheckSystemVersion (8, 0)) {
                    tcs.SetResult (args.Status == CLAuthorizationStatus.AuthorizedAlways || args.Status == CLAuthorizationStatus.AuthorizedWhenInUse);
                } else {
                    tcs.SetResult (args.Status == CLAuthorizationStatus.Authorized);
                }
            };

            manager.Failed += (object sender, Foundation.NSErrorEventArgs e) => {

                Console.WriteLine ("Authorization failed");

                tcs.SetResult (false);
            };

            if (UIDevice.CurrentDevice.CheckSystemVersion (8, 0)) {
                manager.RequestWhenInUseAuthorization ();
                result = await tcs.Task;
            } else {
                result = false;
            }

            Console.WriteLine (String.Format ("Auth complete: {0}", tcs.Task.Result));

        } else {
            if (UIDevice.CurrentDevice.CheckSystemVersion (8, 0)) {
                result = CLLocationManager.Status == CLAuthorizationStatus.AuthorizedAlways || CLLocationManager.Status == CLAuthorizationStatus.AuthorizedWhenInUse;
            } else {
                result = CLLocationManager.Status == CLAuthorizationStatus.Authorized;
            }
        }

        return result;
    }
}

即将方法变成 async 方法,根本不要理会 TaskCompletionSource 除非你真的需要等待任何东西,然后 await [=14= 的结果]的异步操作,return计算其结果。

这将允许该方法立即 return 即使在您调用 CLLocationManager.RequestWhenInUseAuthorization() 的情况下,也不会更改调用者的语义(即它仍然看到 Task<bool> return 值,可以 await 结果)。如果该方法同步完成,则调用者实际上不必等待。如果没有,则假设调用者已被正确编写并且它本身没有阻塞等待结果的线程,操作将能够正常完成,设置完成源的结果并让等待它的代码继续.

备注:

  • 以上内容假设您的平台具有 async/await 功能。如果你不这样做,调整适应就足够简单了;它基本上与上面的技术相同,只是您实际上会对方法中的所有分支使用 TaskCompletionSource 而不仅仅是异步分支,然后 return tcs.Task价值就像你以前一样。请注意,在这种情况下,如果您希望最后一个 Console.WriteLine() 以正确的顺序执行,即在操作实际完成之后,则必须在您的方法中显式调用 ContinueWith()
  • 您的代码也有可能存在的错误。也就是说,如果系统版本符合您的预期,您只会有条件地调用 RequestWhenInUseAuthorization()。此 可能导致您尝试修复的行为,假设 RequestWhenInUseAuthorization() 方法最终导致引发这些事件中的任何一个。如果您不调用该方法,那么显然不会引发任何事件,并且您的代码将永远等待。我将 await 移动到与方法调用本身相同的 if 子句中,并简单地将结果设置为 else 子句中的 false。我在这里没有足够的上下文来确定所有这些应该如何工作;我假设您对正在使用的 API 足够熟悉,如果我的假设不正确,您可以解决任何剩余的细节问题。
  • 最后,我想强调的是,假设您的问题实际上是阻止当前线程阻止您尝试完成的异步操作,那么从此方法中删除等待是必要的,但还不够.您必须确保调用链中没有任何东西也在等待 returned Task<bool> 或以其他方式阻塞线程。我在上面提到了这一点,但是确保它非常关键,而且你没有提供完整的代码示例所以无法确保是这种情况,所以我想确保这一点很重要的一点不容忽视。