简体   繁体   中英

C# AutoResetEvent not releasing

Using Xamarin.Forms (for iOS) I try to implement the functionality to wait for the user confirmation for the GeoLocation permission to have been set before continuing.

The way I try to achieve this is by making the Thread wait until an event is fired using the AutoResetEvent .

The main problem (I believe) is located in the following code:

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 ();

You can find the complete class below:

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;
    }
}

It works fine except that I can't figure out why the manager.AuthorizationChanged or manager.Failed event seems to never get fired and thus the Thread is never release when the Status is Undetermined.

Any help or pointers are greatly appreciated.

Without a good, minimal , complete code example that reliably reproduces the problem, it's impossible to know for sure what the problem is. But your code certainly has a glaring design defect in it, and it's my hope that addressing that defect will resolve your problem.

What's the defect? That you are waiting on anything at all. You wrote an method that apparently is supposed to represent an asynchronous operation — it has "Async" in the name and returns a Task<bool> instead of a bool — and then you write the method in such a way that the Task<bool> that is returned will always be completed, regardless of which code path is taken.

Why is this so bad? Well, besides the simple fact that it completely fails to take advantage of the asynchronous aspect of the interface you're implementing, it is quite likely that the CLLocationManager class you're using expects to be able to run in the same thread in which your IsGeolocationEnabledAsync() method is called. Since this method isn't returning until the events are raised and since the events can't be raised until the method returns, you have a deadlock.

IMHO, this is how your class should be implemented:

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;
    }
}

Ie turn the method into an async method, don't bother with the TaskCompletionSource at all unless you will actually need to wait on anything, and then await the result of the CLLocationManager 's asynchronous operation, returning its result.

This will allow the method to return immediately even in the case when you are calling CLLocationManager.RequestWhenInUseAuthorization() , without changing the semantics for the caller (ie it still sees a Task<bool> return value and can await the result). If the method completes synchronously, the caller won't have to actually wait. If it doesn't, then assuming the caller has been written correctly and it itself is not blocking the thread waiting for the result, the operation will be able to complete normally, setting the completion source's result and letting the code that's waiting on it proceed.

Notes:

  • The above assumes that in your platform you have available the async / await feature. If you do not, it is simple enough to adjust to accommodate; it's basically the same technique as above, except that you would in fact use the TaskCompletionSource for all of the branches in the method instead of just the asynchronous one, and then would return the tcs.Task value just like you were before. Note that in that case, you would have to call ContinueWith() explicitly in your method if you want that last Console.WriteLine() to execute in the correct order, ie after the operation actually completes.
  • Your code also had what appears to be a possible bug. That is, you only call RequestWhenInUseAuthorization() conditionally, if the system version is as you expect. This also could have caused the behavior you are trying to fix, assuming the RequestWhenInUseAuthorization() method is what eventually causes either of those events to be raised. If you don't call the method, then obviously neither event would be raised, and your code would be waiting forever. I moved the await into the same if clause with the method call itself, and simply set the result to false in the else clause. I don't have enough context here to know for sure how all that should work; I'm assuming that you have enough familiarity with the API you're using that you can resolve any remaining fine details like that, should my assumptions not have been correct.
  • Finally, I want to emphasize that, assuming your problem is in fact that blocking the current thread is what prevents the asynchronous operation your attempting from being able to complete, that removing the wait from this method is necessary but not sufficient. You have to make certain nothing in the call chain is waiting on the returned Task<bool> as well or otherwise blocking the thread. I mentioned this above, but it's so critical to ensure, and you didn't provide a complete code example so there's no way I can ensure it's the case, so I want to make sure this very important point is not overlooked.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM