多线程使用:NSThread、NSOperation、GCD

NSThread

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

NSOperation

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

GCD

  • GCD是一个替代诸如NSThread, NSOperationQueue, NSInvocationOperation等技术的很高效和强大的技术。GCD会自动根据任务在多核处理器上分配资源,优化程序。
  • 基于block,能简单的在不同代码作用域之间传递上下文;GCD能自动根据系统负载来增减线程数量,减少了上下文切换以及增加了计算效率;用更高性能的方法优化代码,而且GCD能提供更多的控制权力以及大量的底层函数。
  • 自动管理线程的生命周期(创建线程、调度任务、销毁线程),只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码。

NSThread的使用

线程创建

对于NSThread来说,每一个对象就代表着一个线程,NSThread提供了2种创建线程的方法:

// 方法1
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
// 方法2
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);

// 方法3
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);
// 方法4
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
  • 方法1:直接创建并启动一个线程去Selector。由于没有返回值,如果需要获取新创建的Thread,需要在执行的Selector中调用-[NSThread currentThread]获取。
  • 方法2:初始化线程并返回,线程的入口函数由Selector传入。线程创建出来之后需要手动调用-start方法启动。
  • 方法3:不显式创建线程的方法。
  • 方法4:一般用于在主线程更新UI操作。

示例代码:

- (void)viewDidLoad {
    [super viewDidLoad];

//    [NSThread detachNewThreadSelector:@selector(downloadImage:) toTarget:self withObject:kURL];
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(downloadImage:) object:kURL];
    [thread start];
}

- (void)downloadImage:(NSString *)url {
    NSData *data = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:url]];
    UIImage *image = [[UIImage alloc] initWithData:data];
    if (image == nil) {
        
    } else {
        // 图片下载完回到主线程更新UI
        [self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];
    }
}

- (void)updateUI:(UIImage *)image {
    self.imageView.image = image;
}

线程同步

@interface ViewController () {
    int tickets;
    int count;
    NSThread *ticketsThreadOne;
    NSThread *ticketsThreadTwo;
    NSCondition *ticketsCondition;
    NSLock *theLock;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    tickets = 10;
    count = 0;
    theLock = [[NSLock alloc] init];
    // 一个锁和一个线程检查器:http://www.jianshu.com/p/5d20c15ae690
    ticketsCondition = [[NSCondition alloc] init];
    
    ticketsThreadOne = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [ticketsThreadOne setName:@"Thread-1"];
    [ticketsThreadOne start];
    
    ticketsThreadTwo = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [ticketsThreadTwo setName:@"Thread-2"];
    [ticketsThreadTwo start];
    
}

- (void)run {
    while (TRUE) {
        // 上锁
//        [ticketsCondition lock];
        [theLock lock];
        if (tickets >= 0) {
            [NSThread sleepForTimeInterval:1];
            count = 10 - tickets;
            NSLog(@"当前票数是:%d,售出:%d,线程名:%@", tickets, count, [[NSThread currentThread] name]);
            tickets--;
        } else {
            break;
        }
        // 解锁
//        [ticketsCondition unlock];
        [theLock unlock];
    }
}

@end

说明:

  • 上锁、解锁目的:保证数据在某个时间段只能被一个线程所操作,加上lock之后线程同步保证了数据的正确性。

  • NSCondition 的对象实际上作为一个锁和一个线程检查器:锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。

NSConditon *condition = [[NSCondition alloc] init];
// 一般用于多线程同时访问、修改同一个数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到unlock ,才可访问
[condition lock]; 
// 与lock 同时使用
[condition unlock];
// 让当前线程处于等待状态
[condition wait];
// CPU发信号告诉线程不用在等待,可以继续执行
[condition signal];

线程的顺序执行

