Objective-C多线程编程指南

2020-11-13  本文已影响0人  读行笔记

在iOS开发中,多线程编程实践有多种途径,它们各有侧重。

NSThread

NSThread是Apple官方推荐的多线程操作途径,它的抽象程度最高,确定是需要自己管理线程的生命周期,线程周期等核心问题。

核心属性有:

核心方法:

初始化

直接创建

通过直接使用Apple封装好的接口,就可以多线程执行,简单高效。常用的接口有:

// 1. 直接开启一个新线程执行任务
[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:nil];

// 2. 先创建线程对象,再运行线程操作,运行前可以设置线程优先级等线程信息
NSThread* myThread = [[NSThread alloc] initWithTarget:self selector:@selecto (doSomething: object:nil];
[myThread start];

//3. 不显式创建线程的方法,使用NSObject的类方法创建一个线程
[self performSelectorInBackground:@selector(doSomething) withObject:nil];

完整示例见: 直接通过创建NSThread加载用户头像列表

继承

通过继承NSThread,将耗时任务封装在类内,可以起到“高内聚,低耦合”的作用。

具体而言,重写NSThread的main方法执行相关逻辑,然后调用start方法即可开始执行。

+ (instancetype)threadWithUser:(WRGithubUser *)user{
    return [[self alloc] initWithUser:user];
}

- (instancetype)initWithUser:(WRGithubUser*)user{
    if ((self = [super init])) {
        self.user = user;
    }
    return self;
}

- (void)setHandler:(WRGithubUserAvatarHandler)handler{
    _handler = handler;
    // 开始调用下面的main方法
    [self start];
}

- (void)main{
    if (!_user || !_user.avatarUrlString) {
        return;
    }
    
    NSURL *url = [NSURL URLWithString:_user.avatarUrlString];
    NSData *imageData = [NSData dataWithContentsOfURL:url];
    
    [self.user setAvatar:[UIImage imageWithData:imageData]];
    
    if (_handler) {
        _handler();
    }
}

完整示例见: 通过继承NSThread加载用户头像列表

NSOperation

NSOperation是一个抽象类,不可直接调用,要么使用系统定义好的两个子类NSInvocationOperation和NSBlockOperation,要么继承自定义实现。

实现逻辑和NSThread大体相同,main函数是最终执行单任务逻辑的地方,start用来控制何时以及在哪里开始执行任务,cancel用来取消任务。不同点在于NSOperation可以:

NSOperationQueue

NSOperationQueue用来维护一组NSOperation对象的执行顺序和流程。执行次序不但和加入的顺序相关,而且还和任务的优先级Priority有关,很明显高优先级的任务要先执行,低优先级的任务后执行。

一旦加入进去,就不可移除,直到执行完成为止。执行完成之后,自动释放任务对象。

重要的属性:

重要方法:

简单示例:

- (void)start{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    for (NSString *str in [self urlStrs]) {
        NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImageWithUrlString:) object:str];
        [queue addOperation:operation];
    }
    
    [queue addBarrierBlock:^{
        NSLog(@"all operations finished");
    }];
}

- (void)downloadImageWithUrlString:(NSString*)urlStr{
    NSURL *url = [NSURL URLWithString:urlStr];
    NSData *data = [NSData dataWithContentsOfURL:url];
    
    NSLog(@"response data length:%zd from \nurl:%@", data.length, urlStr);
    // ...
}

- (NSArray<NSString*>*)urlStrs{
    return @[
        @"https://avatar.csdnimg.cn/B/A/A/3_qq_25537177.jpg",
        @"https://profile.csdnimg.cn/B/4/2/3_qq_41185868",
        @"https://profile.csdnimg.cn/8/0/6/3_qq_35190492",
        @"https://profile.csdnimg.cn/5/2/2/3_dataiyangu",
    ];
}

完整代码见:NSOperation实践

GCD

Grand Central Dispatch简称GCD,是Apple为多核设备并发编程提供的一套综合性的解决方案,因为是在系统级别上实现的,所以更高效。

概况

队列Queue

在GCD中,一共有三种队列,分别是:

优先级Priority

