多线程全知道

iOS中三种多线程编程技术分别是:

  • NSThread
  • Cocoa NSOperation
  • GCD (全称: Grand Central Dispatch)

这三种编程方式从上到下, 抽象度层次从低到高, 抽象度越高的使用越简单, 也是Apple最推荐使用的.

各自优缺点:

  • NSThread:

优点: NSThread 比其他两个轻量级
缺点: 需要自己管理线程的生命周期, 线程同步. 而线程同步对数据的枷锁会有一定的系统开销.

  • NSOperation

优点: 不需要关心线程管理, 数据同步的事情, 可以把经理放在自己需要执行的操作上. Cocoa operation 相关的类是 NSOperation, NSOperationQueue.
NSOperation 是个抽象类, 使用它必须用它的子类, 可以实现它挥着使用它自定义的两个子类: NSInvocationOperation和 NSBlockOperation.
创建NSOperation子类的对象, 把对象添加到NSOPerationQueue对队列里执行.

  • GCD

Grand Central Dispatch(GCD)是Apple开发的一个多核编程的解决方法, 在iOS4.0 开始之后才能使用. GCD是一个替代诸如NSThread, NSOperationQueue, NSInvocationOperation等技术的很高效和强大的技术, 现在的iOS系统升级 不用担心技术不能使用.

NSThread的使用

NSThread 有两种直接创建方式

一. 实例方法 (先创建线程并要有个开始start指令,这里可设置线程的优先级等线程信息)

- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 

二. 类方法 (直接创建线程并开始运行线程)

