Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
128 views
in Technique[技术] by (71.8m points)

ios - Is the following a safe use of dispatch_set_target_queue()?

What I want to do is create an indirect queue targeting the main queue.

dispatch_queue_t myQueue = dispatch_queue_create("com.mydomain.my-main-queue", NULL);
dispatch_set_target_queue(myQueue, dispatch_get_main_queue());

My ultimate goal is to use the queue as the underlyingQueue property of an NSOperationQueue, because Apple's documentation clearly states not to use dispatch_get_main_queue(). Though using an indirect queue it technically is following the documentation.

The reason for all this is because NSOperationQueue.mainQueue is not a safe for asynchronous operations, because it is globally accessible and it's maxConcurrentOperationCount is set to 1. So can easily shoot yourself in the foot with this operation queue.

Update 1

It seems there is a lot of confusion about the basis of what this question assumes an "asynchronous NSOperation" is. To be clear this is based on the concepts in this WWDC session The particular concept is using "operation readiness" and dependency management to manage the tasks in your app, which means asynchronous NSOperations are added to NSOperationQueues to take advantage of this. If you take these concepts to the spirit of this question hopefully the reasoning will make more sense, and you can focus on comparing and contrasting the solution with other ones.

Update 2 - Example of issue:

// VendorManager represents any class that you are not in direct control over.

@interface VendorManager : NSObject
@end

@implementation VendorManager

+ (void)doAnsyncVendorRoutine:(void (^)(void))completion {
    // Need to do some expensive work, make sure we are off the main thread
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND 0), ^(void) {
        // Some off main thread background work
        sleep(10);
        // We are done, go back to main thread
        [NSOperationQueue.mainQueue addOperationWithBlock:completion];
    });
}

@end


// MYAsyncBoilerPlateOperation represents all the boilerplate needed
// to implement a useful asnychronous NSOperation implementation.

@interface MYAlertOperation : MYAsyncBoilerPlateOperation
@end

@implementation MYAlertOperation

- (void)main {

    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:"Vendor"
                                                                             message:"Should vendor do work?"
                                                                      preferredStyle:UIAlertControllerStyleAlert];
    __weak __typeof(self) weakSelf = self;
    [alertController addAction:[UIAlertAction actionWithTitle:@"Yes"
                                                        style:UIAlertActionStyleDefault
                                                      handler:^(UIAlertAction *action) {
                                                          [VendorManager doAnsyncVendorRoutine:^{
                                                              // implemented in MYAsyncBoilerPlateOperation
                                                              [weakSelf completeThisOperation];
                                                          }];
                                                      }]];
    [alertController addAction:[UIAlertAction actionWithTitle:@"No"
                                                        style:UIAlertActionStyleDefault
                                                      handler:^(UIAlertAction *action) {
                                                          [weakSelf cancel];
                                                      }]];

    [MYAlertManager sharedInstance] presentAlert:alertController animated:YES];
}

@end

// MYAlertOperation will never complete.
// Because of an indirect dependency on operations being run on mainQueue.
// This example is an issue because mainQueue maxConcurrentOperationCount is 1.
// This example would not be an issue if maxConcurrentOperationCount was > 1.

[NSOperationQueue.mainQueue addOperation:[[MYAlertOperation alloc] init]];

Update 3 - Example 2:

I am not showing the implementation of MyAsyncBlockOperation but you can use this as what it's based on in Swift.

// operation.asynchronous is YES, and things are implemented correctly for state changes.
MyAsyncBlockOperation *operation = [MyAsyncBlockOperation new];
__weak MyAsyncBlockOperation *weakOperation = operation;
// executionBlock is simply invoked in main
// - (void)main { self.executionBlock() };
operation.executionBlock = ^{
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Vendor"
                                                                             message:@"Should vendor do work?"
                                                                      preferredStyle:UIAlertControllerStyleAlert];

    [alertController addAction:[UIAlertAction actionWithTitle:@"Yes"
                                                        style:UIAlertActionStyleDefault
                                                      handler:^(UIAlertAction *action) {
                                                          [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                                                              NSLog(@"Never called");
                                                              [weakOperation completeWithSuccess];
                                                          }];
                                                      }]];

    [alertController addAction:[UIAlertAction actionWithTitle:@"No"
                                                        style:UIAlertActionStyleDefault
                                                      handler:^(UIAlertAction *action) {
                                                          [weakOperation cancel];
                                                      }]];

    [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alertController animated:YES completion:nil];
}];

operation.completionBlock = ^{
    NSLog(@"If YES, Never called. If NO, called.");
};

[[NSOperationQueue mainQueue] addOperation:operation];

So I thought, why not have another NSOperationQueue? One with it's underlyingQueue set to the previously mentioned indirect GCD queue (still following the documentation). So we can have a concurrent NSOperationQueue, legally targeting the serial main GCD queue, and ultimately ensuring the operations run on the main thread.

Let me know if you want clarification, here is an example of the full code:

NSOperationQueue *asyncSafeMainQueue = [[NSOperationQueue alloc] init];
asyncSafeMainQueue.qualityOfService = NSQualityOfServiceDefault; // not needed, just for clarity
dispatch_queue_t underlyingQueue = dispatch_queue_create("com.mydomain.main-thread", NULL);
dispatch_set_target_queue(underlyingQueue, dispatch_get_main_queue());
asyncSafeMainQueue.underlyingQueue = underlyingQueue;

Now... there is a safe operation queue for asynchronous operations that need to run on the main thread, and without any unnecessary context switching.

Is it safe?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I don't understand why you think mainQueue is not safe for asynchronous operations. The reasons you gave would make it unsafe for synchronous operations (because you could deadlock).

Anyway, I think it's a bad idea to try the workaround you're suggesting. Apple didn't explain (on the pages you linked) why you shouldn't set underlyingQueue to the main queue. I recommend you play it safe and follow the spirit of the prohibition rather than the letter.

Update

Looking now at your updated question, with example code, I see nothing that can block the main thread/queue, so there is no possibility of deadlocking. It doesn’t matter that mainQueue has a macConcurrentOperationCount of 1. I don’t see anything in your example that requires or benefits from the creation of a separate NSOperationQueue.

Also, if the underlyingQueue is a serial queue (or has a serial queue anywhere in its target chain), then it doesn’t matter what you set maxConcurrentOperationCount to. The operations will still run serially. Try it yourself:

@implementation AppDelegate {
    dispatch_queue_t concurrentQueue;
    dispatch_queue_t serialQueue;
    NSOperationQueue *operationQueue;
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    concurrentQueue = dispatch_queue_create("q", DISPATCH_QUEUE_CONCURRENT);
    serialQueue = dispatch_queue_create("q2", nil);
    operationQueue = [[NSOperationQueue alloc] init];

    // concurrent queue targeting serial queue
    //dispatch_set_target_queue(concurrentQueue, serialQueue);
    //operationQueue.underlyingQueue = concurrentQueue;

    // serial queue targeting concurrent queue
    dispatch_set_target_queue(serialQueue, concurrentQueue);
    operationQueue.underlyingQueue = serialQueue;

    operationQueue.maxConcurrentOperationCount = 100;

    for (int i = 0; i < 100; ++i) {
        NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"operation %d starting", i);
            sleep(3);
            NSLog(@"operation %d ending", i);
        }];
        [operationQueue addOperation:operation];
    }
}

@end

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...