通过 [ticketsCondition signal]; 发送信号的方式,在一个线程唤醒另外一个线程的等待。wait是等待,加了一个线程3 去唤醒其他两个线程锁中的wait。如下所示:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    tickets = 10;
    count = 0;
    theLock = [[NSLock alloc] init];
    // 一个锁和一个线程检查器:http://www.jianshu.com/p/5d20c15ae690
    ticketsCondition = [[NSCondition alloc] init];
    
    ticketsThreadOne = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [ticketsThreadOne setName:@"Thread-1"];
    [ticketsThreadOne start];
    
    ticketsThreadTwo = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [ticketsThreadTwo setName:@"Thread-2"];
    [ticketsThreadTwo start];
    
    ticketsThreadThree = [[NSThread alloc] initWithTarget:self selector:@selector(run3) object:nil];
    [ticketsThreadThree setName:@"Thread-3"];
    [ticketsThreadThree start];
    
}

- (void)run {
    while (TRUE) {
        // 上锁
        [ticketsCondition lock];
        [ticketsCondition wait];
        [theLock lock];
        if (tickets >= 0) {
            [NSThread sleepForTimeInterval:1];
            count = 10 - tickets;
            NSLog(@"当前票数是:%d,售出:%d,线程名:%@", tickets, count, [[NSThread currentThread] name]);
            tickets--;
        } else {
            break;
        }
        [ticketsCondition unlock];
        [theLock unlock];
    }
}

- (void)run3 {
    while (YES) {
        [ticketsCondition lock];
        [NSThread sleepForTimeInterval:3];
        [ticketsCondition signal];
        [ticketsCondition unlock];
    }
}

睡眠

NSThread提供了2个让线程睡眠的方法,一个是根据NSDate传入睡眠时间,一个是直接传入NSTimeInterval。

+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

注意区分sleepUntilDate:和runloop中的runUntilDate:

sleepUntilDate:相当于执行一个sleep的任务。在执行过程中,即使有其他任务传入runloop,runloop也不会立即响应,必须sleep任务完成之后,才会响应其他任务。

runUntilDate:虽然会阻塞线程,阻塞过程中并不妨碍新任务的执行。当有新任务的时候,会先执行接收到的新任务,新任务执行完之后,如果时间到了,再继续执行runUntilDate:之后的代码。

线程通讯

一般用方法3、4将任务丢给辅助线程,任务执行完成之后再使用方法1、2将结果传回主线程。

// 方法1:将selector丢给主线程执行,可以指定runloop mode
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
// 方法2:将selector丢给主线程执行,runloop mode默认为common mode
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes

// 方法3:将selector丢个指定线程执行,可以指定runloop mode
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
// 方法4:将selector丢个指定线程执行,runloop mode默认为default mode
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);

注意:perform方法只对拥有runloop的线程有效,如果创建的线程没有添加runloop,perform的selector将无法执行。

线程优先级

+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;

@property double threadPriority NS_AVAILABLE(10_6, 4_0); // To be deprecated; use qualityOfService below
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0); // read-only after the thread is started

// 线程优先级枚举
typedef NS_ENUM(NSInteger, NSQualityOfService) {
    /* UserInteractive QoS is used for work directly involved in providing an interactive UI such as processing events or drawing to the screen. */
    // 最高优先级,主要用于提供交互UI的操作,比如处理点击事件,绘制图像到屏幕上
    NSQualityOfServiceUserInteractive = 0x21,
    
    /* UserInitiated QoS is used for performing work that has been explicitly requested by the user and for which results must be immediately presented in order to allow for further user interaction.  For example, loading an email after a user has selected it in a message list. */
    // 次高优先级,主要用于执行需要立即返回的任务
    NSQualityOfServiceUserInitiated = 0x19,
    
    /* Utility QoS is used for performing work which the user is unlikely to be immediately waiting for the results.  This work may have been requested by the user or initiated automatically, does not prevent the user from further interaction, often operates at user-visible timescales and may have its progress indicated to the user by a non-modal progress indicator.  This work will run in an energy-efficient manner, in deference to higher QoS work when resources are constrained.  For example, periodic content updates or bulk file operations such as media import. */
    // 默认优先级,当没有设置优先级的时候,线程默认优先级
    NSQualityOfServiceUtility = 0x11,
    
    /* Background QoS is used for work that is not user initiated or visible.  In general, a user is unaware that this work is even happening and it will run in the most efficient manner while giving the most deference to higher QoS work.  For example, pre-fetching content, search indexing, backups, and syncing of data with external systems. */
    // 普通优先级,主要用于不需要立即返回的任务
    NSQualityOfServiceBackground = 0x09,

    /* Default QoS indicates the absence of QoS information.  Whenever possible QoS information will be inferred from other sources.  If such inference is not possible, a QoS between UserInitiated and Utility will be used. */
    // 后台优先级,用于完全不紧急的任务
    NSQualityOfServiceDefault = -1
} NS_ENUM_AVAILABLE(10_10, 8_0);

