Notes

多线程

Link 部分 Demo

使用performSelectors前缀的方法

使用NSThread

创建一个任务

创建任务

NSThread *purchaseA = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:customerA];

执行任务

[purchaseA start];

使用NSOperation

NSOperationQueue

更多地,NSOperation 将会与 NSOperationQueue 结合使用

同样,NSOperationQueue 也可以通过 KVO 进行监听

通过 [self.operationQueue addOperations:operations waitUntilFinished:NO]; 可以一次添加多个任务,waitUntilFinished 用于指定是否阻塞当前线程,一般都是 NO

使用GCD

GCD队列

  1. 串行分发队列 (Serial dispatch queue)
    • 私有分发队列 (private concurrent queue)
    • 按顺序执行队列中的任务
    • 同一时间只执行一个任务
    • 常用于实现 同步锁
    • dispatch_queue_t serialQueue = dispatch_queue_create("com.example.MyQueue", NULL);
  2. 并发分发队列 (Concurrent dispatch queue)
    • 全局分发队列
    • 按顺序执行队列中的任务(同 串行分发队列),但
    • 顺序开始的多个任务会 并发同时执行
    • 常用于 管理并发任务
    • dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  3. 主分发队列 (Main dispatch queue)
    • 特殊串行分发队列,全局唯一
    • 主线程执行,用于执行 UI 相关操作
    • dispatch_queue_t mainQueue = dispatch_get_main_queue();
    • 其他主题
      • 分发组 dispatch group
      • 信号 semaphores
      • 分发栅栏 dispatch barrier
      • 分发源 dispatch source
    • 建议使用 GCD
      • 使用 dispatch queue 实现同步锁
      • 替代 performSelector 系列方法
      • 使用 dispatch_once 实现线程安全单一执行要求:单例

同步锁的实现

  1. 使用属性的 atomic 属性
  2. 使用 @synchronized
  3. 使用 NSLock,极端状况下,有死锁的危险
// 使用 @synchronized 实现同步锁
- (void)methodNeedsToSynchronize:(id)identifier {
		/* Unique identifer 用于识别不同的同步锁,涉及同一个属性的不同线程应该使用同一个 identifier,一般使用 self,但涉及多个属性是,应使用不同的 identifier*/
		@synchronized(identifier/* Unique identifier*/) {
			// safe
		}
}

// 使用 NSLock 实现同步锁
_lock = [[NSLock alloc] init];
- (void)methodNeedsToSynchronize {
		[_lock lock];
		
		// safe
		
		[_lock unlock];
}

NSOperation>GCD

  1. 需要取消任务
  2. 需要更详细地观察任务的状态,NSOperation 使用 KVO 进行观察
  3. 需要重用线程任务,NSOperation 作为 Objective-C 对象,能存储更多的信息

串行分发队列与并行分发队列

串行分发队列

并发分发队列

线程内存管理

// 生成一个队列
dispatch_queue_t concurrentDispatchQueue = dispatch_queue_create("com.example.gcd.ConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

// 将任务通过 Block 分配给队列
dispatch_async(queue, ^{
	// Do something.
});

// 释放
dispatch_release(concurrentDispatchQueue);

/*
任务通过 dispatch_async 指派到队列后,立刻调用 dispatch_release 函数是正确的
1. dispatch_async 函数中追加 Block 到 concurrentDispatchQueue 中,
   Block 通过 dispatch_retain 函数持有 concurrentDispatchQueue
2. 随后立即调用 dispatch_release,由于 concurrentDispatchQueue 被 Block 持有,
   concurrentDispatchQueue 不会被释放
3. 当 Block 执行完毕后,释放 concurrentDispatchQueue,此时 concurrentDispatchQueue 被回收
*/

改变生成队列的优先级

// 创建自己的队列
dispatch_queue_t serialDispatchQueue = dispatch_queue_create("com.example.gcd.SerialDispatchQueue", NULL);

// 获取到系统提供队列,并且其优先级为目标优先级
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

// 设置优先级
dispatch_set_target_queue(serialDispatchQueue, globalDispatchQueueBackground);

延迟执行

// 设定时间
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);

// 执行
dispatch_after(time, dispatch_get_main_queue(), ^{
	// Do something.
});

dispatch_time_t类型

dispatch_time

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);

dispatch_walltime

dispatch_time_t dispatchTimeByDate(NSDate *date) {
	NSTimeInterval interval;
	double second, subsecond;
	struct timespec time;
	dispatch_time_t milestone;
	
	interval = [date timeIntervalSince1970];
	
	// 将小数分拆为整数与小数部分, interval 为整数部分,
	// &second 为指向存储小数部分位置的地址
	subsecond = modf(interval, &second);
	time.tv_sec = second;
	time.tv_nsec = subsecond * NSEC_PER_SEC;
	milestone = dispatch_walltime(&time, 0);
	
	return milestone;
}