+ (void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument

参数的意义:

  • selector :线程执行的方法,这个selector只能有一个参数,而且不能有返回值。
  • target :selector消息发送的对象
  • argument:传输给target的唯一参数,也可以是nil

不显式创建线程的方法:

用NSObject的类方法 performSelectorInBackground:withObject: 创建一个线程:

[Obj performSelectorInBackground:@selector(doSomething) withObject:nil];

线程间通讯

线程下载完图片后怎么通知主线程更新界面呢?

[self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];

performSelectorOnMainThread是NSObject的方法,除了可以更新主线程的数据外,还可以更新其他线程的比如:

performSelector:onThread:withObject:waitUntilDone:

线程同步

如果没有线程同步的lock,卖票数可能是-1.加上lock之后线程同步保证了数据的正确性。
线程的顺序执行

他们都可以通过[ticketsCondition signal]; 发送信号的方式,在一个线程唤醒另外一个线程的等待。

使用****NSCondition****,实现多线程的同步,即可实现生产者消费者问题。

基本思路是,首先要创建公用的****NSCondition****实例。然后:
消费者取得锁,取产品,如果没有,则wait,这时会释放锁,直到有线程唤醒它去消费产品;
生产者制造产品,首先也是要取得锁,然后生产,再发signal,这样可唤醒wait的消费者。

这里需要注意****wait****和****signal****的问题:
1: 其实,wait函数内部悄悄的调用了unlock函数(猜测,有兴趣可自行分析),也就是说在调用wati函数后,这个NSCondition对象就处于了无锁的状态,这样其他线程就可以对此对象加锁并触发该NSCondition对象。当NSCondition被其他线程触发时,在wait函数内部得到此事件被触发的通知,然后对此事件重新调用lock函数(猜测),而在外部看起来好像接收事件的线程(调用wait的线程)从来没有放开NSCondition对象的所有权,wati线程直接由阻塞状态进入了触发状态一样。这里容易造成误解。
2: wait函数并不是完全可信的。也就是说wait返回后,并不代表对应的事件一定被触发了,因此,为了保证线程之间的同步关系,使用NSCondtion时往往需要加入一个额外的变量来对非正常的wait返回进行规避。
3: 关于多个wait时的调用顺序,测试发现与wait执行顺序有关。具体请查阅文档。

其它同步:

可使用指令 @synchonized 来简化 NSLock 的使用, 这样不必显示创建NSLock, 加锁并解锁相关代码.
除外还有比如:循环锁 NSRecursiveLock, 条件锁 NSConditionLock, 分布式锁 NSDistributedLock等等

Cocoa NSOperation 的使用

使用 NSOperation 的方式有两种,
一种是用定义好的两个子类:

  • NSInvocationOperation 和 NSBlockOperation.
  • 另一种是继承NSOperation

NSOperation 和 java.lang.Runnable接口很相似. 和其一样, NSOperation 也是设计用来扩展的, 只需继承重写NSOperation的一个main, 相当于 java 中Runnable的 Run方法. 然后把NSOperation子类的对象放入 NSOperationQueue队列中, 该队列就会启动并开始处理它.

默认情况下,NSOperation并不具备封装操作的能力,必须使用它的子类,使用NSOperation子类的方式有3种:

1> 自定义子类继承NSOperation,实现内部相应的方法
2> NSBlockOperation
3>NSInvocationOperation

先介绍如何用NSOperation封装一个操作,后面再结合NSOperationQueue来使用。
1.****首先介绍自定义****NSOperation****:
NSOperation是没法直接使用的,它只是提供了一个工作的基本逻辑,具体实现还是需要你通过定义自己的NSOperation子类来获得。

#import <Foundation/Foundation.h>
@protocol NSDefineOprationDelegate <NSObject>
- (void) handleDelegate;
@end
@interface NSDefineOpration : NSOperation
@property (nonatomic, assign) id <NSDefineOprationDelegate> delegate;
- (id)initWithDelegate:(id<NSDefineOprationDelegate>) delegate;
@end

实现文件里:

#import "NSDefineOpration.h"
@implementation NSDefineOpration
- (id)initWithDelegate:(id<NSDefineOprationDelegate>) delegate{
    if(self = [super init]){
        self.delegate = delegate;
    }
    return self;
}
- (void)main{
    @autoreleasepool {
        //do something
        sleep(15);
        NSLog(@"op1........handle......  on thread num :%@",[NSThread currentThread]);
        
        if([self.delegate respondsToSelector:@selector(handleDelegate)])
        {
            [self.delegate performSelector:@selector(handleDelegate) withObject:nil];
        }
    }
    
}
@end

这里的sleep(15)主要用来做一些延时的操作,比如网络下载等。
调用:

- (void)oprationTest
{
    NSDefineOpration *op1 = [[NSDefineOpration alloc] initWithDelegate:self];
    op1.completionBlock = ^(){
        NSLog(@"op1........OK !!");
    };
    [op1 start];
}

从执行结果可以看出,因为在实现的main函数里没有使用异步线程处理,导致直接阻塞了主线程1,所以使用这种方式一定注意main函数里操作时间过长导致主线程阻塞问题。耗时比较长的都放到其他线程里处理。

2.****接下来介绍****NSBlockOperation
第一种使用NSBlockOperation的方式 block类方法

    NSLog(@"block start");
    NSBlockOperation *bop2 = [NSBlockOperation blockOperationWithBlock:^{
        sleep(15);
        NSLog(@"bop2.....handle..... on thread num%@",[NSThread currentThread]);
    }];
    [bop2 setCompletionBlock:^{
        NSLog(@"bop2........OK !!");
    }];
    [bop2 start];

首先初始化了一个NSBlockOperation对象,它是用一个Block来封装需要执行的操作 调用了start方法,紧接着会马上执行Block中的内容
这里还是在当前线程同步执行操作,并没有异步执行,阻塞主线程。

第二种使用NSBlockOperation的方式: alloc 实例方法

- (void)blockOprationTest
{
    NSLog(@"block start");
    NSBlockOperation * op2 = [[NSBlockOperation alloc] init];
    [op2 addExecutionBlock:^{
        sleep(10);
        NSLog(@"op2.....handle..... on 10 hread num%@",[NSThread currentThread]);
    }];
    
    [op2 addExecutionBlock:^{
        sleep(6);
        NSLog(@"op2.....handle..... on 6 thread num%@",[NSThread currentThread]);
    }];
    
    [op2 addExecutionBlock:^{
        sleep(4);
        NSLog(@"op2.....handle..... on 4 thread num%@",[NSThread currentThread]);
    }];
    
    [op2 addExecutionBlock:^{
        sleep(8);
        NSLog(@"op2.....handle..... on 8 thread num%@",[NSThread currentThread]);
    }];
    
    [op2 addExecutionBlock:^{
        sleep(1);
        NSLog(@"op2.....handle..... on 1 thread num%@",[NSThread currentThread]);
    }];
    [op2 setCompletionBlock:^{
        NSLog(@"op2........OK !!");
    }];
    [op2 start];
    
    //bop2
    NSBlockOperation *bop2 = [NSBlockOperation blockOperationWithBlock:^{
        sleep(15);
        NSLog(@"bop2.....handle..... on thread num%@",[NSThread currentThread]);
    }];
    [bop2 setCompletionBlock:^{
        NSLog(@"bop2........OK !!");
    }];
    [bop2 start];
}

分析下结果:
首先看到了有1和2两个线程,线程2在56秒的时候开始执行6秒的操作,接下来执行4,1秒结束时间为07秒。线程1在56的时候开始执行10秒的操作,接下来执行8秒,结束时间为14秒。最后执行Bop2的15秒操作至29秒。时间看起来没有问题。为什么会启用2个线程而不是3个或者更多?

3.****接下来介绍****NSInvocationOperation

- (void)invocationOperation
{
    NSInvocationOperation * op3 = [[NSInvocationOperation alloc] initWithTarget:(id)self selector:@selector(handleInvoOpDelegate) object:nil];
    [op3 setCompletionBlock:^{
        NSLog(@"op3........OK !!");
    }];
    [op3 start];
}
selector函数:
- (void)handleInvoOpD
{
    sleep(5);
    NSLog(@"op3.....handle.....  on thread num :%@",[NSThread currentThread]);
}

NSInvocationOperation比较简单,就是继承了NSOperation,区别就是它是基于一个对象和selector来创建操作,可以直接使用而不需继承来实现自己的操作处理。

4.****最后介绍下****NSOperationQueue
把NSOperation子类的对象放入NSOperationQueue队列中,该队列就会启动并开始处理它。队列里可以加入很多个NSOperation, 可以把NSOperationQueue看作一个线程池,可往线程池中添加操作(NSOperation)到队列中。线程池中的线程可看作消费者,从队列中取走操作,并执行它。
eg:

- (void)handleOpqueue
{
    NSOperationQueue *qu = [[NSOperationQueue alloc] init];
    
    NSBlockOperation * bkOp1 = [NSBlockOperation blockOperationWithBlock:^{
        sleep(10);
        NSLog(@"bkOp1.....handle.....on thread num%@",[NSThread currentThread]);
    }];
    [bkOp1 setCompletionBlock:^{
        NSLog(@"bkOp1........OK !!");
    }];
    
    
    NSBlockOperation * bkOp2 = [NSBlockOperation blockOperationWithBlock:^{
        sleep(2);
        NSLog(@"bkOp2.....handle.....on thread num%@",[NSThread currentThread]);
    }];
    [bkOp2 setCompletionBlock:^{
        NSLog(@"bkOp2........OK !!");
    }];
    
    NSBlockOperation * bkOp3 = [NSBlockOperation blockOperationWithBlock:^{
        sleep(1);
        NSLog(@"bkOp3.....handle.....on thread num%@",[NSThread currentThread]);
    }];
    [bkOp3 setCompletionBlock:^{
        NSLog(@"bkOp3........OK !!");
    }];
    
    NSBlockOperation * bkOp4 = [NSBlockOperation blockOperationWithBlock:^{
        sleep(10);
        NSLog(@"bkOp4.....handle.....on thread num%@",[NSThread currentThread]);
    }];
    [bkOp4 setCompletionBlock:^{
        NSLog(@"bkOp4........OK !!");
    }];
    
    NSBlockOperation * bkOp5 = [NSBlockOperation blockOperationWithBlock:^{
        sleep(5);
        NSLog(@"bkOp5.....handle.....on thread num%@",[NSThread currentThread]);
    }];
    [bkOp5 setQueuePriority:NSOperationQueuePriorityHigh];
    [bkOp5 setCompletionBlock:^{
        NSLog(@"bkOp5........OK !!");
    }];
    
    NSInvocationOperation *invoOp6 = [[NSInvocationOperation alloc] initWithTarget:(id)self selector:@selector(handleInvoOp) object:nil];
    [invoOp6 setCompletionBlock:^{
        NSLog(@"invoOp6........OK !!");
    }];
    [invoOp6 setQueuePriority:NSOperationQueuePriorityHigh];
    
    [qu setMaxConcurrentOperationCount:2];
    [qu addOperation:bkOp3];
    [qu addOperation:bkOp2];
    [qu addOperation:bkOp1];
    [qu addOperation:bkOp4];
    [qu addOperation:bkOp5];
    [qu addOperation:invoOp6];
}

在设置了bkop5以及invOp6的优先级为高时,他们会优先执行,当然这个优先时相对,是相对正在排队的,不包括已经正在执行的。

总结:NSOperation、NSBlockOperation、NSInvocationOperation、NSOperationQueue都比较简单,NSOperation、NSBlockOperation、NSInvocationOperation单个都是表示一种操作,而NSOperationQueue是一个可以包含多个NSOperation的队列,可以自己在多个线程处理,只要加入队列之后,我们就不用去操作,直到Callback或者完成。

** 3. GCD****的介绍和使用**
介绍: Grand Central Dispatch 简称(GCD)是苹果公司开发的技术, 以优化的应用程序支持多核心处理器和其他的对称处理系统的系统. 这简历在任务并行执行的线程池模式的基础上的. 它首次发布在Mac OS X 10.6, iOS 4及以上可用.

设计: GCD的工作原理是: 让程序平行排列的特定任务, 根据可用的处理资源, 安排他们在任何可用的处理器核心上执行任务.

一个任务可以是一个函数(function)或者是一个block. GCD的底层依然是用线程实现, 不过这样可以让程序员不用关注实现的细节.

GCD中的FIFO队列称为Dispatch Queue, 它可以保证先进来的任务先得到执行.

Dispatch Queue分下面三种:
Serial
又称为private dispatch queues, 同时只执行一个任务. serial queue 通常用于同步访问特定的资源或数据. 当你创建多个 Serial Queue 时, 虽然他们各自是同步执行的, 但 Serial Queue 与 Serial queue之间是并发执行的.

Concurrent
又称为global dispatch queue, 可以并发执行多个任务, 但执行完成的顺序是随机的.

Main dispatch queue
它是全局可用的serial queue, 它是在应用程序主线程上执行任务的.

常用的方法 dispatch_async

为了避免界面在处理耗时的操作时卡死, 比如读取网络数据, IO, 数据库读写等, 我们会在另外一个线程中处理这些操作, 然后通知主线程更新界面.

用GCD实现这个流程的操作比前面介绍的 NSThread NSOperation 的方法都要简单. 代码框结构如下:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{   
    NSURL * url = [NSURL URLWithString:@"http://image.tianjimedia.com/uploadImages/2012/233/38/H439I0N71ARI.jpg"];   
    NSData * data = [[NSData alloc]initWithContentsOfURL:url];   
    UIImage *image = [[UIImage alloc]initWithData:data];   
    if (data != nil) {   
        dispatch_async(dispatch_get_main_queue(), ^{   
            self.imageView.image = image;   
         });   
    }   
});

