理解iOS中的线程池


在GCD和NSOperationQueue之前,iOS使用线程一般是用NSThread,而NSThread是对POSIX thread的封装,也就是pthread,本文最后会面附上一段使用pthread下图片的代码,现在我们还是继续上面的讨论。使用NSThread的一个最大的问题是:直接操纵线程,线程的生命周期完全交给developer控制,在大的工程中,模块间相互独立,假如A模块并发了8条线程,B模块需要并发6条线程,以此类推,线程数量会持续增长,最终会导致难以控制的结果。

GCD和NSOperationQueue出来以后,developer可以不直接操纵线程,而是将所要执行的任务封装成一个unit丢给线程池去处理,线程池会有效管理线程的并发,控制线程的生命周期。因此,现在如果考虑到并发场景,基本上是围绕着GCD和NSOperationQueue来展开讨论。GCD是一种轻量的基于block的线程模型,使用GCD一般要注意两点:一是线程的priority,二是对象间的循环引用问题。NSOperationQueue是对GCD更上一层的封装,它对线程的控制更好一些,但是用起来也麻烦一些。关于这两个孰优熟劣,需要根据具体应用场景进行讨论:stackoverflow:GCD vs NSopeartionQueue

我们后面会以下载图片为例,首先来分析在非并发的情况下NSOperationQueue和GCD的用法和特性,然后分析在并发的情况下讨论NSOperationQueue对线程的管理。

测试图片来自这里

异步下载

先从NSOperationQueue最简单的用法开始:

_opQueue = [[NSOperationQueue alloc]init];
[_opQueue addOperationWithBlock:^{
        NSData* data = [NSData dataWithContentsOfURL:[NSURL URLWithString:url3]];
        //_imgv3.image = [UIImage imageWithData:data];

        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            _imgv3.image = [UIImage imageWithData:data];
        }];
    }];

这种写法和使用GCD相比没任何优势,使用GCD代码写起来还更顺手:

- (void)downloadWithGCD
{
    dispatch_async(_gcdQueue, ^{
        NSData* data = [NSData dataWithContentsOfURL:[NSURL URLWithString:url4]];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imgv4.image = [UIImage imageWithData:data];
             NSLog(@"done downloading 3rdimage ");
        });
    });
}

线程同步

接下来我们再对比一下GCD和NSOperation对线程的控制性,假设我们有两张图要下载,第二张要在第一张完成后再去下载,显然这是一个线程同步的问题,我们先用NSOperation来实现

- (void)downloadWithNSOperationDependency
{
    ETOperation* op1 = [ETOperation new];
    op1.url = [NSURL URLWithString:url3];
    __weak ETOperation* _op1 = op1;
    [op1 setCompletionBlock:^{
        _imgv3.image = _op1.image;
    }];
    [op1 start];
    
    ETOperation* op2 = [ETOperation new];
    op2.url = [NSURL URLWithString:url4];
    __weak ETOperation* _op2 = op2;
    [op2 setCompletionBlock:^{
        _imgv4.image = _op2.image;
    }];
    [op2 addDependency:op1];
    [op2 start];
}

上述代码中op2将在op1执行完成后执行。接下来我们使用GCD来完成同样的任务,仅就上面的case来说,使用GCD有很多种方式,比如最常用的就是使用一个串行队列,当第一个下载block执行完后在启动第二个block进行下载,这种方式太过简单,这里就不做过多介绍,下面给出一种使用dispatch_group的方式,这种方式略显笨拙,但是可以展示如何使用GCD来做线程同步

- (void)downloadWithGCDGroups
{
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_async(group, queue, ^(){
        NSData* data = [NSData dataWithContentsOfURL:[NSURL URLWithString:url3]];
        dispatch_group_async(group, dispatch_get_main_queue(), ^(){
            self.imgv3.image = [UIImage imageWithData:data];
        });
    });
    // This block will run once everything above is done:
    dispatch_group_notify(group, dispatch_get_main_queue(), ^(){ 
        dispatch_async(_gcdQueue, ^{
            NSData* data = [NSData dataWithContentsOfURL:[NSURL URLWithString:url4]];
            dispatch_async(dispatch_get_main_queue(), ^{
                self.imgv4.image = [UIImage imageWithData:data];   
            });
        });
    });
}