GCD中,所有任务都可以指定优先级,共分为四种:

不过,任务优先级现在被另一个特性服务质量QOS所取代,QOS即Quality of Service。它有五个值,和优先级有一定的对应关系。

对于global queue,也就是系统级的并发队列,任务优先级和QOS之间的对应关系如下:

Priority Quality of Service
DISPATCH_QUEUE_PRIORITY_HIGH QOS_CLASS_USER_INITIATED
DISPATCH_QUEUE_PRIORITY_DEFAULT QOS_CLASS_DEFAULT
DISPATCH_QUEUE_PRIORITY_LOW QOS_CLASS_UTILITY
DISPATCH_QUEUE_PRIORITY_BACKGROUND QOS_CLASS_BACKGROUND
dispatch_queue_t main, serial, concur1, concur2, concur3;

// 主线程队列,用来维护在主线程执行的任务执行次序
main = dispatch_get_main_queue();

// 串行队列
serial = dispatch_queue_create("COM.WALKER.S", DISPATCH_QUEUE_SERIAL);

// 并发队列
concur1 = dispatch_queue_create("COM.WALKER.C", DISPATCH_QUEUE_CONCURRENT);
// 下面两种是同一回事,但是推荐后面的写法
concur2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
concur3 = dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0);

实践

创建任务

任务表示一段逻辑上完整且具有意义的代码块,可分为同步任务和异步任务。

// 在串行队列serial中执行同步任务
dispatch_sync(serial, ^{
    [self do...]
});

// 在并行队列concur1中执行异步任务
dispatch_async(concur1, ^{
    [self do...]
});

// 下载图片
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
     NSURL * url = [NSURL URLWithString:@"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"];
     NSData * data = [[NSData alloc]initWithContentsOfURL:url];
     UIImage *image = [[UIImage alloc]initWithData:data];
     if (data != nil) {
            // 在主线程更新UI
            dispatch_async(dispatch_get_main_queue(), ^{
                self.imageView.image = image;
            });
     }
});

单次任务dispatch_once

在一些场景中,某个任务只被允许执行一次,比如创建单例。

// 单次任务
- (void)doOnceTask{
    static NSData *data;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"some-url"]];
    });
}

// 单例
+ (instancetype)sharedManager{
    static WRSnippetManager *manager;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [WRSnippetManager new];
    });
    return manager;
}

延迟执行dispatch_after

延迟执行的使用场景很多,比如显示一些反馈信息给用户,但需要过一小段时间之后隐藏,如登录成功、失败,上传任务完成等。

/**
时间单位:
    秒:NSEC_PER_SEC
    毫秒:NSEC_PER_MSEC
    纳秒:NSEC_PER_USEC
*/
// 延迟2秒后执行
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
dispatch_after(delayTime, dispatch_get_main_queue(), ^{
    [self do...]
});

dispatch_barrier

GCD的dispatch_barrier相关API和NSOperationQueue的addBarrierBlock:类似,都可以保证在当前加入队列的任务执行时,前面已经加入的所有任务都执行完成,但dispatch_barrier更加强大灵活。用它可以高效地实现读写问题,即单一资源的线程安全问题。

注意:使用Dispatch Barrier API时,Dispatch Queue必须是DISPATCH_QUEUE_CONCURRENT类型的。

下面是一个多读单写实现。

@implementation GCDQueueExample{
    dispatch_queue_t wrQueue;
    NSMutableDictionary *userInfo;
}

- (instancetype)init{
    if ((self = [super init])) {
        wrQueue = dispatch_queue_create("COM.WALKER.WRQ", DISPATCH_QUEUE_CONCURRENT);
        userInfo = [NSMutableDictionary dictionary];
    }
    return self;
}

- (void)setValue:(id)value forKey:(NSString *)key{
    // 如果调用者传入的是一个NSMutableString,在返回之后如果修改key值,则可能出错
    // 所以,为了避免这些问题,对key进行copy
    key = [key copy];
    dispatch_barrier_async(wrQueue, ^{
        if (key && value) {
            [self->userInfo setValue:value forKey:key];
        }
    });
}