派遣组,同步多个队列

在 Dispatch Queue 中处理多个任务后最后执行结束操作,只需要使用一个 Serial Dispatch Queue,并将所有任务追加到该 Dispatch Queue 中。 但在使用 Concurrent Dispatch Queue 时,或同时使用多个 Dispatch Queue 时,我们应该使用 Dispatch Group。

dispatch_group_async

// 1. 获得一个 并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// 2. 创建一个派遣组
dispatch_group_t group = dispatch_group_create();

// 3. 对 group 进行监视,并将任务添加到 queue 中
dispatch_group_async(group, queue, ^{/*block 1*/});
dispatch_group_async(group, queue, ^{/*block 2*/});
dispatch_group_async(group, queue, ^{/*block 3*/});

// 4. 添加任务完成之后的处理,即最后一步
dispatch_group_notify(group, dispatch_get_main_queue(), ^{/*Final task*/});

// 5. 同样,dispatch group 也需要被释放,ARC 不负责释放 Dispatch Queue 和 Dispatch Group
dispatch_release(group);

dispatch_group_wait

// 1. 创建一个队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// 2. 创建一个派遣组
dispatch_group_t group = dispatch_group_create();

// 3. 对 group 进行监视,并将任务添加到 queue 中
dispatch_group_async(group, queue, ^{/*block 1*/});
dispatch_group_async(group, queue, ^{/*block 2*/});
dispatch_group_async(group, queue, ^{/*block 3*/});

// 4. 等待所有任务结束
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

// 5. 同样,dispatch group 也需要被释放,ARC 不负责释放 Dispatch Queue 和 Dispatch Group
dispatch_release(group);

解决并发队列中数据竞争的问题

dispatch_async(queue, readingBlock1);
dispatch_async(queue, readingBlock2);
dispatch_async(queue, readingBlock3);
dispatch_async(queue, readingBlock4);

dispatch_barrier_async(queue, writingBlock);

dispatch_async(queue, readingBlock5);
dispatch_async(queue, readingBlock6);
dispatch_async(queue, readingBlock7);
dispatch_async(queue, readingBlock8);

dispatch_barrier_async

  1. dispatch_barrier_async 函数会等待追加到并发队列上的并发执行的任务全部结束(如上面代码的 readingBlock1~4 )
  2. 将指定的处理(如上面代码 writingBlock )追加到并发队列上
  3. 并发队列等待 2. 中的任务结束后,再恢复为一般的操作(继续并发执行,如继续执行上面代码的 readingBlock5~8 )

同步执行

dispatch_sync

队列的挂起与恢复

使用情境:当 Dispatch Queue 追加了大量的任务后,希望不执行已追加的任务。

dispatch_suspend

dispatch_resume


线程同步的信号量

// 这个书上的例子,目的是为了安全地向 array 中添加数据

// 将使用到系统提供的全局派遣队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// 生成信号量,并设置计数起始值为1
// 意味着:能访问 array 的线程,同时只能有1个
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

NSMutableArray *array = [[NSMutableArray alloc] init];

for(int i = 0; i < 10000; i++) {
	dispatch_async(queue, ^{
	
		// 一直等待,直到 semaphore 的值大于或等于1
		// 一直等待,以为着:在这个任务中,这行代码以后的代码,将暂停执行
		dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
		
		/*
			一直等待。。。。
		*/
		
		// 突然,上面那行代码等待结束了,将会发生一下事情
		// 1. semaphore 的计数值大于或等于1
		// 2. 为了安全地执行后面的任务,semaphore 又被减去1
		// 3. dispatch_semaphore_wait 等待结束,返回
		
		// 到了这个时候,semaphore 恒为0,当为0的时候,任务才能安全地执行
		// 保证访问 array 的线程同时只有1个
		
		[array addObject:@(i)];
		
		// 啊,任务安全地完成了
		// 将 array 释放给其他任务使用
		// 将 semaphore += 1
		
		// 上面等待 semaphore 的值大于等于1,就是等这个时候了
		dispatch_semaphore_signal(semaphore);
		
		// 如果存在其他任务是通过 dispatch_semaphore_wait 等待 semaphore >= 1 的
		// 则,按照等待的顺序依次执行(最先等待的先执行)
		
		// 写了这么多,其实这个例子中的任务就3行代码。。。
	});
}

/* ARC 里面,不用使用 dispatch_release(semaphore) 了
	即使你想这么做,开着 ARC 的编译器也不让你这么做
*/

