• 什么是gcd


    版权声明:本文为博主原创文章。未经博主同意不得转载。

    https://blog.csdn.net/coolwu123/article/details/28615029


    概述

    我将分四步来带大家研究研究程序的并发计算。第一步是主要的串行程序,然后使用GCD把它并行计算化。假设你想顺着步骤来尝试这些程序的话,能够下载源代码。注意。别执行imagegcd2.m,这是个反面教材。

      imagegcd.zip (8.4 KB, 79 次)

     

    原始程序

    我们的程序仅仅是简单地遍历~/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。

    程序中重要的部分都在这里了。. Start 函数和 End 函数仅仅是简单的计时函数(内部实现是使用的gettimeofday函数)。

    ThumbnailDataForData函数使用Cocoa库来载入图片数据生成Image对象。然后将图片缩小到320×320大小。最后将其编码为JPEG格式。

     

    简单而天真的并发

    乍一看。我们感觉将这个程序并发计算化,非常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还是为我们提供了简单有效的方法来进行并发计算。

  • 相关阅读:
    9、SpringMVC:Ajax技术(扩展:百度搜索框提示应用)
    8、SpringMVC:整合SSM
    SpringMVC:多视图解析器配置以及问题
    7、SpringMVC:JSON讲解
    IDEA中lombok的插件配置及使用
    C语言编程练习23:美丽数
    C语言编程练习45:ACM CLUB晚会
    C语言编程练习22:2^x mod n = 1
    C语言编程练习21:找规律填数字
    C语言编程练习20:计算合并
  • 原文地址:https://www.cnblogs.com/ldxsuanfa/p/10555387.html
  • Copyright © 2020-2023  润新知