52

忍受着我,這是要做一些解釋。我有一個如下所示的函數。dispatch_sync與主隊列上的dispatch_async

上下文:「aProject」是名爲LPProject的核心數據實體,其數組名爲「memberFiles」,其中包含另一個名爲LPFile的Core Data實體的實例。每個LPFile代表磁盤上的一個文件,我們想要做的就是打開這些文件並解析它的文本,尋找指向OTHER文件的@import語句。如果我們找到@import語句,我們希望找到它們指向的文件,然後通過向表示第一個文件的核心數據實體添加關係,將該文件「鏈接」到該文件。由於所有這些都可能需要一些時間處理大文件,我們將使用GCD從主線程中刪除它。

- (void) establishImportLinksForFilesInProject:(LPProject *)aProject { 
    dispatch_queue_t taskQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
    for (LPFile *fileToCheck in aProject.memberFiles) { 
     if (//Some condition is met) { 
      dispatch_async(taskQ, ^{ 
       // Here, we do the scanning for @import statements. 
       // When we find a valid one, we put the whole path to the imported file into an array called 'verifiedImports'. 

       // go back to the main thread and update the model (Core Data is not thread-safe.) 
       dispatch_sync(dispatch_get_main_queue(), ^{ 

        NSLog(@"Got to main thread."); 

        for (NSString *import in verifiedImports) { 
          // Add the relationship to Core Data LPFile entity. 
        } 
       });//end block 
      });//end block 
     } 
    } 
} 

現在,這裏的事情變得怪異:

此代碼的工作,但我看到一個奇怪的問題。如果我在有幾個文件(大約20個)的LPProject上運行它,它可以完美運行。但是,如果我在具有更多文件(例如60-70)的LPProject上運行它,它確實運行正確NOT。我們永遠不會回到主線程,NSLog(@"got to main thread");從不出現,應用程序掛起。但是,(這是真正奇怪的地方)---如果我在小型項目FIRST上運行代碼然後在大型項目上運行代碼,那麼一切都很完美。只有當我在大型項目上運行代碼時,纔會出現問題。

而這裏的踢球,如果我改變第二派遣線到這一點:

dispatch_async(dispatch_get_main_queue(), ^{ 

(也就是說,使用async代替sync到塊分派到主隊列),一切正常,所有的時間。完美。無論項目中有多少文件!

我無法解釋這種行爲。任何幫助或提示下一步測試將不勝感激。

+0

注意:爲了簡潔起見,我已經編寫了「掃描」和「核心數據條目」代碼片段。但是,我幾乎肯定他們不是罪魁禍首,因爲如果我將所有內容放在單個線程中,並且他們在上述多線程情況下完美工作(通過首先運行一個小型項目「預熱」所有內容和/或在主隊列上使用dispatch_async()而不是dispatch_sync())。 – Bryan

+1

聽起來就像是碰到了一個死鎖問題 –

+0

當你處於這種狀態時,你應該對你的應用程序運行示例或工具來查看其他線程都在做什麼。如果他們陷入僵局,發生的事情應該更加明顯。 –

回答

53

這是與磁盤I/O和GCD相關的常見問題。基本上,GCD可能會爲每個文件產生一個線程,並且在某個時刻,您有太多的線程可以讓系統在合理的時間內進行服務。

每次調用dispatch_async()並在該塊中試圖執行任何I/O(例如,看起來您正在讀取某些文件)時,可能是該代碼塊中的線程正在執行將等待數據從文件系統中讀取時將阻塞(由操作系統暫停)。 GCD的工作方式是當它看到它的一個工作線程在I/O上被阻塞,並且你仍然要求它同時做更多的工作時,它會產生一個新的工作線程。因此,如果您嘗試在併發隊列中打開50個文件,則最終可能會導致GCD產生大約50個線程。

這對於系統來說意味着服務的線程太多,並且最終導致CPU的主線程捱餓。

解決此問題的方法是使用串行隊列而不是併發隊列來執行基於文件的操作。這很容易做到。您需要創建一個串行隊列並將其作爲ivar存儲在您的對象中,以免最終創建多個串行隊列。所以刪除此電話:

dispatch_queue_t taskQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

在你的init方法補充一點:

taskQ = dispatch_queue_create("com.yourcompany.yourMeaningfulLabel", DISPATCH_QUEUE_SERIAL);

在你的dealloc方法補充一點:

dispatch_release(taskQ);

並把這個作爲在你的班級聲明中有一個ivar:

dispatch_queue_t taskQ;

+6

邁克阿什也有一個關於這個問題的優秀寫作:http://mikeash.com/pyblog/friday-qa-2009-09-25-gcd-practicum.html –

+2

@Ryan - 感謝您的意見。這也發生在我身上,但如果問題太多併發線程,我們會期望大型項目每次都會失敗。在這種情況下,只要我先在一個較小的項目上運行代碼,它就可以工作。 (請注意,這兩個項目是完全獨立的文件,因此沒有文件被緩存等) – Bryan

+1

如果啓用了自動引用計數,該怎麼辦? – byJeevan

5

我認爲瑞安是在正確的道路上:有太多的線程時一個項目有1500個文件正在催生

所以,我重構了(我決定測試量。)上面的代碼工作是這樣的:

- (void) establishImportLinksForFilesInProject:(LPProject *)aProject 
{ 
     dispatch_queue_t taskQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 

    dispatch_async(taskQ, 
    ^{ 

    // Create a new Core Data Context on this thread using the same persistent data store  
    // as the main thread. Pass the objectID of aProject to access the managedObject 
    // for that project on this thread's context: 

    NSManagedObjectID *projectID = [aProject objectID]; 

    for (LPFile *fileToCheck in [backgroundContext objectWithID:projectID] memberFiles]) 
    { 
     if (//Some condition is met) 
     { 
       // Here, we do the scanning for @import statements. 
       // When we find a valid one, we put the whole path to the 
       // imported file into an array called 'verifiedImports'. 

       // Pass this ID to main thread in dispatch call below to access the same 
       // file in the main thread's context 
       NSManagedObjectID *fileID = [fileToCheck objectID]; 


       // go back to the main thread and update the model 
       // (Core Data is not thread-safe.) 
       dispatch_async(dispatch_get_main_queue(), 
       ^{ 
        for (NSString *import in verifiedImports) 
        { 
         LPFile *targetFile = [mainContext objectWithID:fileID]; 
         // Add the relationship to targetFile. 
        } 
       });//end block 
     } 
    } 
    // Easy way to tell when we're done processing all files. 
    // Could add a dispatch_async(main_queue) call here to do something like UI updates, etc 

    });//end block 
    } 

所以,基本上,我們現在產卵一個線程讀取所有的文件,而不是一個線程每個文件。另外,事實證明,在main_queue上調用dispatch_async()是正確的方法:工作線程會將該塊分派給主線程,並且在繼續掃描下一個文件之前不等待它返回。

這個實現基本上建立了一個Ryan建議的「串行」隊列(for循環是它的串行部分),但有一個優點:當for循環結束時,我們完成了所有文件的處理,我們可以在那裏粘貼一個dispatch_async(main_queue)塊來做我們想做的任何事情。這是一個非常好的方式,可以告訴我什麼時候併發處理任務已完成,並且在舊版本中不存在。

這裏的缺點是在多線程上使用Core Data要複雜一點。但是,這種方法似乎是防彈的項目與5000頁的文件(這是我測試過的最高水平。)

+1

是的,你原來的問題並沒有說明你的「文件」真的是CoreData對象。這是一個完全不同的問題。我的回覆涉及到實際的文件I/O。在這一點上,我意識到如果沒有看到完整的源代碼列表,我實際上無法知道你在做什麼。我不確定何時進行文件I/O或從CoreData中讀取數據。如果您需要更多輸入信息,請隨時列出您正在做的事情的來源。 – Ryan

0

我覺得它更容易與圖就明白了:

對於筆者所描述的情況:

| taskQ | ***********開始|

| dispatch_1 *********** | ---------

| dispatch_2 ************* | --- ------

| dispatch_n *************************** | ----------

| main隊列(同步)| **開始派送到main |

************************* | --dispatch_1-- | --dispatch_2-- | --dispatch3-- | ** *************************** | --dispatch_n |,

這使得同步主隊列如此繁忙以致最終導致任務失敗。

相關問題