GCD串行队列似乎没有串行执行

Posted

技术标签:

【中文标题】GCD串行队列似乎没有串行执行【英文标题】:GCD serial queue does not seem to execute serially 【发布时间】:2013-03-15 03:46:01 【问题描述】:

我有一个方法,有时可以在我的代码中调用。下面是一个非常基本的示例,因为代码处理了 iphone 照片库中的图像和文件,并在使用该方法完成时将它们标记为已处理。

@property (nonatomic, assign) dispatch_queue_t serialQueue;

....

-(void)processImages

    dispatch_async(self.serialQueue, ^
        //block to process images
        NSLog(@"In processImages");

        ....

        NSLog(@"Done with processImages");
    );

我认为每次调用此方法时,我都会得到以下输出... “正在处理图像” “完成处理图像” “正在处理图像” “完成处理图像” 等等……

但我总是得到

“处理中的图像” “正在处理图像” “完成处理图像” “完成处理图像” 等等……

我认为串行队列会等到第一个块完成,然后开始。对我来说,它似乎正在启动该方法,然后再次调用它并在第一次调用完成之前启动,创建通常不会被处理的图像副本,因为如果它真的连续执行,该方法会知道它们已经被处理了。也许我对串行队列的理解并不具体。有输入吗?谢谢你。

编辑:下面的更多上下文,这就是块中发生的事情......这会导致问题吗???

@property (nonatomic, assign) dispatch_queue_t serialQueue;

....

-(void)processImages

    dispatch_async(self.serialQueue, ^
        //library is a reference to ALAssetsLibrary object 

        [library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos usingBlock:^(ALAssetsGroup *group, BOOL *stop)
        
            [group enumerateAssetsUsingBlock:^(ALAsset *asset, NSUInteger index, BOOL *stop)
            
             ....
             //Process the photos here
            ];
        failureBlock:^(NSError *error)  NSLog(@"Error loading images from library");
        ];

    );


-(id)init

    self = [super init];
    if(self)
    
        _serialQueue = dispatch_queue_create("com.image.queue",NULL);
    
    return self;

这个对象只创建一次,据我所知,根据我的代码永远不会再次创建...我将运行测试以确保。

更新 2:我认为正在发生的事情,如果您同意/不同意,请对此发表评论......

显然,我的主要问题是,这块代码似乎是同时执行的,创建了重复的条目(导入同一张照片两次),而如果它是连续运行的,通常不会这样做。处理照片时,会对其应用“脏”位,以确保下次调用该方法时会跳过该图像,但这不会发生,并且某些图像会被处理两次。这可能是因为我使用 enumerategroupswithtypes: 在那个 serialQueue 中枚举第二个队列中的对象吗?

    调用 processImages 枚举对象 立即从 enumerateObjects 返回,因为它本身是异步的 结束对 processImages 的调用

processImages 并没有真正完成,因为 enumerategroups 可能仍在运行,但队列可能会完成它,因为它在 enumerategroups 完成工作之前到达块的末尾。这对我来说似乎是一种可能性?

【问题讨论】:

我想知道您是否不小心多次致电dispatch_queue_create。也许在那里放一个NSLog 声明。您的期望是正确的(它应该是严格连续的),但还有其他简单的事情发生。 没有更多上下文很难回答,但您的假设是正确的。我想知道您是否正在使用具有自己队列的多个对象。确保您的队列是所有队列的共享。尝试在 NSLogs 中打印当前线程和 serialQueue 的值 您能否在dispatch_async 调用处设置断点,并在每次调用processImages 时验证serialQueue 是同一个对象。听起来您的队列正在重新创建。 我为块中发生的事情添加了更多上下文...我不认为我正在做的事情会导致问题,但你永远不会知道 您要么创建多个队列,要么不创建串行队列。你能分享你创建队列的实际代码吗? 【参考方案1】:

串行队列绝对会串行执行。但是,它们不能保证在同一个线程上执行。

假设您使用相同的串行队列,问题是当从不同线程同时调用时,NSLog 不能保证以正确的顺序输出结果。

这是一个例子:

    SQ 在线程 X 上运行,发送“In processImages” 日志打印“In proc” 线程 X 上的 SQ,发送“Done with processImages” SQ 在线程 Y 上运行,发送“In processImages” 日志打印“essImages\n”

5. 之后,NSLog 不一定知道打印哪个,3. 还是 4.

如果您绝对需要按时间排序的日志记录,则需要一个专用的日志记录队列。在实践中,我只使用主队列没有问题:

dispatch_async(dispatch_get_main_queue(), ^
    NSLog(@"whatever");
);