dispatch_semaphore_create(信号量起始值n)

dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)

使用GCD作定时器

Runloop是用GCD的 dispatch_source_t 实现的 Timer, GCD 定时器不依赖 RunLoop 和 mode,比 NSTimer 更加准时,性能更好

自己封装的一个定时器

// PreciseTimer.h

@interface PreciseTimer : NSObject

/**
 使用 GCD 执行的定时器

 @param source 定时器,需要外界 hook 住
 @param delay 开始时间,距离现在的延时(秒)
 @param interval 定时操作的时间间隔
 @param execution 定时操作
 */
+ (void)preciseTimer:(dispatch_source_t *)source startAtDelayFromNow:(NSTimeInterval)delay interval:(NSTimeInterval)interval execute:(void(^)())execution;

@end
// PreciseTimer.m

#import "PreciseTimer.h"

@implementation PreciseTimer

+ (void)preciseTimer:(dispatch_source_t *)source startAtDelayFromNow:(NSTimeInterval)delay interval:(NSTimeInterval)interval execute:(void(^)())execution {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 创建起源
    dispatch_source_t _source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    // 创建回调时间间隔
    int64_t _interval = (int64_t)(interval * NSEC_PER_SEC);
    // 设置开始时间
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC));
    
    // 设置定时器
    dispatch_source_set_timer(_source, start, _interval, 0);
    
    // 设置定时回调
    dispatch_source_set_event_handler(_source, ^{
        execution();
    });
    
    // 启动定时器
    dispatch_resume(_source);
    *source = _source;
}

@end

使用示例

@interface ViewController ()

@property (nonatomic, strong) dispatch_source_t timer;

@end

@implementation ViewController

- (void)viewDidLoad {
	/*...*/
	
	dispatch_source_t timer = nil;
    [PreciseTimer preciseTimer:&timer startAtDelayFromNow:1 interval:2 execute:^{
        NSLog(@"here");
    }];
    // 记住要将 timer 捕获住,否则 timer 释放了之后就不会触发定时操作
    self.timer = timer;
}

@end
// 取消定时操作
dispatch_cancel(self.timer);

线程同步

NSLock 线程同步

// 创建一把锁
self.lock = [[NSLock alloc] init];

// 加锁
[self.lock tryLock];

// 解锁
[self.lock unlock];

线程与 RunLoop

线程需要挂靠在一个 RunLoop 下才能完整地完成任务

在 iOS 项目下,由于 iOS App 本身存在一个 RunLoop, 因此,如果 Demo 中的例子移植到 iOS 项目下跑,能够完整地完成

在命令行工具项目下,由于并不存在 RunLoop, 因此,程序将会从 main.c 中一股脑子向下执行,并且执行 return 0;. 而由于多线程的执行是异步的,任务其实也是可以进行,但是并不能确定任务执行的完整度,即任务在执行的半路中,程序已经退出了,因此,任务并没有完整地完成

于是,若要在命令行工具中,任务也能完成地完成,可以简单地在 main.c 的最后,return 0; 之上,添加一个死循环模拟 RunLoop, 当任务执行完成后,需要手动终止程序,否则 CPU 占用率会很高

// ...
while (YES) {};
return 0;

NSCondition 线程同步

NSCondition 既是一个锁,也是一个检查点(checkpoint)

使用 NSCondition 的正确方法

  1. 为 condition 加锁
  2. 测试一个条件,判断是否能执行任务
    • false, 不能执行任务,调用 wait 等方法阻塞当前线程,并循环检查条件
    • true, 可以执行任务
  3. 有需要的话,更新任务执行的条件,并调用 signalboardcast 方法发送信号
  4. 为 condition 解锁
// 创建一个 condition
self.condiction = [[NSCondition alloc] init];

// 为 condition 加锁
[self.condiction lock];

// 为 condition 解锁
[self.condiction unlock];

// 阻塞当前线程,等待信号发出后继续执行
[self.condiction wait];

// 向 condition 发送信号,唤醒一个等待的线程来执行任务
[self.condiction signal];

// 向 condition 发送信号,唤醒所有等待的线程来执行任务
[self.condiction broadcast];

并发与并行的区别

并发 concurrent 两条队共用一台咖啡机,线程们轮流交替在一个 CPU 中执行

并行 parralle 两条队各用一台咖啡机,线程们可以多个 CPU 执行

在并发编程中,需要理解清楚三个概念:

队列管理任务 任务在线程中执行 如果任务需要同步执行,则在当前线程执行 如果任务需要异步执行,则在另外一个线程(新线程)中执行 队列是一个任务容器,线程是任务的执行者

目前的理解:我们只需要管理并发,并行又系统管理