什么是gcd


概述html

我将分四步来带你们研究研究程序的并发计算。并发

第一步是主要的串行程序,而后使用GCD把它并行计算化。假设你想顺着步骤来尝试这些程序的话,可以下载源代码。async

注意。别执行imagegcd2.m,这是个反面教材。。函数

  imagegcd.zip (8.4 KB, 79 次)post

 

原始程序ui

咱们的程序仅仅是简单地遍历~/Pictures而后生成缩略图。这个程序是个命令行程序,没有图形界面(虽然是使用Cocoa开发库的),主函数例如如下:编码

    int main(int argc, char **argv)
    {
        NSAutoreleasePool *outerPool = [NSAutoreleasePool new];
        
        NSApplicationLoad();
        
        NSString *destination = @"/tmp/imagegcd";
        [[NSFileManager defaultManager] removeItemAtPath: destination error: NULL];
        [[NSFileManager defaultManager] createDirectoryAtPath: destination
                                        withIntermediateDirectories: YES
                                        attributes: nil
                                        error: NULL];
        
        
        Start();
        
        NSString *dir = [@"~/Pictures" stringByExpandingTildeInPath];
        NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath: dir];
        int count = 0;
        for(NSString *path in enumerator)
        {
            NSAutoreleasePool *innerPool = [NSAutoreleasePool new];
            
            if([[[path pathExtension] lowercaseString] isEqual: @"jpg"])
            {
                path = [dir stringByAppendingPathComponent: path];
                
                NSData *data = [NSData dataWithContentsOfFile: path];
                if(data)
                {
                    NSData *thumbnailData = ThumbnailDataForData(data);
                    if(thumbnailData)
                    {
                        NSString *thumbnailName = [NSString stringWithFormat: @"%d.jpg", count++];
                        NSString *thumbnailPath = [destination stringByAppendingPathComponent: thumbnailName];
                        [thumbnailData writeToFile: thumbnailPath atomically: NO];
                    }
                }
            }
            
            [innerPool release];
        }
        
        End();
        
        [outerPool release];
    }
 

假设你要看到所有的副主函数的话。到文章顶部下载源代码吧。当前这个程序是imagegcd1.m。atom

程序中重要的部分都在这里了。. Start 函数和 End 函数仅仅是简单的计时函数(内部实现是使用的gettimeofday函数)。ThumbnailDataForData函数使用Cocoa库来载入图片数据生成Image对象。而后将图片缩小到320×320大小,最后将其编码为JPEG格式。spa

 

简单而天真的并发操作系统

乍一看,咱们感受将这个程序并发计算化,很是easy。

