以下是否安全使用 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 的 underlyingQueue 属性,因为 Apple 的文档明确指出不使用 dispatch_get_main_queue()。虽然使用间接队列,但从技术上讲,它是遵循文档的。

造成这一切的原因是因为 NSOperationQueue.mainQueue对于异步操作来说并不安全,因为它是全局可访问的,并且 maxConcurrentOperationCount设置为 1。所以可以很容易地用这个手术队列射自己的脚。

更新 1

对于这个问题假设的"异步NSOperation"的基础似乎有很多困惑。需要明确的是,这是基于此 WWDC 会话中的概念 特定概念是使用"操作准备"和依赖项管理来管理应用中的任务,这意味着异步 NSOperations 将添加到 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呢?一个将其底层队列设置为前面提到的间接 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;

现在。。。对于需要在主线程上运行的异步操作,有一个安全的操作队列,没有任何不必要的上下文切换

安全吗?

我不明白为什么你认为mainQueue对异步操作不安全。您给出的原因会使同步操作不安全(因为您可能会死锁)。

无论如何,我认为尝试您建议的解决方法是个坏主意。苹果没有解释(在你链接的页面上)为什么你不应该underlyingQueue设置为主队列。我建议你谨慎行事,遵循禁令的精神,而不是文字。

更新

现在看看你更新的问题,用示例代码,我没有看到任何可以阻塞主线程/队列的东西,所以没有死锁的可能性。mainQueuemacConcurrentOperationCount为 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

嗯。如果使用setTarget而不是指定的构造函数,这在 Swift-4 中会崩溃得非常严重。

如果你使用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 是我能找到的唯一用例。并且您可以独立地恢复/挂起自定义队列上的任务,即使其基础队列是主队列或其他队列。和挂起/恢复所有其他队列所针对的一个队列,将依次挂起/恢复所有其他队列。

最新更新