线程通知

// 由当前线程派生出第一个其他线程时发送,一般一个线程只发送一次
FOUNDATION_EXPORT NSNotificationName const NSWillBecomeMultiThreadedNotification;
// 这个通知目前没有实际意义,可以忽略
FOUNDATION_EXPORT NSNotificationName const NSDidBecomeSingleThreadedNotification;
// 线程退出之前发送这个通知
FOUNDATION_EXPORT NSNotificationName const NSThreadWillExitNotification;

其它方法、属性

// 判断当前线程是否多线程
+ (BOOL)isMultiThreaded;

// 判断当前线程是否是主线程
@property (readonly) BOOL isMainThread NS_AVAILABLE(10_5, 2_0);
#if FOUNDATION_SWIFT_SDK_EPOCH_AT_LEAST(8)
@property (class, readonly) BOOL isMainThread NS_AVAILABLE(10_5, 2_0); // reports whether current thread is main
// 获取主线程
@property (class, readonly, strong) NSThread *mainThread NS_AVAILABLE(10_5, 2_0);
// 获取当前线程
@property (class, readonly, strong) NSThread *currentThread;

// 启动线程
- (void)start NS_AVAILABLE(10_5, 2_0);

// 判断线程是否需要退出
@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);
// 该方法并不会立刻取消线程,它仅仅是将cancelled属性设置为YES。线程取消的功能需在main函数中自己实现。
- (void)cancel NS_AVAILABLE(10_5, 2_0);

// 线程立即退出:调用之后会立即终止线程。即使任务还没有执行完成也会中断。这就非常有可能导致内存泄露等严重问题,所以一般不推荐使用。
// 对于有runloop的线程,可以使用CFRunLoopStop()结束runloop配合-cancel结束线程。
+ (void)exit;

线程使用流程

  • 1、创建线程,并指定入口main函数为-threadMain。
  • 2、设置线程的优先级,qualityOfService属性必须在线程启动之前设置,启动之后将无法再设置。
  • 3、调用start方法启动线程。
  • 4、设置线程的名字,这一步不是必须的,主要是为了debug的时候更方便,可以直接看出这是哪个线程。
  • 5、自定义的线程默认是没有runloop的,调用-currentRunLoop,方法内部会为线程创建runloop。
  • 6、如果没有数据源,runloop会在启动之后会立刻退出。所以需要给runloop添加一个数据源,这里添加的是NSPort数据源。
  • 7、定期检查isCancelled,当外部调用-cancel方法将isCancelled置为YES的时候,线程可以退出。
  • 8、启动runloop。
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil]; 
self.thread.qualityOfService = NSQualityOfServiceDefault; 
[self.thread start]; 

- (void)threadMain {
    [[NSThread currentThread] setName:@"myThread"];
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];   
    while (![[NSThread currentThread] isCancelled]) {           
        [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];
    }
}
  • 线程分配任务:线程创建好了之后我们就可以给线程丢任务了,当我们有一个需要比较耗时的任务的时候,我们可以调用perform方法将task丢给这个线程。用于处理耗时任务。
[self performSelector:@selector(threadTask) onThread:self.thread withObject:nil waitUntilDone:NO];
  • 结束线程

该方法须在self.thread线程下调用。如果当前是主线程,可以perform到self.thread下调用这个方法结束线程。

- (void)cancelThread {
    [[NSThread currentThread] cancel];
    CFRunLoopStop(CFRunLoopGetCurrent());
}

NSOperation的使用

NSInvocationOperation和NSBlockOperation