如果所有的 NSlog 调用都在同一个队列上,你就不应该有这个问题。

【讨论】:

【参考方案2】:

enumerateGroupsWithTypes:usingBlock:failureBlock: 在另一个线程上异步工作,并在完成后调用传入的块(我认为是在主线程上)。从另一个角度来看,如果它在方法调用完成时同步完成了所有操作,则它可以只返回组的枚举器对象,例如,对于更简单的 API。

来自文档:

此方法是异步的。枚举组时,可能会要求用户确认应用程序对数据的访问;但是,该方法会立即返回。你应该对 enumerationBlock 中的资产执行任何你想做的工作。

我不确定您为什么要尝试使用串行队列来完成,但如果您只是想防止同时访问,那么您可以在某处添加一个变量来跟踪我们当前是否正在枚举或如果您不必担心同步问题,请先检查一下。 (如果你这样做了,也许你应该考虑使用 GCD 组,但在这种情况下它可能有点矫枉过正。)

【讨论】:

我之所以使用串行队列/GCD,是因为没有它,枚举图片时确实会锁定UI。在该块内,从调整大小到面部识别,正在进行大量的图像处理。当手机首次加载应用程序时,它会调用此方法。在后台可以调用该方法。有时这些事件可能会发生冲突,这就是我想创建一个串行队列的原因,这样如果事件开始,然后在处理该事件时再次调用该事件,我可以将它放在串行队列中以在完成时执行。 目前我确实使用 BOOL 变量来确定我们当前是否正在枚举。我只是认为使用串行队列会更好。但看起来不会。在我原来的帖子中的“更新 2:我认为正在发生的事情”下查看我的原因。【参考方案3】:

如果问题是“串行队列可以异步执行任务吗?”那么答案是否定的。 如果您认为可以,您应该确保所有任务都在同一个队列上真正执行。您可以在块中添加以下行并比较输出:

dispatch_async(self.serialQueue, ^
    NSLog(@"current queue:%p current thread:%@",dispatch_get_current_queue(),[NSThread currentThread]);

确保将 NSLog 写入在队列上执行的块中,而不是在 enumerateGroupsWithTypes:usingBlock:failureBlock 中: 您也可以尝试像这样创建队列

dispatch_queue_create("label", DISPATCH_QUEUE_SERIAL);

但我认为这不会改变任何事情

编辑: 顺便说一下方法

enumerateGroupsWithTypes:usingBlock:failureBlock:

是异步的,为什么要在另一个队列上调用呢?

更新 2: 我可以提出这样的建议:

dispatch_async(queue, ^
    NSLog(@"queue");

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER, *pmutex = &mutex;
    pthread_mutex_lock(pmutex);

    ALAssetsLibraryGroupsEnumerationResultsBlock listGroupBlock = ^(ALAssetsGroup *group, BOOL *stop) 
        NSLog(@"block");
        if (group) 
            [groups addObject:group];
         else 

            [self.tableView performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO];
            dispatch_async(dispatch_get_current_queue(), ^
                pthread_mutex_unlock(pmutex);
            );
        
        NSLog(@"block end");
    ;

    [assetsLibrary enumerateGroupsWithTypes:groupTypes usingBlock:listGroupBlock failureBlock:failureBlock];
    pthread_mutex_lock(pmutex);
    pthread_mutex_unlock(pmutex);
    pthread_mutex_destroy(pmutex);
    NSLog(@"queue end");
);

【讨论】:

所以你是说异步队列不能像我上面所做的那样串行运行? 串行队列将始终串行执行。您的问题出在其他地方。 谢谢,请根据您的 EDIT 声明查看我的更新 2。 是否有可能,您调用 -(void) processImages 方法两次?如果是,我猜 enumerateGroupsWithTypes 可以同时执行多次,它可能会导致重复。我刚刚在文档中找到了这一行:“枚举完成后,将调用 enumerationBlock 并将组设置为 nil。”也许你应该使用某种锁来等待枚举完成。 我肯定会调用 processImages 两次、三次或更多。这就是我想使用串行队列的主要原因,以确保它以 FIFO 顺序处理。所以我猜它可能是串行运行的,但是 enumerateGroupsWithTypes 还没有完成。【参考方案4】:

我遇到了这样的问题,我的答案是意识到来自序列化队列上的方法的异步调用会转到另一个队列进行处理——一个未序列化的队列。

因此,您必须使用显式 dispatch_async(serializedQueue, ^) 将所有调用封装在 main 方法中,以确保一切都以正确的顺序完成...

【讨论】:

【参考方案5】:

使用 Swift 和信号量来说明一种序列化方法:

给定:一个具有异步“运行”方法的类,该方法将一次在多个对象上运行,目标是每个对象在完成之前不会运行。

问题在于 run 方法分配了大量内存并使用大量系统资源,如果同时运行太多,可能会导致内存压力以及其他问题。

所以想法是:如果使用串行队列,那么一次只会运行一个,一个接一个。

通过类在全局空间创建串行队列:

let serialGeneratorQueue: DispatchQueue = DispatchQueue(label: "com.limit-point.serialGeneratorQueue", autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency.workItem)

class Generator 

    func run() 
         asynchronous_method()
    

    func start() 

        serialGeneratorQueue.async 
            self.run()
        
    

    func completed() 
       // to be called by the asynchronous_method() when done
    

这个类的'run'方法将被创建和运行很多对象将在串行队列上处理:

serialGeneratorQueue.async 
    self.run()

在这种情况下,autoreleaseFrequency 是 .workItem,用于在每次运行后清理内存。

run 方法有一些通用的形式:

func run() 
   asynchronous_method()

这样做的问题是:run方法在asynchronous_method完成之前就退出了,队列中的下一个run方法会运行等等。所以没有达到目的,因为每个asynchronous_method都是并行运行的,毕竟不是串行的。

使用信号量来修复。在类中声明

let running = DispatchSemaphore(value: 0)

现在异步方法完成它调用“完成”方法:

func completed() 
   // some cleanup work etc.

信号量可用于序列化异步方法链,方法是在“run”方法中添加“running.wait()”:

func run() 
    asynchronous_method()

    running.wait() 

然后在completed()方法中添加‘running.signal()’

func completed() 
   // some cleanup work etc.

    running.signal()

'run' 中的 running.wait() 将阻止它退出,直到使用 running.signal() 的已完成方法发出信号,这反过来又阻止串行队列启动队列中的下一个运行方法。这样,异步方法链确实会串行运行。

所以现在这个类的形式是:

class Generator 

    let running = DispatchSemaphore(value: 0)

    func run() 
         asynchronous_method()

         running.wait() 
    

    func start() 

        serialGeneratorQueue.async 
            self.run()
        
    

    func completed() 
       // to be called by the asynchronous_method() when done

       running.signal()
    

【讨论】:

【参考方案6】:

我认为串行队列会等待 [直到] 第一个块完成...

确实如此。但是您的第一个块只是调用 enumerateGroupsWithTypes 和 the documentation 警告我们该方法异步运行:

此方法是异步的。枚举组时,可能会要求用户确认应用程序对数据的访问;但是,该方法会立即返回。

(FWIW,每当您看到具有块/闭包参数的方法时,这是一个危险信号,表明该方法可能正在异步执行某些操作。您可以随时参考相关方法的文档并确认,就像我们在这里一样。)

所以,底线,你的队列串行的,但它只是顺序启动一系列异步任务,但显然不等待那些异步任务完成,违背串行队列的意图.

所以,如果你真的需要让每个任务等待前面的异步任务,这个问题有很多传统的解决方案:

    使用递归模式。即,编写一个 processImage 的演绎版,它需要一个图像数组来处理,并且:

    检查是否有要处理的图像; 处理第一张图片;和 完成后(即在完成处理程序块中),从数组中删除第一个图像,然后再次调用 processImage

    考虑使用operation queues,而不是调度队列。然后您可以将您的任务实现为“异步”NSOperation 子类。这是包装异步任务的一种非常优雅的方式。https://***.com/a/21205992/1271826 对此进行了说明。

    您可以使用信号量使此异步任务同步运行。 https://***.com/a/21205992/1271826 也说明了这一点。

选项 1 是最简单的,选项 2 是最优雅的,选项 3 是一个脆弱的解决方案,应该尽可能避免。

【讨论】:

【参考方案7】:

您可能有多个对象,每个对象都有自己的串行队列。分派到任何单个串行队列的任务都是按顺序执行的,但是分派到不同串行队列的任务绝对是交错的。

另一个简单的错误是创建的不是串行队列,而是并发队列...

【讨论】:

以上是关于GCD串行队列似乎没有串行执行的主要内容,如果未能解决你的问题,请参考以下文章

GCD 中的并发队列与串行队列

GCD使用 串行并行队列 与 同步异步执行的各种组合 及要点分析

ios多线程同步异步、串行并行队列、死锁

IOS多线程知识总结/队列概念/GCD/串行/并行/同步/异步

iOS多线程知识总结/队列概念/GCD/串行/并行/同步/异步

同步,异步,串行队列,并发队列,全局队列,主队列等概念的总结