GCD串行队列似乎没有按顺序执行。

7

我有一个方法,在我的代码中有时会被调用。下面是一个非常基本的例子,因为该代码处理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内枚举第二个队列中的对象引起的呢?

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

虽然枚举组可能仍在运行,但由于队列在枚举组完成工作之前已经到达块的末尾,因此processImages实际上并未完成。我认为这似乎是一种可能性?


我想知道你是否意外地多次调用了 dispatch_queue_create。也许在那里放一个 NSLog 语句。你的期望是正确的(它应该是严格串行的),但还有其他简单的事情发生了。 - Rob
没有更多上下文的情况下很难回答,但是你的假设是正确的。我想知道你是否正在使用具有自己队列的多个对象。确保你的队列在所有对象之间共享。尝试在你的NSLogs中打印当前线程和serialQueue的值。 - Fernando Mazzon
你能否在 dispatch_async 调用处设置一个断点,并验证每次调用 processImagesserialQueue 是相同的对象。听起来你的队列正在被重新创建。 - Tom Redman
我在代码块中添加了更多的上下文信息...我不认为我的操作会引起问题,但你永远也不知道。 - inks2002
你要么创建多个队列,要么不创建串行队列。你能分享一下你创建队列的实际代码吗? - Carl Veazey
请参见上面的附加上下文... - inks2002
7个回答

5

串行队列绝对会按顺序执行,但不保证在同一线程上执行。

假设您正在使用相同的串行队列,则问题在于当来自不同线程的 NSLog 几乎同时调用时,它不能保证以正确的顺序输出结果。

以下是一个示例:

  1. SQ 在 X 线程上运行,发送“正在处理图像”
  2. 日志输出“正在处”
  3. SQ 在 X 线程上运行,发送“已完成处理图像”
  4. SQ 在 Y 线程上运行,发送“正在处理图像”
  5. 日志输出“essImages\n”

在第 5 步之后,NSLog 不一定知道要打印第 3 步还是第 4 步。

如果您绝对需要时间有序的日志记录,则需要为日志记录专门创建一个队列。实际上,我只使用主队列而没有遇到任何问题:

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

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

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

从文档中可以看到:

此方法是异步的。当枚举组时,可能会要求用户确认应用程序对数据的访问权限;但是,该方法立即返回。您应该在枚举块中处理所需的任何工作。

我不确定您使用串行队列想要实现什么,但如果您只想防止同时访问,则可以添加一个变量来跟踪当前是否正在枚举,然后首先检查该变量,如果您不必担心同步问题。 (如果需要,请考虑使用GCD组,但这可能过于复杂。)


我使用串行队列/GCD的原因是,如果不使用它,在枚举图片时会锁定UI。在该块中进行了大量的图像处理,包括调整大小和面部识别。当手机首次加载应用程序时,它会调用此方法。该方法可以在后台调用。有时这些事件可能会发生冲突,这就是为什么我想创建一个串行队列的原因,以便如果事件启动,然后在处理事件时再次调用,我可以将其放入串行队列中以在完成后执行。 - inks2002
目前我使用一个BOOL变量来确定我们是否正在枚举。我认为使用串行队列会更好,但看起来并不是这样。请参见我原始帖子中“更新2:我认为正在发生的事情”下的原因。 - inks2002

0
我遇到了这样的问题,对我来说答案是意识到从序列化队列上的方法发起的异步调用会进入另一个队列进行处理——这个队列不是序列化的。
因此,您必须在主方法中将所有调用都包装在显式的dispatch_async(serializedQueue, ^{})中,以确保一切按正确的顺序完成...

0
我认为一个串行队列会等待第一个块完成...它确实会这样做。但你的第一个块只是调用enumerateGroupsWithTypes方法,文档警告我们该方法是异步运行的:
“此方法是异步的。当枚举组时,用户可能会被要求确认应用程序对数据的访问;但是,该方法会立即返回。”
(顺便说一下,每当您看到一个带有块/闭包参数的方法时,这表明该方法很可能在执行某些异步操作。您可以随时参考相关方法的文档并确认,就像我们在这里所做的那样。)
因此,归根结底,您的队列确实是串行的,但它仅按顺序启动一系列异步任务,显然不等待这些异步任务完成,从而违反了串行队列的目的。
因此,如果您确实需要每个任务等待前一个异步任务完成,那么有许多传统解决方案可以解决此问题:
  1. 使用递归模式。即编写一个版本的processImage,它接受要处理的图像数组,并:

    • 检查是否有任何要处理的图像;
    • 处理第一张图像;和
    • 完成后(即在完成处理程序块中),从数组中删除第一张图像,然后再次调用processImage
  2. 考虑使用操作队列而不是调度队列。然后,您可以将任务实现为“异步”NSOperation子类。这是一种非常优雅的包装异步任务的方式,如https://dev59.com/t2Ei5IYBdhLWcg3whckh#21205992所示。

  3. 您可以使用信号量使此异步任务表现得同步。这也在https://dev59.com/t2Ei5IYBdhLWcg3whckh#21205992中说明。

选项1是最简单的,选项2是最优雅的,选项3是一个脆弱的解决方案,如果可以的话应该避免使用。


0
如果问题是“串行队列能异步执行任务吗?”那么答案是否定的。 如果你认为它可以,你应该确保所有任务都在同一个队列上真正执行。你可以在代码块中添加以下一行并比较输出结果:
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");
});

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

0

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

假设:有一个具有异步“运行”方法的类,将同时在多个对象上运行,目标是每个对象都不会在前一个对象完成之前运行。

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

因此,想法是:如果使用串行队列,则只有一个对象会一次运行,一个接一个地运行。

在类中的全局空间创建一个串行队列:

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方法就已经退出了,接下来队列中的下一个run方法会运行。所以目标没有被实现,因为每个异步方法都是并行运行的,而不是串行的。
使用信号量进行修复。在类中声明。
let running = DispatchSemaphore(value: 0)

现在异步方法完成后,它调用了“completed”方法:

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()被completed方法发出信号,进而阻止串行队列启动队列中的下一个run方法。这样一来,异步方法链确实是串行运行的。

因此,现在该类的形式为:

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

-2

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接