循环中的每个迭代器都可以放入GCD global queue中。咱们可以使用dispatch queue来等待它们完毕。为了保证每次迭代都会获得惟一的文件名称数字,咱们使用OSAtomicIncrement32来原子操做级别的添加count数:

    dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
    dispatch_group_t group = dispatch_group_create();
    __block uint32_t count = -1;
    for(NSString *path in enumerator)
    {
        dispatch_group_async(group, globalQueue, BlockWithAutoreleasePool(^{
            if([[[path pathExtension] lowercaseString] isEqual: @"jpg"])
            {
                NSString *fullPath = [dir stringByAppendingPathComponent: path];
                
                NSData *data = [NSData dataWithContentsOfFile: fullPath];
                if(data)
                {
                    NSData *thumbnailData = ThumbnailDataForData(data);
                    if(thumbnailData)
                    {
                        NSString *thumbnailName = [NSString stringWithFormat: @"%d.jpg",
                                                   OSAtomicIncrement32(&count;)];
                        NSString *thumbnailPath = [destination stringByAppendingPathComponent: thumbnailName];
                        [thumbnailData writeToFile: thumbnailPath atomically: NO];
                    }
                }
            }
        });
    }
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

这个就是imagegcd2.m,但是。注意,别执行这个程序。有很是大的问题。

 

假设你无视个人警告仍是执行这个imagegcd2.m了,你现在很是有多是在从新启动了电脑后。又打开了个人页面。

。假设你乖乖地没有执行这个程序的话。执行这个程序发生的状况就是(假设你有很是多很是多图片在~/Pictures中):电脑没反应。很久很久都不动。假死了。。

 

问题在哪

问题出在哪?就在于GCD的智能上。GCD将任务放到全局线程池中执行,这个线程池的大小依据系统负载来随时改变。

好比,个人电脑有四核,因此假设我使用GCD载入任务,GCD会为我每个cpu核建立一个线程,也就是四个线程。假设电脑上其它任务需要进行的话,GCD会下降线程数来使其它任务得以占用cpu资源来完毕。

但是。GCD也可以添加活动线程数。它会在其它某个线程堵塞时添加活动线程数。

假设现在有四个线程正在执行,忽然某个线程要作一个操做,比方。读文件,这个线程就会等待磁盘响应。此时cpu核心会处于未充分利用的状态。

这是GCD就会发现这个状态,而后建立还有一个线程来填补这个资源浪费空缺。

现在,想一想上面的程序发生了啥?主线程很是迅速地将任务不断放入global queue中。

GCD以一个少许工做线程的状态開始,而后開始执行任务。

这些任务执行了一些很是轻量的工做后。就開始等待磁盘资源。慢得不像话的磁盘资源。

咱们别忘记磁盘资源的特性,除非你使用的是SSD或者牛逼的RAID。不然磁盘资源会在竞争的时候变得异常的慢。

刚開始的四个任务很是轻松地就同一时候訪问到了磁盘资源。而后開始等待磁盘资源返回。这时GCD发现CPU開始空暇了。它继续添加工做线程。而后,这些线程执行不少其它的磁盘读取任务。而后GCD再建立不少其它的工资线程。。

可能在某个时间文件读取任务有完毕的了。现在,线程池中可不止有四个线程,相反,有成百上千个。。。GCD又会尝试将工做线程下降(太多使用CPU资源的线程),但是下降线程是由条件的。GCD不可以将一个正在执行任务的线程杀掉,并且也不能将这种任务暂停。它必须等待这个任务完毕。所有这些状况都致使GCD没法下降工做线程数。

而后所有这上百个线程開始一个个完毕了他们的磁盘读取工做。它们開始竞争CPU资源,固然CPU在处理竞争上比磁盘先进多了。

问题在于。这些线程读完文件后開始编码这些图片,假设你有很是多很是多图片。那么你的内存将開始爆仓。。而后内存耗尽咋办?虚拟内存啊,虚拟内存是啥。磁盘资源啊。Oh shit!~

而后进入了一个恶性循环,磁盘资源竞争致使不少其它的线程被建立,这些线程致使不少其它的内存使用,而后内存爆仓致使虚拟内存交换。直至GCD建立了系统规定的线程数上限(多是512个),而这些线程又无法被杀掉或暂停。

这就是使用GCD时,要注意的。

GCD能智能地依据CPU状况来调整工做线程数。但是它却没法监视其它类型的资源情况。

假设你的任务牵涉大量IO或者其它会致使线程block的东西,你需要把握好这个问题。

 

修正
问题的根源来自于磁盘IO,而后致使恶性循环。攻克了磁盘资源碰撞,就攻克了这个问题。

GCD的custom queue使得这个问题易于解决。Custom queue是串行的。假设咱们建立一个custom queue而后将所有的文件读写任务放入这个队列,磁盘资源的同一时候訪问数会大大下降,资源訪问碰撞就避免了。

虾米是咱们修正后的代码。使用IO queue(也就是咱们建立的custom queue专门用来读写磁盘):

    dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t ioQueue = dispatch_queue_create("com.mikeash.imagegcd.io", NULL);
    dispatch_group_t group = dispatch_group_create();
    __block uint32_t count = -1;
    for(NSString *path in enumerator)
    {
        if([[[path pathExtension] lowercaseString] isEqual: @"jpg"])
        {
            NSString *fullPath = [dir stringByAppendingPathComponent: path];
            
            dispatch_group_async(group, ioQueue, BlockWithAutoreleasePool(^{
                NSData *data = [NSData dataWithContentsOfFile: fullPath];
                if(data)
                    dispatch_group_async(group, globalQueue, BlockWithAutoreleasePool(^{
                        NSData *thumbnailData = ThumbnailDataForData(data);
                        if(thumbnailData)
                        {
                            NSString *thumbnailName = [NSString stringWithFormat: @"%d.jpg",
                                                       OSAtomicIncrement32(&count;)];
                            NSString *thumbnailPath = [destination stringByAppendingPathComponent: thumbnailName];
                            dispatch_group_async(group, ioQueue, BlockWithAutoreleasePool(^{
                                [thumbnailData writeToFile: thumbnailPath atomically: NO];
                            }));
                        }
                    }));
            }));
        }
    }
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

 这个就是咱们的 imagegcd3.m.

GCD使得咱们很是easy就将任务的不一样部分放入一样的队列中去(简单地嵌套一下dispatch)。

此次咱们的程序将会表现地很是好。

。。

我是说多数状况。。

。。

问题在于任务中的不一样部分不是同步的,致使了整个程序的不稳定。咱们的新程序的整个流程例如如下:

    Main Thread          IO Queue            Concurrent Queue
    
    find paths  ------>  read  ----------->  process
                                             ...
                         write <-----------  process

图中的箭头是非堵塞的,并且会简单地将内存中的对象进行缓冲。

 

 现在假设一个机器的磁盘足够快,快到比CPU处理任务(也就是图片处理)要快。事实上不难想象:虽然CPU的动做很是快,但是它的工做更繁重。解码、压缩、编码。

从磁盘读取的数据開始填满IO queue,数据会占用内存。很是可能越占越多(假设你的~/Pictures中有很是多很是多图片的话)。

而后你就会内存爆仓,而后開始虚拟内存交换。

。。又来了。。

这就会像第一次同样致使恶性循环。一旦不论什么东西致使工做线程堵塞,GCD就会建立不少其它的线程,这个线程执行的任务又会占用内存(从磁盘读取的数据),而后又開始交换内存。

结果:这个程序要么就是执行地很是顺畅。要么就是很是低效。

注意假设磁盘速度比較慢的话,这个问题依然会出现。因为缩略图会被缓冲在内存里,只是这个问题致使的低效比較不easy出现。因为缩略图占的内存少得多。

 

真正的修复

因为上一次咱们的尝试出现的问题在于没有同步不一样部分的操做,因此让我写出同步的代码。最简单的方法就是使用信号量来限制同一时候执行的任务数量。

那么,咱们需要限制为多少呢?

显然咱们需要依据CPU的核数来限制这个量,咱们又想马儿好又想马儿不吃草,咱们就设置为cpu核数的两倍吧。只是这里仅仅是简单地这样处理,GCD的做用之中的一个就是让咱们不用关心操做系统的内部信息(比方cpu数)。现在又来读取cpu核数,确实不太妙。或许咱们在实际应用中。可以依据其它需求来定义这个限制量。

现在咱们的主循环代码就是这样了:

    dispatch_queue_t ioQueue = dispatch_queue_create("com.mikeash.imagegcd.io", NULL);
    
    int cpuCount = [[NSProcessInfo processInfo] processorCount];
    dispatch_semaphore_t jobSemaphore = dispatch_semaphore_create(cpuCount * 2);
    
    dispatch_group_t group = dispatch_group_create();
    __block uint32_t count = -1;
    for(NSString *path in enumerator)
    {
        WithAutoreleasePool(^{
            if([[[path pathExtension] lowercaseString] isEqual: @"jpg"])
            {
                NSString *fullPath = [dir stringByAppendingPathComponent: path];
                
                dispatch_semaphore_wait(jobSemaphore, DISPATCH_TIME_FOREVER);
            
                dispatch_group_async(group, ioQueue, BlockWithAutoreleasePool(^{
                    NSData *data = [NSData dataWithContentsOfFile: fullPath];
                    dispatch_group_async(group, globalQueue, BlockWithAutoreleasePool(^{
                        NSData *thumbnailData = ThumbnailDataForData(data);
                        if(thumbnailData)
                        {
                            NSString *thumbnailName = [NSString stringWithFormat: @"%d.jpg",
                                                       OSAtomicIncrement32(&count;)];
                            NSString *thumbnailPath = [destination stringByAppendingPathComponent: thumbnailName];
                            dispatch_group_async(group, ioQueue, BlockWithAutoreleasePool(^{
                                [thumbnailData writeToFile: thumbnailPath atomically: NO];
                                dispatch_semaphore_signal(jobSemaphore);
                            }));
                        }
                        else
                            dispatch_semaphore_signal(jobSemaphore);
                    }));
                }));
            }
        });
    }
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

终于咱们写出了一个能平滑执行且又高速处理的程序。

 

基准測试

我測试了一些执行时间。对7913张图片:

 

程序处理时间 (秒)
imagegcd1.m 984
imagegcd2.m 没执行。这个仍是别执行了
imagegcd3.m 300
imagegcd4.m 279

 

 

注意,因为我比較懒。因此我在执行这些測试的时候,没有关闭电脑上的其它程序。。

。严格的进行对比的话。实在是太蛋疼了。。

因此这个数值咱们仅仅是參考一下。

比較有意思的是,3和4的执行情况几乎相同,大概是因为我电脑有15g可用内存吧。

内存比較小的话,这个imagegcd3应该跑的很是吃力,因为我发现它使用最多的时候。占用了10g内存。

而4的话,没有占多少内存。

结论

GCD是个比較范特西的技术,可以办到很是多事儿。但是它不能为你办所有的事儿。因此。对于进行IO操做并且可能会使用大量内存的任务。咱们必须细致斟酌。

固然,即便这样,GCD仍是为咱们提供了简单有效的方法来进行并发计算。

相关文章
相关标签/搜索