- (id)valueForKey:(NSString *)key{
    __block id value = nil;
    dispatch_barrier_sync(wrQueue, ^{
        value = [userInfo objectForKey:key];
    });
    return value;
}

在这个例子中,写操作是异步执行,读操作是同步执行。因为对于很多场景,只要能够按照调用者的意图写入数据就可以了,至于要不要等待并不重要;而对于读,能够立即获得数据是值得的。

dispatch_apply

利用dispatch_apply可以快速迭代,因为可以并行执行任务。

for (int i=0; i<1e6; i++) {
    // ...
}

dispatch_apply(1e6, DISPATCH_APPLY_AUTO, ^(size_t x) {
    // ...
});

但是呢🤔,经过测试发现:在一般任务上dispatch_apply比for循环要慢。

任务组dispatch_group

任务组dispatch_group和任务队列dispatch_queue的道理一样,都用来对任务进行约束,但任务组除过约束单个任务之后,还可以约束队列。也就是说,任务组dispatch_group的约束维度更高。

在复杂问题中,任务组dispatch_group是非常必要的,比如监视一组由不同队列组成的任务,在适当时机进行适当处理。

常用的方法有:

下面这个例子,通过两个类GCDTaskItem、GCDTaskScheduler来模拟任务组的使用方法。

@implementation GCDTaskItem

- (instancetype)initWithSleepSeconds:(NSInteger)seconds name:(nonnull NSString *)name queue:(nonnull dispatch_queue_t)queue{
    if (self = [super init]) {
        self.sleepSeconds = seconds;
        self.name = name;
        self.queue = queue;
    }
    return self;
}

- (void)start{
    NSDate *start = [NSDate date];
    NSLog(@"task-%@ start do task.", _name);
    
    [NSThread sleepForTimeInterval:_sleepSeconds];
    NSLog(@"---task-%@ using %.3f seconds finishing task ---", _name, [[NSDate date] timeIntervalSinceDate:start]);
}

- (void)asyncStart{
    NSDate *start = [NSDate date];
    NSLog(@"task-%@ start do task.", _name);
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_sleepSeconds * NSEC_PER_SEC)), _queue, ^{
        NSLog(@"---task-%@ using %.3f seconds finishing task ---", self.name, [[NSDate date] timeIntervalSinceDate:start]);
    });
}

@end

@implementation GDCGroupTaskScheduler

- (instancetype)initWithTasks:(NSArray<GCDTaskItem *> *)tasks name:(nonnull NSString *)name{
    if (self = [super init]) {
        self.tasks = tasks;
        self.name = name;
        self.group = dispatch_group_create();
    }
    return self;
}

- (void)dispatchTasksWaitUntilDone{
    NSDate *start = [NSDate date];
    
    NSLog(@"group-%@ start dispatch tasks",_name);
    
    for (GCDTaskItem *task in _tasks) {
        dispatch_group_async(_group, task.queue, ^{
            [task start];
        });
    }
    // 同步【synchronously】等待当前组中的所有队列中的任务完成,会阻塞当前线程
    dispatch_group_wait(_group, DISPATCH_TIME_FOREVER);
    
    NSLog(@"group-task-%@ using %.3f seconds finishing task", _name, [[NSDate date] timeIntervalSinceDate:start]);
    NSLog(@"=========================");
}

- (void)dispatchTasksUntilDoneNofityQueue:(dispatch_queue_t)queue nextTask:(GDCGroupTasksCompletionHandler)next{
    NSDate *start = [NSDate date];
    
    NSLog(@"group-%@ start dispatch tasks",_name);
    
    for (GCDTaskItem *task in _tasks) {
        dispatch_group_async(_group, task.queue, ^{
            [task start];
        });
    }
    
    dispatch_group_notify(_group, queue, ^{
        NSLog(@"group-task-%@ using %.3f seconds finishing task", self.name, [[NSDate date] timeIntervalSinceDate:start]);
        NSLog(@"=========================");
        
        if (next) {
            next();
        }
    });
}

@end

初始化任务:

- (void)initGroupTasks{
    queue1 = dispatch_get_global_queue(0, 0);
    queue2 = dispatch_get_global_queue(0, 0);
    
    tasks1 = @[
        [[GCDTaskItem alloc] initWithSleepSeconds:2 name:@"T11" queue:queue1],
        [[GCDTaskItem alloc] initWithSleepSeconds:5 name:@"T12" queue:queue2]
    ];
    tasks2 = @[
        [[GCDTaskItem alloc] initWithSleepSeconds:1 name:@"T21" queue:queue1],
        [[GCDTaskItem alloc] initWithSleepSeconds:3 name:@"T22" queue:queue2]
    ];
    
    scheduler1 = [[GDCGroupTaskScheduler alloc] initWithTasks:tasks1 name:@"S1"];
    scheduler2 = [[GDCGroupTaskScheduler alloc] initWithTasks:tasks2 name:@"S2"];
}

使用dispatch_group_wait同步等待任务完成:

- (void)performTasksWithWait{
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
        [self->scheduler1 dispatchTasksWaitUntilDone];
        [self->scheduler2 dispatchTasksWaitUntilDone];
    });
}

// 结果:
/**
2020-11-13 09:27:18.443424+0800 Snippets[16360:1149514] group-S1 start dispatch tasks
2020-11-13 09:27:18.445682+0800 Snippets[16360:1149517] task-T11 start do task.
2020-11-13 09:27:18.447914+0800 Snippets[16360:1149516] task-T12 start do task.
2020-11-13 09:27:20.452293+0800 Snippets[16360:1149517] ---task-T11 using 2.007 seconds finishing task ---
2020-11-13 09:27:23.452638+0800 Snippets[16360:1149516] ---task-T12 using 5.005 seconds finishing task ---
2020-11-13 09:27:23.453117+0800 Snippets[16360:1149514] group-task-S1 using 5.010 seconds finishing task
2020-11-13 09:27:23.453377+0800 Snippets[16360:1149514] =========================
2020-11-13 09:27:23.454756+0800 Snippets[16360:1149514] group-S2 start dispatch tasks
2020-11-13 09:27:23.454999+0800 Snippets[16360:1149516] task-T21 start do task.
2020-11-13 09:27:23.455093+0800 Snippets[16360:1149517] task-T22 start do task.
2020-11-13 09:27:24.457332+0800 Snippets[16360:1149516] ---task-T21 using 1.002 seconds finishing task ---
2020-11-13 09:27:26.457219+0800 Snippets[16360:1149517] ---task-T22 using 3.002 seconds finishing task ---
2020-11-13 09:27:26.457524+0800 Snippets[16360:1149514] group-task-S2 using 3.003 seconds finishing task
2020-11-13 09:27:26.457746+0800 Snippets[16360:1149514] =========================
*/

使用dispatch_group_notify异步等待完成通知:

- (void)performTasksWithNofity{
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
        [self->scheduler1 dispatchTasksUntilDoneAndNofity];
        [self->scheduler2 dispatchTasksUntilDoneAndNofity];
    });
}

// 结果:
/**
2020-11-13 09:41:58.804265+0800 Snippets[16462:1157254] group-S1 start dispatch tasks
2020-11-13 09:41:58.805894+0800 Snippets[16462:1157254] group-S2 start dispatch tasks
2020-11-13 09:41:58.806184+0800 Snippets[16462:1157255] task-T11 start do task.
2020-11-13 09:41:58.806658+0800 Snippets[16462:1157532] task-T12 start do task.
2020-11-13 09:41:58.807275+0800 Snippets[16462:1157254] task-T21 start do task.
2020-11-13 09:41:58.808840+0800 Snippets[16462:1157533] task-T22 start do task.
2020-11-13 09:41:59.815052+0800 Snippets[16462:1157254] ---task-T21 using 1.008 seconds finishing task ---
2020-11-13 09:42:00.812159+0800 Snippets[16462:1157255] ---task-T11 using 2.006 seconds finishing task ---
2020-11-13 09:42:01.816091+0800 Snippets[16462:1157533] ---task-T22 using 3.007 seconds finishing task ---
2020-11-13 09:42:01.816527+0800 Snippets[16462:1157182] group-task-S2 using 3.011 seconds finishing task
2020-11-13 09:42:01.816773+0800 Snippets[16462:1157182] =========================
2020-11-13 09:42:03.813934+0800 Snippets[16462:1157532] ---task-T12 using 5.007 seconds finishing task ---
2020-11-13 09:42:03.814274+0800 Snippets[16462:1157182] group-task-S1 using 5.010 seconds finishing task
2020-11-13 09:42:03.814508+0800 Snippets[16462:1157182] =========================
*/