在实际项目中,如果是两个线程之间的同步问题,我们不会书类似写上面的代码。实际上dispatch_group的作用在于控制多个线程并发,并为这些线程提供一个线程同步点,即当group内的所有线程都执行完成后,再通知外部(类似Java中线程的join操作)。因此,通常情况下,dispatch_group的用法如下:

dispatch_queue_t queue = dispatch_get_global_queue( 0, 0 );
dispatch_group_t group = dispatch_group_create();

//run task #1
dispatch_group_enter(group);
dispatch_async( queue, ^{
    NSLog( @"task 1 finished: %@", [NSThread currentThread] );
    dispatch_group_leave(group);
} );

//run task #2
dispatch_group_enter(group);
dispatch_async( queue, ^{
    NSLog( @"task 2 finished: %@", [NSThread currentThread] );
    dispatch_group_leave(group);
} );

//sychronization point
dispatch_group_notify( group, queue, ^{
    NSLog( @"all task done: %@", [NSThread currentThread] );
} );

如果考虑控制线程,相比GCD来说NSOperation是个更好的选择,它提供了很多GCD没有的高级用法:

  1. Operation之间可指定依赖关系
  2. 可指定每个Operation的优先级
  3. 可以Cancel正在执行的Operation
  4. 可以使用KVO观察对任务状态:isExecuteingisFinishedisCancelled

NSOperationQueue与线程池

下面我们在来观察并发的情况,这也是今天重点要讨论的。我们先从NSOperationQueue的并发模型开始:

这里是apple关于并发NSOperationQueue的Guideline;

总结一下,要点有这么几条:

  1. 如果要求concurrent,那么NSOperation的生命周期要自己把控
  2. 并发的operation要继承NSOperation而且必须override这几个方法:
    • startisExecutingisFinishedisConcurrent
  3. 复写isExecutingisFinished要求:
    • 线程安全
    • 手动出发kvo通知

满足这三点,就可以使用NSOperationQueue并发了,我们先按照上面的要求创建一个NSOperation

@interface MXOperation : NSOperation
{
    NSString*   _threadName;
    NSString*   _url;
    BOOL        executing;
    BOOL        finished;
}

@end

@implementation MXOperation

- (id)initWithUrl:(NSString*)url name:(NSString*)name;
{
    self = [super init];
    
    if (self) {
        if (name!=nil)
        _threadName = name;
        _url = url;
        executing = NO;
        finished = NO;
        
    }
    return self;
}

- (BOOL)isConcurrent {
    return YES;
}

- (BOOL)isExecuting {
    return executing;
}

- (BOOL)isFinished {
    return finished;
}

- (void)start
{
    [NSThread currentThread].name = _threadName;
    currentThreadInfo(@"start");
    
    if ([self isCancelled])
    {
        // Must move the operation to the finished state if it is canceled.
        [self willChangeValueForKey:@"isFinished"];
        finished = YES;
        [self didChangeValueForKey:@"isFinished"];
        return;
    }

    // If the operation is not canceled, begin executing the task.
    [self willChangeValueForKey:@"isExecuting"];

    executing = YES;
    
    //下载图片
    [NSData dataWithContentsOfURL:[NSURL URLWithString:_url]];
    
    //完成下载
    [self completeOperation];

    [self didChangeValueForKey:@"isExecuting"];
}

