49

我遇到了一個場景,我有一個可能發生在主線程或另一個線程上的委託回調,並且直到運行時纔會知道它(使用StoreKit.framework)。爲什麼我們不能在當前隊列上使用dispatch_sync?

我也有,我需要在回調函數執行前發生哪些需要更新UI代碼,所以我最初的想法是有這樣的功能:當

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

    // code here that depends upon the UI getting updated 
} 

那偉大工程,它在後臺線程上執行。但是,在主線程上執行時,程序會發生死鎖。

這本身似乎對我有意思,如果我讀dispatch_sync權的文檔,然後我希望它只是執行塊顧左右而言他,不擔心調度入runloop,作爲所述here

作爲優化,此函數儘可能調用當前線程上的塊。

但是,這還不算什麼大不了的事,它只是意味着多一點打字,這導致我這種方法:

-(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); 
} 

然而,這似乎有點倒退。這是製作GCD的錯誤,還是我在文檔中缺少的東西?

+2

'dispatch_get_current_queue()'由現在已經過時。去檢測主隊列的方式是'NSThread.isMainThread()'(SWIFT)或[NSThread isMainThread](目標C) – udondan

+0

'NSThread.isMainThread()'是不可靠的,因爲在極少數情況下在主隊列塊, GCD重用主線程來執行其他隊列。參見[1](http://blog.krzyzanowskim.com/2016/06/03/queues-are-not-bound-to-any-specific-thread/),[2](http://blog.benjamin -encz.de/post/main-queue-vs-main-thread/)。 – Jano

+0

@jtbandes在將問題標爲重複項時請注意。這個問題顯然比你所聯繫的問題更老,活動也更多,也許他們應該朝相反的方向關閉。 –

回答

44

我發現這個在the documentation (last chapter)

不要從上您傳遞給你的函數調用同一個隊列執行 任務調用dispatch_sync功能。這樣會使隊列死鎖 。如果您需要分派到當前隊列,做 所以異步使用dispatch_async功能。

而且,我跟着你提供的鏈接,並在dispatch_sync的描述我這樣說的:

調用此函數並在僵局針對當前隊列結果。

所以我不認爲這是GCD的問題,我認爲唯一明智的方法就是在發現問題後發明的方法。

+0

嗯。我想我要做的是定義一個宏,調用我認爲適用於'dispatch_sync'的行爲。 –

+11

我必須說我不同意** dispatch_sync **的行爲方式有什麼問題。如果你仔細想想,** dispatch_sync **和** async **都會排隊這些任務,但是第一個任務在執行任務之前也不會返回。在您提供的示例中,該任務已排隊但從未執行過,這是造成死鎖的直接原因。所以請記住,這個函數的主要功能是實際排隊任務,而不是調用它。調用是一個不同的故事,但是從你寫的內容來看,你期望這個函數能夠真正調用你的任務。 – lawicko

+7

我不同意。我沒有真正關心'dispatch_sync'如何在封面下工作,我關心的是,從上到下看,它的作用是'在給定線程上執行這個代碼,並在完成時返回'。如果我在目標線程上,對於我來說,檢查我是否在目標線程上是沒有意義的,因爲函數應該爲我做。這真的讓我感到意外,但是因爲大多數蘋果的API比這更聰明,我猜測開發人員在工作上只是懶惰? :) –

3

dispatch_asyncdispatch_sync都執行將它們的動作推送到期望的隊列上。行動不會立即發生;它發生在隊列運行循環的將來迭代中。 dispatch_asyncdispatch_sync之間的區別在於dispatch_sync會阻止當前隊列,直到動作結束。

想想當你在當前隊列上異步執行某些操作時會發生什麼。再次,它不會立即發生;它將它放入FIFO隊列中,並且必須等到運行循環的當前迭代完成之後(並且可能還要等待在執行此新動作之前隊列中的其他操作)。

現在你可能會問,當爲當前隊列異步執行一個動作時,爲什麼不總是直接調用該函數,而不是等到將來某個時間。答案是兩者之間有很大的區別。很多時候,你需要執行一個動作,但是在之後需要執行,無論副作用是由運行循環當前迭代中的堆棧函數執行的,或者您需要在運行循環中已安排的某些動畫操作後執行您的操作等。這就是爲什麼很多時候您會看到代碼[obj performSelector:selector withObject:foo afterDelay:0](是的,它與[obj performSelector:selector withObject:foo]不同)。

如前所述,dispatch_syncdispatch_async相同,只是它會阻止,直到動作完成。所以很明顯爲什麼它會死鎖 - 至少在運行循環的當前迭代完成之後,塊才能執行;但我們正在等待它完成,然後再繼續。

理論上可以爲dispatch_sync做一個特殊情況,因爲當它是當前線程時,立即執行它。 (performSelector:onThread:withObject:waitUntilDone:存在這種特殊情況,當線程爲當前線程且waitUntilDone:爲YES時,它立即執行該線程。)但是,我想蘋果決定,無論隊列如何,最好在這裏保持一致的行爲。

+0

討論但這是沒有意義的。應該至少有一個日誌消息輸出到控制檯以防萬一,因爲有其他API(例如,遞歸的NSLock)。 –

+0

@newacct「dispatch_sync阻止當前線程」?阻止當前線程或當前隊列? – onmyway133

+0

@entropy:固定 – newacct

6

的文件明確指出,傳遞當前隊列會導致死鎖。

現在他們並沒有說他們爲什麼這樣設計(除了實際上需要額外的代碼才能使它工作),但我懷疑這樣做的原因是因爲在這種特殊情況下,塊會是「跳躍」隊列,即在正常情況下,你的塊結束隊列上的所有其它模塊已經運行後運行,但在這種情況下,將之前運行。

