Является ли следующее безопасным использование 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