- (void)completeOperation {
    [self willChangeValueForKey:@"isFinished"];
    [self willChangeValueForKey:@"isExecuting"];
    
    executing = NO;
    finished = YES;
    
    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

- (void)dealloc
{
    dumpThreads(@"dealloc");
}

@end

首先我们按照要求完成了MXOperation的并发代码。其次我们在start的方法中,给当前线程增加了name,方便观察。然后使用NSData去下载图片,图片下载完成后通过KVO通知OperationQueue任务完成。最后我们在delloc的方法中,观察当前active的线程情况。

currentThreadInfodumpThreads两个工具函数,涉及到了kernel的一些API,作用是用来查看当前线程的状态:

static inline void currentThreadInfo(NSString* str)
{
    if (str)
        NSLog(@"---------%@----------",str);

    NSThread* thread = [NSThread currentThread];
    mach_port_t machTID = pthread_mach_thread_np(pthread_self());
    NSLog(@"current thread num: %x thread name:%@", machTID,thread.name);
    
    if (str)
        NSLog(@"-------------------");
}


static inline void dumpThreads(NSString* str) {
    
    NSLog(@"---------%@----------",str);
    currentThreadInfo(nil);
    char name[256];
    thread_act_array_t threads = NULL;
    mach_msg_type_number_t thread_count = 0;
    task_threads(mach_task_self(), &threads, &thread_count);
    for (mach_msg_type_number_t i = 0; i < thread_count; i++) {
        thread_t thread = threads[i];
        pthread_t pthread = pthread_from_mach_thread_np(thread);
        pthread_getname_np(pthread, name, sizeof name);
        NSLog(@"mach thread %x: getname: %s", pthread_mach_thread_np(pthread), name);
    }
    NSLog(@"-------------------");
}

然后我们来并发下载4张图片,图片大小在100kb左右:

// Do any additional setup after loading the view, typically from a nib.
   NSArray* urls = @[@"http://www.collegedj.net/wp-content/uploads/2010/10/6.jpg",
                     @"http://www.collegedj.net/wp-content/uploads/2010/10/Rihanna.jpg",
                     @"http://www.collegedj.net/wp-content/uploads/2010/10/chris-brown.jpg",
                     @"http://www.collegedj.net/wp-content/uploads/2010/10/dj_scary.jpg",
                   ];
   _queue = [NSOperationQueue new];
   for (int i=0; i<urls.count;i++) 
   {
       MXOperation* operation = [[MXOperation alloc]initWithUrl:urls[i] name:[NSString stringWithFormat:@"%d",i]];
       [_queue addOperation:operation];
   }
    

观察日志输出:

---------start----------
---------start----------
---------start----------
---------start----------
current thread num: 1403 thread name:0
current thread num: 3307 thread name:1
current thread num: 3603 thread name:2
current thread num: 3703 thread name:3
-------------------
-------------------
-------------------
-------------------
---------dealloc----------
current thread num: 1403 thread name:0
mach thread a0b: getname: 
mach thread d03: getname: 
mach thread 1403: getname: 0
mach thread 3307: getname: 1
mach thread 3603: getname: 2
mach thread 3703: getname: 3
mach thread 3f03: getname: com.apple.NSURLConnectionLoader
mach thread 4007: getname: 
mach thread 4707: getname: 
mach thread 6203: getname: 
mach thread 6303: getname: com.apple.CFSocket.private
-------------------
---------dealloc----------
current thread num: 3307 thread name:1
mach thread a0b: getname: 
mach thread d03: getname: 
mach thread 1403: getname: 0
mach thread 3307: getname: 1
mach thread 3603: getname: 2
mach thread 3703: getname: 3
mach thread 3f03: getname: com.apple.NSURLConnectionLoader
mach thread 4007: getname: 
mach thread 4707: getname: 
mach thread 6203: getname: 
mach thread 6303: getname: com.apple.CFSocket.private
-------------------
---------dealloc----------
current thread num: 3603 thread name:2
mach thread a0b: getname: 
mach thread d03: getname: 
mach thread 1403: getname: 0
mach thread 3307: getname: 1
mach thread 3603: getname: 2
mach thread 3703: getname: 3
mach thread 3f03: getname: com.apple.NSURLConnectionLoader
mach thread 4007: getname: 
mach thread 4707: getname: 
mach thread 6203: getname: 
mach thread 6303: getname: com.apple.CFSocket.private
-------------------
---------dealloc----------
current thread num: 3703 thread name:3
mach thread a0b: getname: 
mach thread d03: getname: 
mach thread 1403: getname: 0
mach thread 3307: getname: 1
mach thread 3603: getname: 2
mach thread 3703: getname: 3
mach thread 3f03: getname: com.apple.NSURLConnectionLoader
mach thread 4007: getname: 
mach thread 4707: getname: 
mach thread 6203: getname: 
mach thread 6303: getname: com.apple.CFSocket.private
-------------------

有种眼花缭乱的感觉,静下心慢慢看:

  1. 我们首先并发了4个线程:

     ---------start----------
     current thread num: 1403 thread name:0
     ---------start----------
     current thread num: 3307 thread name:1
     ---------start----------
     current thread num: 3603 thread name:2
     ---------start----------
     current thread num: 3703 thread name:3
    

线程id是系统分配的,线程名字是我们自定义的,用0-3去标识

  1. 然后,图片下载完成后,MXOperation被释放掉:

     ---------dealloc----------
     current thread num: 1403 thread name:0
     mach thread a0b: getname: 
     mach thread d03: getname: 
     mach thread 1403: getname: 0
     mach thread 3307: getname: 1
     mach thread 3603: getname: 2
     mach thread 3703: getname: 3
     mach thread 3f03: getname: com.apple.NSURLConnectionLoader
     mach thread 4007: getname: 
     mach thread 4707: getname: 
     mach thread 6203: getname: 
     mach thread 6303: getname: com.apple.CFSocket.private
    

这个时候可以看到:当前线程id1403,我们标识其为0号,同时存在的线程还有1,2,3和一些包括主线程在内,获取不到名字的线程。然后有两个是网络请求的线程。

到目前为止,结果复合预期,没什特别的地方。接着我们再下载两张图:

NSArray* urls = @[ @"http://www.collegedj.net/wp-content/uploads/2010/10/3-150x150.jpg",
                       @"http://www.collegedj.net/wp-content/uploads/2010/10/3-300x199.jpg"];
    
 for (NSString* url in urls)
 {
     MXOperation* operation = [[MXOperation alloc]initWithUrl:url name:nil];
     [_queue addOperation:operation];
 }

注意,这里并没有指定其name,我们要验证thread id,观察日志输出结果:

--------again-----------
---------start----------
---------start----------
current thread num: 1403 thread name:
current thread num: 3307 thread name:
-------------------
-------------------
---------dealloc----------
current thread num: 1403 thread name:
mach thread a0b: getname: 
mach thread d03: getname: 
mach thread 1403: getname: 
mach thread 3307: getname: 
mach thread 3603: getname: 2
mach thread 3703: getname: 3
mach thread 3f03: getname: com.apple.NSURLConnectionLoader
mach thread 4007: getname: 
mach thread 4707: getname: 
mach thread 6203: getname: 
mach thread 6303: getname: com.apple.CFSocket.private
mach thread 6903: getname: 
mach thread 6a03: getname: 
mach thread 6b03: getname: 
-------------------
---------dealloc----------
current thread num: 3307 thread name:
mach thread a0b: getname: 
mach thread d03: getname: 
mach thread 1403: getname: 
mach thread 3307: getname: 
mach thread 3603: getname: 2
mach thread 3703: getname: 3
mach thread 3f03: getname: com.apple.NSURLConnectionLoader
mach thread 4007: getname: 
mach thread 4707: getname: 
mach thread 6203: getname: 
mach thread 6303: getname: com.apple.CFSocket.private
mach thread 6903: getname: 
mach thread 6a03: getname: 
mach thread 6b03: getname: 
-------------------

我们发现重新下载的两条线程id分别为:

---------start----------
current thread num: 1403 thread name:
---------start----------
current thread num: 3307 thread name:

这说明NSOperationQueue的线程池起了作用,1403和3307线程在下载完后,率先进入休眠状态,有新任务来时,两条线程再次被唤醒,而不是重新再起线程。为了验证这个的判断,我们来改一下NSOperationQueue的并发数:

_queue.maxConcurrentOperationCount = 3;

然后我们下载6张图:

NSArray* urls = @[@"http://www.collegedj.net/wp-content/uploads/2010/10/1-150x150.jpg",
                      @"http://www.collegedj.net/wp-content/uploads/2010/10/Rihanna.jpg",
                      @"http://www.collegedj.net/wp-content/uploads/2010/10/chris-brown.jpg",
                      @"http://www.collegedj.net/wp-content/uploads/2010/10/dj_scary.jpg",
                      @"http://www.collegedj.net/wp-content/uploads/2010/10/3-150x150.jpg",
                      @"http://www.collegedj.net/wp-content/uploads/2010/10/3-300x199.jpg"
                    ];

再次观察日志:

---------start----------
---------start----------
---------start----------
current thread num: 1403 thread name:0
current thread num: 3307 thread name:1
current thread num: 3603 thread name:2
-------------------
-------------------
-------------------
---------start----------
current thread num: 1403 thread name:3
-------------------
---------start----------
current thread num: 3307 thread name:4
-------------------
---------dealloc----------
---------start----------
current thread num: 3603 thread name:2
current thread num: 3d07 thread name:5

由于并发数为3,率先有3条线程并发出去,由于1403最先去下载,我们特意为其安排一个150x150的小图,其下载完成后,立刻处于休眠状态,然后第4张图片被下载,1403又被唤醒,同理,3307也是相同的情况。而当第6张图要去下载时,我们看到是一条新的线程id为3d07,不在1403,3307,3603之内。这说明线程池当前没有可调度的线程了,只好创建一个新线程。

最后,我们图片全部下载完,清空这个线程池:

[_queue cancelAllOperations];
    _queue = nil;
    dumpThreads(@"finish");

结果为:

---------finish----------
current thread num: a0b thread name:
mach thread a0b: getname: 
mach thread d03: getname: 
mach thread 3c03: getname: com.apple.NSURLConnectionLoader
mach thread 5b03: getname: com.apple.CFSocket.private
-------------------

这个结果也在我们意料之中,所有线程池创建的线程全被销毁,只留下一个主线程,一个不知道名字的线程,两个网络请求的线程。

Resource

//附:pthread代码:

//使用ptrhead
struct threadInfo {
    unsigned char* url;
    size_t count;
};

struct threadResult {

    unsigned char* imageRawData;
    unsigned short int imageLength;
};

void * downloadImage(void *arg)
{
    struct threadInfo const * const info = (struct threadInfo *) arg;

    unsigned char* url = info->url;
 
    NSURL* nsUrl  =[ NSURL URLWithString:[NSString stringWithUTF8String:(char*)url]];
    NSData* data = [NSData dataWithContentsOfURL:nsUrl];
    
    struct threadResult * const result = (struct threadResult *) malloc(sizeof(*result));
    result -> imageRawData = (unsigned char*)data.bytes;
    result -> imageLength = data.length;
    
    return result;
}


- (void)downloadImageWithPthread
{
    //线程参数结构体
    struct threadInfo * const info = (struct threadInfo *) malloc(sizeof(*info));
    info->url = (unsigned char*)url1.UTF8String;
    
    //
    pthread_t tid;
    int err_create = pthread_create(&tid, NULL, &downloadImage, info);
    NSCAssert(err_create == 0, @"pthread_create() failed: %d", err_create);
    
    // Wait for the threads to exit:
    struct threadResult * results;
    
    int err_join = pthread_join(tid, (void**)(&results));
    NSCAssert(err_join == 0, @"pthread_join() failed: %d", err_join);
    
    NSData* imgData = [NSData dataWithBytes:results->imageRawData length:results->imageLength];
    _imgv1.image = [UIImage imageWithData: imgData];
  
    free(results);
    results = NULL;
}