相比前两种, GCD会自动根据任务在多喝处理器上分配资源, 优化程序.

系统给每一个应用程序提供了三个 concurrent dispatch queues. 这三个并发调度队列是全局的, 他们只是优先级的不同, 因为是全局的, 我们不需要去创建,. 我们只需要通过使用函数dispath_get_global_queue去得到队列, 如下:

dispatch_queue_t globalQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

这里也用到了系统默认就有一个串行队列main_queue:
dispatch_queue_t mainQ = dispatch_get_main_queue();

虽然dispatch queue 是引用计数对象, 但是以上两个都是全局的队列, 不用retain 或 release.

dispatch_group_async的使用

dispatch可以监听一组任务是否完成, 完成后得到通知执行其他的操作. 这个方法很有用, 比如你执行三个下载任务, 当三个任务都下载完成后你才通知界面说完成的了, 举例:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);   
dispatch_group_t group = dispatch_group_create();   
dispatch_group_async(group, queue, ^{   
    [NSThread sleepForTimeInterval:1];   
    NSLog(@"group1");   
});   
dispatch_group_async(group, queue, ^{   
    [NSThread sleepForTimeInterval:2];   
    NSLog(@"group2");   
});   
dispatch_group_async(group, queue, ^{   
    [NSThread sleepForTimeInterval:3];   
    NSLog(@"group3");   
});   
dispatch_group_notify(group, dispatch_get_main_queue(), ^{   
    NSLog(@"updateUI”);   
});   
dispatch_release(group);

dispatch_group_async是异步的方法,运行后可以看到打印结果:
每隔一秒打印一个,当第三个任务执行后,upadteUI被打印。

dispatch_barrier_async的使用

dispatch_barrier_async是在前面的任务执行结束后他才执行, 而且它后面的任务等它执行完成之后才会执行
举例:

dispatch_queue_t queue = dispatch_queue_create(“haha.com”, DISPATCH_QUEUE_CONCURRENT);   
dispatch_async(queue, ^{   
    [NSThread sleepForTimeInterval:2];   
    NSLog(@"dispatch_async1");   
});   
dispatch_async(queue, ^{   
    [NSThread sleepForTimeInterval:4];   
    NSLog(@"dispatch_async2");   
});   
// 先执行完前面的在执行后面的
dispatch_barrier_async(queue, ^{   
    NSLog(@"dispatch_barrier_async");   
    [NSThread sleepForTimeInterval:4];   

});   
dispatch_async(queue, ^{   
    [NSThread sleepForTimeInterval:1];   
    NSLog(@"dispatch_async3");   
});

dispatch_apply
执行某个代码片段N次.

dispatch_apply(5, globalQ, ^(size_t index)) {
 // 执行5次
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容