为什么我们不能在当前队列上使用dispatch_sync?

Ric*_*III 58 multithreading objective-c grand-central-dispatch objective-c-blocks

我遇到了一个场景,我有一个委托回调,可能发生在主线程或另一个线程上,我不知道哪个直到运行时(使用StoreKit.framework).

我还需要在该回调中更新UI代码,这需要在函数执行之前发生,因此我最初的想法是拥有这样的函数:

-(void) someDelegateCallback:(id) sender
{
    dispatch_sync(dispatch_get_main_queue(), ^{
        // ui update code here
    });

    // code here that depends upon the UI getting updated
}
Run Code Online (Sandbox Code Playgroud)

当它在后台线程上执行时,它工作得很好.但是,当在主线程上执行时,程序陷入僵局.

这本身似乎对我有意思,如果我读的文档dispatch_sync正确的,那么我希望它只是执行彻底,不担心它安排到runloop块,如说在这里:

作为优化,此函数在可能的情况下调用当前线程上的块.

但是,这不是太大的交易,它只是意味着更多的打字,这导致我采用这种方法:

-(void) someDelegateCallBack:(id) sender
{
    dispatch_block_t onMain = ^{
        // update UI code here
    };

    if (dispatch_get_current_queue() == dispatch_get_main_queue())
       onMain();
    else
       dispatch_sync(dispatch_get_main_queue(), onMain);
}
Run Code Online (Sandbox Code Playgroud)

然而,这似乎有点倒退.这是制作GCD的一个错误,还是我在文档中遗漏了什么?

Jan*_*ano 72

dispatch_sync 做两件事:

  1. 排队一个块
  2. 阻止当前线程,直到块完成运行

鉴于主线程是一个串行队列(这意味着它只使用一个线程),以下语句:

dispatch_sync(dispatch_get_main_queue(), ^(){/*...*/});
Run Code Online (Sandbox Code Playgroud)

将导致以下事件:

  1. dispatch_sync 将块排队到主队列中.
  2. dispatch_sync 阻止主队列的线程,直到块完成执行.
  3. dispatch_sync 因为块应该运行的线程被阻塞而永远等待.

理解这一点的关键是dispatch_sync不执行块,它只对它们进行排队.执行将在运行循环的未来迭代中发生.

以下方法:

if (queueA == dispatch_get_current_queue()){
    block();
} else {
    dispatch_sync(queueA,block);
}
Run Code Online (Sandbox Code Playgroud)

完全没问题,但请注意,它不会保护您免受涉及队列层次结构的复杂场景的影响.在这种情况下,当前队列可能与您尝试发送块的先前阻塞的队列不同.例:

dispatch_sync(queueA, ^{
    dispatch_sync(queueB, ^{
        // dispatch_get_current_queue() is B, but A is blocked, 
        // so a dispatch_sync(A,b) will deadlock.
        dispatch_sync(queueA, ^{
            // some task
        });
    });
});
Run Code Online (Sandbox Code Playgroud)

对于复杂的情况,在调度队列中读/写键值数据:

dispatch_queue_t workerQ = dispatch_queue_create("com.meh.sometask", NULL);
dispatch_queue_t funnelQ = dispatch_queue_create("com.meh.funnel", NULL);
dispatch_set_target_queue(workerQ,funnelQ);

static int kKey;

// saves string "funnel" in funnelQ
CFStringRef tag = CFSTR("funnel");
dispatch_queue_set_specific(funnelQ, 
                            &kKey,
                            (void*)tag,
                            (dispatch_function_t)CFRelease);

dispatch_sync(workerQ, ^{
    // is funnelQ in the hierarchy of workerQ?
    CFStringRef tag = dispatch_get_specific(&kKey);
    if (tag){
        dispatch_sync(funnelQ, ^{
            // some task
        });
    } else {
        // some task
    }
});
Run Code Online (Sandbox Code Playgroud)