可以看见,用任务组dispatch_group约束来自不同队列的任务之后,程序依然可按照预期的流程执行。

详细示例见:使用dispatch_group约束任务的执行流程

信号量dispatch_semaphore

信号量适合控制一个(组)仅限于有限个用户访问的共享资源,信号量的初始值表示可同时访问的数量,或者共享资源的数量。

信号量只有两种操作方式,waitsignal,前者表示信号量减一,后者表示信号量加一。如果信号量为0,则需要等待,直至信号量为正方可进行后续操作。

在GCD中,信号量dispatch_semaphore的使用方法包括:

下面这个例子演示了海底捞火锅店的营业活动。

@implementation GCDSemaphoreExample
{
    dispatch_semaphore_t chairs; // 表示海底捞的椅子数量
}

- (instancetype)init{
    if ((self = [super init])) {
        chairs = dispatch_semaphore_create(10);
    }
    return self;
}

- (void)startOperation{
    NSLog(@"HiHotPot start operation");
        
    __block NSTimer *timer = [NSTimer timerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [self consumeHiHotPot];
    }];
    
    [NSRunLoop.mainRunLoop addTimer:timer forMode:NSDefaultRunLoopMode];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(20 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [timer invalidate];
        NSLog(@"HiHotPot end operation");
    });
}

- (void)consumeHiHotPot{
    NSLog(@"start waiting for chair...");
    dispatch_semaphore_wait(chairs, DISPATCH_TIME_FOREVER);
    NSLog(@"starting eating... ");
    
    NSUInteger duration = arc4random()%5;
    // 一定时间之后吃完,时间随机
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"finish eating...");
        dispatch_semaphore_signal(self->chairs);
    });
}

@end

完整示例见:用信号量模拟海底捞的营业活动

调度块dispatch_block

调度块是根据现有的block对象,根据特定信息在堆上创建一个新的调度块对象。

我们都知道,直接用GCD创建的任务一旦完成创建,就不能取消,只能等待执行。这在有些场景中就会出现问题,而调度块就能实现取消,但也有个条件:此任务还没有被执行

而且,它也可以结合任务组一起使用。

常用方法有:

dispatch_source

Dispatch Source API是一组对低层次系统对象进行监控的接口,比如监视其他进程变化、内存压力、文件修改等。

需要注意的是,创建好特定类型的Dispatch Source之后,要通过dispatch_resume或者dispatch_activate(更推荐)进行激活,因为它们是以非活动状态创建的。

进程PROC

用Dispatch Source监控进程状态的变化,比如退出、创建子进程等。

文件系统

int const fd = open([[dirUrl path] fileSystemRepresentation], O_EVTONLY);
if (fd < 0) {
        char buffer[80];
        strerror_r(errno, buffer, sizeof(buffer));
        NSLog(@"Unable to open \"%@\": %s (%d)", [dirUrl path], buffer, errno);
        return;
}

dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fd,
DISPATCH_VNODE_WRITE | DISPATCH_VNODE_DELETE, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(source, ^(){
        unsigned long const data = dispatch_source_get_data(source);
        if (data & DISPATCH_VNODE_WRITE) {
            NSLog(@"The directory changed.");
        }
        if (data & DISPATCH_VNODE_DELETE) {
            NSLog(@"The directory has been deleted.");
        }
});

dispatch_source_set_cancel_handler(source, ^(){
        close(fd);
});

dirSource = source;

dispatch_activate(dirSource);

完整示例见:用Dispatch Source监控文件系统

上一篇下一篇

猜你喜欢

热点阅读