這個問題,當你試圖使用GCD的互斥機制出現,這種特殊的情況下,等同於使用遞歸互斥。我不想陷入爭論它是否最好使用GCD或傳統的互斥API,如並行線程互斥,甚至無論是使用遞歸互斥體是一個好主意;我會讓別人爲此爭論,但肯定會有這樣的需求,特別是當它是你正在處理的主要隊列時。

就個人而言,我認爲dispatch_sync會比較有用的,如果它支持這樣或如果有,所提供的替代行爲的另一功能。我會敦促那些認爲如此的人向蘋果公司提交錯誤報告(正如我所做的那樣,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); 
} 

注:以前,我有一個使用dispatch_get_current_queue()的例子,但現在已被棄用。

+0

我已經做了類似的事情,除了一個宏,所以我用'dispatch_sync'編寫的其他代碼沒有被破壞。 +1給你! –

+1

一個宏可以很好地工作,但一般來說,我建議你在不能使用靜態內聯函數時只使用一個宏,因爲它們可以用於許多原因,宏並沒有什麼優勢。 –

+1

dispatch_get_current_queue從iOS 6.x開始已棄用 – openfrog

61

dispatch_sync做兩兩件事:

  1. 隊列塊
  2. 塊當前線程,直到該塊已經完成運行

鑑於主線程是一個串行隊列(這意味着它只使用一個線程),以下語句:

dispatch_sync(dispatch_get_main_queue(), ^(){/*...*/}); 

將導致以下事件:

  1. dispatch_sync隊列在主隊列塊。
  2. dispatch_sync阻塞主隊列的線程,直到塊完成執行。
  3. dispatch_sync永遠等待,因爲塊應該運行的線程被阻塞。

理解這一點的關鍵是dispatch_sync不執行塊,它只是排隊它們。執行將在運行循環的未來迭代中發生。

以下方法:

if (queueA == dispatch_get_current_queue()){ 
    block(); 
} else { 
    dispatch_sync(queueA,block); 
} 

是完全沒有問題的,但要知道,它不會保護你免受涉及隊列的層次結構複雜的場景。在這種情況下,當前隊列可能與您嘗試發送塊的先前阻止的隊列不同。例如:在調度隊列

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 
     }); 
    }); 
}); 

對於複雜的情況下,讀/寫密鑰值數據:

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 
    } 
}); 

說明:

  • 我創建一個workerQ隊列指向一個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更新完成的)。

+1

顯然這是正確的。 'dispatch_sync'幾乎永遠不會走,我只需要幾次就可以從我的應用程序的UI部分更新和獲取結果,而過去你需要選擇其他的東西。檢查隊列層次結構的瘋狂技術可能只會導致痛苦。 –

+0

這很麻煩,我寧願有一個內置的'amIChildOfQueue:',但使用特定於隊列的上下文是Apple針對複雜情況推薦的解決方案。請參閱線程[dispatch_get_current_queue()棄用](https://devforums.apple.com/message/710745)中的帖子#6。 – Jano

+0

你可以看看這個問題嗎? http://stackoverflow.com/questions/19833744/how-to-use-dispatch-queue-set-specific-and-dispatch-get-specific – hfossli

12

我知道你的困惑來自:

作爲一種優化,這個函數調用了當前 線程塊時可能。

小心,它說當前線程

主題!=隊列

隊列自己並不擁有的線程和線程未綁定到一個隊列。有線程和隊列。每當一個隊列想要運行一個塊時,它需要一個線程,但這並不總是相同的線程。它只需要任何線程(每次都可以是不同的線程),並且在完成運行塊(目前)時,同一個線程現在可以被不同的隊列使用。

這句話談到的優化是關於線程,而不是關於隊列。例如。考慮你有兩個串行隊列,QueueAQueueB現在你做到以下幾點:

dispatch_async(QueueA, ^{ 
    someFunctionA(...); 
    dispatch_sync(QueueB, ^{ 
     someFunctionB(...); 
    }); 
}); 

QueueA運行塊,它會暫時擁有一個線程,任何線程。 someFunctionA(...)將在該線程上執行。現在在做同步調度時,QueueA不能做任何其他事情,它必須等待調度完成。 QueueB另一方面,也將需要一個線程來運行其塊並執行someFunctionB(...)。所以要麼QueueA暫時掛起其線程和QueueB使用一些其他線程運行該塊或QueueA把它的線程轉移到QueueB(畢竟它不會需要它,直到同步調度完成)和QueueB直接使用當前線程QueueA

不用說,最後一個選項要快得多,因爲不需要線程切換。而這個就是優化句子講的。所以一個dispatch_sync()到不同的隊列可能並不總是會導致線程切換(不同的隊列,也許是同一個線程)。

但是dispatch_sync()仍然不能發生在相同的隊列(相同的線程,是的,相同的隊列,沒有)。這是因爲一個隊列會在塊之後執行塊,當它執行一個塊時,它將不會執行另一個塊,直到完成一個塊爲止。所以它執行BlockABlockA在同一隊列上執行BlockBdispatch_sync()。隊列將不能運行,只要BlockB它仍然運行BlockA,但運行BlockA不會繼續下去,直到BlockB已經跑了。看到問題了嗎?

2

從下列文檔實測值。 https://developer.apple.com/library/ios/documentation/Performance/Reference/GCD_libdispatch_Ref/index.html#//apple_ref/c/func/dispatch_sync

不像dispatch_async, 「dispatch_sync」 函數不返回,直到塊已完成。調用此函數並定位當前隊列會導致死鎖。

dispatch_async不同,不在目標隊列上執行保留。由於對該功能的調用是同步的,因此調用者的引用「借用」。此外,在塊上執行Block_copy沒有

作爲一種優化,該函數調用在當前線程上的塊時可能的。

相關問題