说明:

  • 我创建了一个workerQ指向funnelQ队列的队列.在实际代码中,如果您有多个"工作"队列并且想要一次性恢复/暂停(通过恢复/更新其目标funnelQ队列来实现),这将非常有用.
  • 我可以在任何时候汇集我的工作人员队列,以便知道他们是否有漏洞,我funnelQ用"漏斗"这个词标记.
  • 下山的路我dispatch_sync的东西workerQ,而不管是什么原因,我想dispatch_syncfunnelQ的,但避免dispatch_sync当前的队列,所以我检查了标签,并采取相应的行动.因为get在层次结构中向上移动,所以不会找到该值,workerQ但会在其中找到funnelQ.这是一种查明层次结构中是否存在值的队列的方法.因此,要防止dispatch_sync到当前队列.

如果您想知道读/写上下文数据的函数,有三个:

  • dispatch_queue_set_specific:写入队列.
  • dispatch_queue_get_specific:从队列中读取.
  • dispatch_get_specific:从当前队列中读取的便捷功能.

密钥通过指针进行比较,从不解除引用.setter中的最后一个参数是释放密钥的析构函数.

如果您想知道"将一个队列指向另一个队列",那就意味着这一点.例如,我可以将队列A指向主队列,它将导致队列A中的所有块在主队列中运行(通常这是为UI更新完成的).

  • 显然这是正确的.`dispatch_sync`几乎是不可能的,我只需要几次来更新并从我的应用程序的UI部分获得结果,过去,你需要选择其他东西.你检查队列层次结构的疯狂技术可能只会导致痛苦. (2认同)

law*_*cko 51

我在文档中找到了这个(最后一章):

不要从传递给函数调用的同一队列上执行的任务调用dispatch_sync函数.这样做会使队列死锁.如果需要调度到当前队列,请使用dispatch_async函数异步执行此操作.

另外,我按照你提供的链接和dispatch_sync的描述我读了这个:

调用此函数并以当前队列为目标会导致死锁.

所以我不认为这是GCD的问题,我认为唯一合理的方法是你在发现问题后发明的方法.

  • 我必须说我不同意**dispatch_sync**的行为方式有问题.如果你考虑一下,**dispatch_sync**和**async**都会对任务进行排队,但是第一个任务在执行任务之前也不会返回.在您提供的示例中,任务排队但从未执行过,这是导致死锁的直接原因.所以请记住,此函数的主要功能是实际排队任务,而不是调用它.调用是一个不同的故事,但从您编写的内容看起来您希望此函数实际调用您的任务. (12认同)
  • @Richard:我相信你的想法中的错误就在这里:«我关心的是,从上到下看,它的作用是在给定的***线程***上执行此代码,并在它的时候返回».`dispatch_sync()`不适用于_threads_,它适用于队列.主队列保证在主线程上运行的事实与`dispatch_sync()`的观点是一致的.为了立即执行你想要入队的块会破坏它的含义 - 在当前的任务完成之前执行下一个任务意味着你将不再有队列行为. (10认同)
  • @ RichardJ.RossIII,你似乎忽略了这样一个事实:你正在使用的API是一个串行队列,并且当你等待它后面的项目执行时你试图阻止该队列上的当前项目.API没有按照您的要求执行操作并不意味着它的实现很差.它完全符合记录的要求. (9认同)
  • 我不同意.关于`dispatch_sync`如何在幕后工作,我并不感兴趣,我关心的是,从上到下看,它的作用是"在给定的线程上执行此代码,并在完成时返回".如果我在目标线程上,那么我无需检查我是否在目标线程上,因为该函数应该为我做.这真的让我感到惊讶,不过因为大多数苹果的API都比这更聪明,我猜开发人员在工作中只是懒得?:) (8认同)
  • 问题是,在99.9%的时间内,没有人真正想要真正的串行队列语义。他们不在乎顺序。他们只希望没有并发。在某些情况下,dispatch_sync语义是有意义的,但我认为它们引起问题的可能性远大于其造成的帮助。就是说,如果您只想在主线程上运行一些代码,`performSelectorOnMainThread:`具有您要查找的语义。或者只是写#define dispatch_sync_safe(queue,block){if(queue == dispatch_get_current_queue()){block(); } else {dispatch_sync(queue,block);}}`并调用它。 (2认同)

