`

GCD实战2:资源竞争

    博客分类:
  • ios
阅读更多

 

转自http://www.dreamingwish.com/dream-2012/gcd%E5%AE%9E%E6%88%982%EF%BC%9A%E8%B5%84%E6%BA%90%E7%AB%9E%E4%BA%89.html

概述

我将分四步来带大家研究研究程序的并发计算。第一步是基本的串行程序,然后使用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格式。

 

简单而天真的并发

乍一看,我们感觉将这个程序并发计算化,很容易。循环中的每个迭代器都可以放入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使得我们很容易就将任务的不同部分放入相同的队列中去(简单地嵌套一下dispatch)。这次我们的程序将会表现地很好。。。我是说多数情况。。。。

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

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

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

 

 现在假设一个机器的磁盘足够快,快到比CPU处理任务(也就是图片处理)要快。其实不难想象:虽然CPU的动作很快,但是它的工作更繁重,解码、压缩、编码。从磁盘读取的数据开始填满IO queue,数据会占用内存,很可能越占越多(如果你的~/Pictures中有很多很多图片的话)。

然后你就会内存爆仓,然后开始虚拟内存交换。。。又来了。。

这就会像第一次一样导致恶性循环。一旦任何东西导致工作线程阻塞,GCD就会创建更多的线程,这个线程执行的任务又会占用内存(从磁盘读取的数据),然后又开始交换内存。。

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

注意如果磁盘速度比较慢的话,这个问题依旧会出现,因为缩略图会被缓冲在内存里,不过这个问题导致的低效比较不容易出现,因为缩略图占的内存少得多。

 

真正的修复

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

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

显然我们需要根据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还是为我们提供了简单有效的方法来进行并发计算。

分享到:
评论

相关推荐

    GCD实战二:资源竞争 - 51CTO.COM1

    GCD实战二:资源竞争 - 51CTO.COM1

    GCD实战一:使用串行队列实现简单的预加载 - 51CTO.COM1

    GCD实战一:使用串行队列实现简单的预加载 - 51CTO.COM1

    imagegcd实例源码

    imagegcd示例源码,GCD实战2:资源竞争

    GCD资源竞争Demo

    GCD资源竞争Demo。注意,别运行imagegcd2.m,这是个反面教材。

    Grand Central Dispatch(GCD)介绍要点难点案例分享代码解析

    功能:GCD提供了一个易于使用的、基于任务的并发模型,可以将任务提交给系统,系统会自动管理线程的创建、销毁和调度,以实现最佳的性能和资源利用率。 应用:在iOS和macOS等苹果操作系统中广泛应用于多线程编程,可...

    Coursera_GCD_Project:获取和清理数据项目

    标题 作者 日期 输出 GettingAndCleaningData_Project_ReadMe 道格·奥尔森(Doug Olson) 2015年6月20日 ... 接下来是我的run_analysis.R脚本的演练,并添加了dim()和str()的一些额外实例(希望如此)有助于澄清...

    GCD_CP:Coursera 获取和清理数据 - 课程项目

    获取和清理数据 - 编程作业 自述文件 获取和清理数据:课程项目 这个自述文件解释了如何使用和运行 run_analysis.R 脚本 ##目的和数据来源 该脚本是为 Coursera“获取和清理数据”课程项目编写的,该项目使用从三星 ...

    Poly_GCD_POWER_6_July2005.zip_gcd_gcd for polynomial_gcd matlab_

    If we need to verify the fact that a Polynomial has...a) A function to compute the GCD of 2 Polynomials. Since I could not find a Standard Matlab function for this, I created this function : Poly_GCD.m :

    gcd-project:获取和清理数据课程项目

    gcd-项目获取和清理数据课程项目gcd-project 存储库包括以下文件: 'README.md' '密码本.md' 'run_analysis.R' '整理数据.txt' 笔记 : Run_analysis 脚本位于“UCI HAR 数据集”目录中,该目录还包含训练和测试...

    DS-GCD-CourseProject:课程项目

    DS-GCD课程计划该项目的目的是演示如何从给定的数据集中准备整齐的数据集。 对于此项目,使用了现有数据集,该数据集是从Samsung Galaxy S智能手机的加速度计中收集的。 有关此数据的完整描述,请参见。 用于项目的...

    GCD_CourseProject:课程项目,获取和清理数据

    GCD_课程项目获取和清理数据课程项目###Repo 此 Repo 包含以下文件和目录: 输入原始数据: UCI HAR 数据集,来源: ://d396qusza40orc.cloudfront.net/getdata/projectfiles/UCI HAR Dataset.zip 。 完整说明可在...

    GCD_Project:用于获取和清理数据项目

    标题 作者 日期 输出 自述文件 赛阿斯兰 2015 年 1 月 25 日,星期日 ... 这是“获取和清理数据课程项目工作”的提交。 该提交仅包含一个名为 run_analysis.R 的脚本。... 脚本中名为 run_analysis 的这个函数只接受一个...

    GCD_CourseProject:获取和清理数据课程项目

    GCD_课程项目 获取和清理数据课程项目文件包括: 包含在 repo 中的 README.md 文件 CodeBook.md 描述变量、数据以及为清理数据而执行的任何转换或工作的代码簿 run_analysis.R 脚本 ================== 运行脚本 a)...

    gcd-django:大漫画数据库网站

    GCD Web应用程序 该文件的最新更新: 2020年8月 这是Grand Comics数据库的当前实现,托管在 (生产)和 (新功能的公共Beta版)上。 请参阅和的“以获取有关我们的工作方式和迄今为止所做的工作的更多详细信息。 该...

    GCD_CourseProjectPasBesoin:获取和清理数据课程的课程项目

    GCD_CourseProjectPasBesoin 获取和清理数据课程的课程项目 准备 从下载数据集并解压; 把它放在 repo 的顶部。 它应该被称为“UCI HAR 数据集”。 这个目录会被 Git 忽略,不会被添加到 repo 中。 跑步 只有一种...

    gcd_proj:Coursera 课程“获取和清理数据”

    gcd_proj Coursera 课程“Getting and Cleaning Data”:课程项目介绍目前所有数据科学中最令人兴奋的领域之一是可穿戴计算。 Fitbit、Nike 和 Jawbone Up 等公司正在竞相开发最先进的算法来吸引新用户。 从课程网站...

    GCD_CourseProject:获取和清理数据

    获取和清理数据:课程项目:自述文件 由 Manoj Chandrasekar 提交 ...第2步: 加载并合并基础数据集(测试和训练)。 testData &lt;- read.table("./UCI HAR Dataset/test/x_test.txt", header=FALSE, c

    GCD_CourseProject:课程项目作业文件

    GCD_课程项目##课程项目分配文件###run_analysis.R 的自述文件这个 README.md 的目的是逐步解释 run_analysis.R 文件是如何工作的*在开始时,代码正在读取相关的原始数据 - 数据路径以相对路径的形式提供,因此为了...

    GCD_Project:获取和清理数据课程项目回购

    GCD_项目 获取和清理数据课程项目回购 这个脚本是如何工作的…… 首先,从您的工作目录中,您需要下载并提取以下数据集: : 有了工作目录中的数据,您就可以按如下方式使用 run_analysis.R。 在相应的工作目录中...

Global site tag (gtag.js) - Google Analytics