Является ли следующее безопасным использование dispatch_set_target_queue()?

То, что я хочу сделать, это создать косвенную очередь, ориентированную на основную очередь.

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

Моя конечная цель - использовать очередь в качестве базового свойства NSOperationQueue, поскольку в документации Apple четко указано, что не следует использовать dispatch_get_main_queue (). Хотя используя косвенную очередь, он технически следует документации.

Причина всего этого заключается в том, что NSOperationQueue.mainQueue не является безопасным для асинхронных операций, потому что он доступен глобально и его maxConcurrentOperationCount имеет значение 1. Так что можете легко выстрелить себе в ногу с помощью этой очереди операций.

Обновление 1

Кажется, существует много путаницы в отношении того, что этот вопрос предполагает "асинхронной NSOperation". Чтобы было понятно, это основано на концепциях в этом сеансе WWDC. Конкретная концепция использует "готовность к работе" и управление зависимостями для управления задачами в вашем приложении, что означает, что асинхронные операции NSO добавляются в NSOperationQueues, чтобы воспользоваться этим. Если вы отнесете эти концепции к духу этого вопроса, надеюсь, рассуждения будут иметь больше смысла, и вы сможете сосредоточиться на сравнении и сопоставлении решения с другими.

Обновление 2 - пример проблемы:

// 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]];

Обновление 3 - Пример 2:

Я не показываю реализацию MyAsyncBlockOperation, но вы можете использовать это как то, на чем он основан в 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];

Вот я и подумал, а почему бы не иметь еще одну NSOperationQueue? Один с базовым значением Queue, установленным в ранее упомянутую косвенную очередь GCD (все еще следуя документации). Таким образом, мы можем иметь параллельную NSOperationQueue, легально нацеленную на последовательную главную очередь GCD, и, в конечном итоге, обеспечивающую выполнение операций в главном потоке.

Дайте мне знать, если вы хотите получить разъяснения, вот пример полного кода:

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;

Теперь... есть безопасная очередь операций для асинхронных операций, которые должны выполняться в главном потоке без каких-либо ненужных переключений контекста.

Это безопасно?

2 ответа

Хм.. Это очень сильно падает в Swift-4 при использовании setTarget вместо назначенного конструктора..

Если вы используете мост Objective-C, то вы можете сделать:

@interface MakeQueue : NSObject
+ (NSOperationQueue *)makeQueue:(bool)useSerial;
@end

@implementation MakeQueue
+ (NSOperationQueue *)makeQueue:(bool)useSerial {
    dispatch_queue_t serial = dispatch_queue_create("serial", nil);
    dispatch_queue_t concurrent = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);

    dispatch_queue_t queue = useSerial ? serial : concurrent;
    dispatch_set_target_queue(queue, dispatch_get_main_queue());

    NSOperationQueue *opq = [[NSOperationQueue alloc] init];
    opq.underlyingQueue = queue;
    opq.maxConcurrentOperationCount = 8;
    return opq;
}
@end

и если вы используете Swift, у вас есть:

func makeQueue(_ useSerial: Bool) -> OperationQueue? {

    let testCrash: Bool = false
    var queue: DispatchQueue!

    if testCrash {
        let serial = DispatchQueue(label: "serial")
        let concurrent = DispatchQueue(label: "concurrent", attributes: .concurrent)
        queue = useSerial ? serial : concurrent
        queue.setTarget(queue: DispatchQueue.main)
    }
    else {
        let serial = DispatchQueue(label: "serial", qos: .default, attributes: .init(rawValue: 0), autoreleaseFrequency: .inherit, target: DispatchQueue.main)
        let concurrent = DispatchQueue(label: "concurrent", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: DispatchQueue.main)
        queue = useSerial ? serial : concurrent
    }

    let opq = OperationQueue()
    opq.underlyingQueue = queue
    opq.maxConcurrentOperationCount = 8;
    return opq
}

Итак, теперь мы проверяем это:

class ViewController : UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        //Test Objective-C
        let operationQueue = MakeQueue.makeQueue(false)!
        operationQueue.addOperation {
            self.download(index: 1, time: 3)
        }

        operationQueue.addOperation {
            self.download(index: 2, time: 1)
        }

        operationQueue.addOperation {
            self.download(index: 3, time: 2)
        }


        //Test Swift
        let sOperationQueue = makeQueue(false)!
        sOperationQueue.addOperation {
            self.download(index: 1, time: 3)
        }

        sOperationQueue.addOperation {
            self.download(index: 2, time: 1)
        }

        sOperationQueue.addOperation {
            self.download(index: 3, time: 2)
        }
    }

    func download(index : Int, time: Int){
        sleep(UInt32(time))
        print("Index: \(index)")
    }
}

В любом случае, это не имеет значения, что maxConcurrentOperations являются... если базовая очередь является последовательной, то установка этого значения, по-видимому, НИЧЕГО не делает.. Однако, если базовая очередь параллельна, она накладывает ограничение на то, сколько операций можно запустить одновременно.

В общем, как только основная очередь MainQueue или любой последовательной очереди, все операции передаются в нее (последовательно) и блокируются (она ожидает, потому что это последовательная очередь).

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

Это: https://gist.github.com/jspahrsummers/dbd861d425d783bd2e5a - единственный вариант использования, который я смог найти... И что вы можете независимо возобновлять / приостанавливать задачи в своей пользовательской очереди, даже если ее основная очередь является главной или какой-либо другой очередь. И приостановка / возобновление одной очереди, на которую нацелены все остальные очереди, в свою очередь приостановит / возобновит все остальные очереди.

Я не понимаю, почему вы думаете mainQueue не является безопасным для асинхронных операций. Причины, которые вы привели, делают его небезопасным для синхронных операций (потому что вы можете зайти в тупик).

Во всяком случае, я думаю, что это плохая идея попробовать обходной путь, который вы предлагаете. Apple не объяснила (на страницах, на которые вы ссылались), почему вы не должны устанавливать underlyingQueue в основную очередь. Я рекомендую вам соблюдать осторожность и следовать духу запрета, а не буквы.

Обновить

Глядя на ваш обновленный вопрос с примером кода, я не вижу ничего, что могло бы блокировать основной поток / очередь, поэтому нет возможности взаимоблокировки. Это не важно mainQueue имеет macConcurrentOperationCount 1. Я не вижу в вашем примере ничего, что требовало бы или извлекало выгоду из создания отдельного NSOperationQueue,

Кроме того, если underlyingQueue является последовательной очередью (или имеет последовательную очередь где-либо в своей целевой цепочке), тогда не имеет значения, что вы установили maxConcurrentOperationCount к. Операции по-прежнему будут выполняться последовательно. Попробуй сам:

@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
Другие вопросы по тегам