Mec*_*cki 15

我知道你的困惑来自哪里:

作为优化,此函数在可能的情况下调用当前线程上的块.

小心,它说当前的线程.

线程!=队列

队列不拥有线程,线程未绑定到队列.有线程,有队列.每当队列想要运行一个块时,它就需要一个线程,但这并不总是相同的线程.它只需要任何线程(这可能每次都是不同的),当它完成运行块(暂时)时,同一个线程现在可以由不同的队列使用.

这句话所讨论的优化是关于线程,而不是关于队列.例如,考虑你有两个串行队列,QueueA并且QueueB现在你做到以下几点:

dispatch_async(QueueA, ^{
    someFunctionA(...);
    dispatch_sync(QueueB, ^{
        someFunctionB(...);
    });
});
Run Code Online (Sandbox Code Playgroud)

QueueA运行块时,它将暂时拥有一个线程,任何线程.someFunctionA(...)将在该线程上执行.现在在进行同步调度时,QueueA不能做任何事情,它必须等待调度完成.QueueB另一方面,还需要一个线程来运行它的块并执行someFunctionB(...).因此要么QueueA暂时挂起它的线程并QueueB使用其他一些线程来运行块或将QueueA其线程交给QueueB(毕竟它不需要它,直到同步调度完成)并QueueB直接使用当前线程QueueA.

不用说最后一个选项要快得多,因为不需要线程切换.而是句子谈到了优化.因此dispatch_sync(),对于不同的队列,可能并不总是导致线程切换(不同的队列,可能是相同的线程).

但是dispatch_sync()仍然不能发生在同一个队列中(相同的线程,是的,相同的队列,没有).这是因为队列将在块之后执行,当它执行一个块时,它将不执行另一个队列,直到当前执行完毕.因此,执行BlockABlockA做了dispatch_sync()BlockB在同一个队列.BlockB只要它仍然运行BlockA,队列就不会运行,但是在运行BlockA之前运行不会继续BlockB.看到问题?这是一个经典的僵局.


Chr*_*ter 6

文档明确指出传递当前队列将导致死锁.

现在他们没有说明他们为什么设计这样的东西(除了它实际上需要额外的代码使其工作),但我怀疑这样做的原因是因为在这种特殊情况下,块会"跳跃"队列,即在正常情况下,你的块在队列上的所有其他块运行之后最终运行,但在这种情况下它将在之前运行.

当您尝试将GCD用作互斥机制时会出现此问题,并且此特定情况等同于使用递归互斥锁.我不想讨论是否更好地使用GCD或传统的互斥API(如pthreads互斥体),或者使用递归互斥体是否是个好主意; 我会让其他人争论,但肯定有这个需求,特别是当它是你正在处理的主要队列时.

就个人而言,我认为dispatch_sync如果支持这个或者有另一个提供备用行为的函数会更有用.我会敦促其他人这么认为向Apple提交错误报告(正如我所做的那样,ID:12668073).

您可以编写自己的函数来执行相同的操作,但这有点像黑客:

// Like dispatch_sync but works on current queue
static inline void dispatch_synchronized (dispatch_queue_t queue,
                                          dispatch_block_t block)
{
  dispatch_queue_set_specific (queue, queue, (void *)1, NULL);
  if (dispatch_get_specific (queue))
    block ();
  else
    dispatch_sync (queue, block);
}
Run Code Online (Sandbox Code Playgroud)

NB以前,我有一个使用dispatch_get_current_queue()的示例,但现在已弃用.

  • 但是对于主队列,你可以使用`if([NSThread isMainThread]){block()} else {dispatch_sync(dispatch_get_main_queue(),block); 这是安全的,因为所有以主队列为目标的队列也在主线程上执行(因为主队列是一个串行队列). (2认同)