示例如下:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 1.创建一个操作
    NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:kURL];
    
    // 2.将操作添加到队列中 —— 方法1
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:operation];
    
    // 2.将操作添加到队列中 —— 方法2:使用start方法系统不会开辟一个新的线程去执行任务,任务会在当前线程(不一定是主线程)同步执行
    //[operation start];
    
    //    // 使用NSBlockOperation创建操作:NSBlockOperation封装的操作数大于1的时候,就会执行异步操作
//    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
//        NSLog(@"task1---%@", [NSThread currentThread]); // 主线程
//        [self downloadImage:kURL];
//    }];
//    [operation addExecutionBlock:^{
//        NSLog(@"task2---%@", [NSThread currentThread]); // 新线程
//    }];
//    [operation addExecutionBlock:^{
//        NSLog(@"task3---%@", [NSThread currentThread]); // 新线程
//    }];
//    [operation start];
    
//    2017-05-29 21:18:15.573661+0800 NSThreadDemo[385:107939] task1---<NSThread: 0x17406d080>{number = 1, name = main}
//    2017-05-29 21:18:15.573667+0800 NSThreadDemo[385:107972] task2---<NSThread: 0x17007b2c0>{number = 3, name = (null)}
//    2017-05-29 21:18:15.573743+0800 NSThreadDemo[385:107972] task3---<NSThread: 0x17007b2c0>{number = 3, name = (null)}
}

- (void)downloadImage:(NSString *)url {
    NSData *data = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:url]];
    UIImage *image = [[UIImage alloc] initWithData:data];
    if (image == nil) {

    } else {
        // 图片下载完回到主线程更新UI
        [self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];
    }
}

- (void)updateUI:(UIImage *)image {
    self.imageView.image = image;
}

@end

注意:

  • 不把操作添加到队列中而直接调用start方法时,系统并不会开辟一个新的线程去执行任务,任务会在当前线程同步执行。NSOperation的start方法默认是同步执行任务。
  • 当NSOperation被添加到NSOperationQueue中后,就会全自动地执行异步操作。
  • 使用NSBlockOperation,操作数大于1的时候,就会执行异步操作。

自定义NSOperation

操作步骤

1、子类化NSOperation。
2、在.m文件里面实现-(void)main方法。
3、初始化该操作的时候直接调用alloc及init即可。
4、同样可以通过start方法让你自定义的任务跑在当前线程中。

NSOperationQueue

NSOperationQueue种类

  • 自带主队列:[NSOperationQueue mainQueue], 添加到主队列中的任务都会在主线程中执行。
  • 自己创建队列:NSOperationQueue *queue = [[NSOperationQueue alloc] init],这种队列同时包含串行、并发的功能,添加到非主队列的任务会自动放到子线程中执行。

向NSOperationQueue中添加操作

  • 直接添加
    [queue addOperation:operation];
  • 使用block添加:block的内容会被包装成operation对象添加到队列
    [queue addOperationWithBlock:^{
    
    }];

其它

// 设置NSOperationQueue的最大并发数:当并发数为1就变成了串行执行任务
@property NSInteger maxConcurrentOperationCount;

// 取消单个操作
- (void)cancel;
// 取消队列里面全部的操作:不能取消正在进行中的任务,队列调用了cancelAllOperations后会等当前正在进行的任务执行完闭后取消后面的操作
- (void)cancelAllOperations;

// 判断是否挂起:YES=挂起,NO=恢复。不能挂起正在运行中的操作,队列会等当前操作结束后将后面的操作暂停(挂起)
@property (getter=isSuspended) BOOL suspended;

// 设置操作间的依赖,可以确定这些操作的执行顺序
- (void)addDependency:(NSOperation *)op;
- (void)removeDependency:(NSOperation *)op;

注意:

  • 自定义NSOperation的时候需要注意,最好可以经常通过判断isCancelled方法检测操作是否被取消,以响应外部可能进行的取消操作。

线程间通信

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 子线程下载图片
[queue addOperationWithBlock:^{
    NSURL *url = [NSURL URLWithString:@"http://img.pconline.com.cn/images/photoblog/9/9/8/1/9981681/200910/11/1255259355826.jpg"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [[UIImage alloc] initWithData:data];
    
    // 回到主线程进行显示
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        self.imageView.image = image;
    }];
}];

GCD的使用

请参考:

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

推荐阅读更多精彩内容