iOS线程中的概念及类

整理一下iOS多线程开发时遇到的一些点。因涉及的知识点有点繁杂,故有些乱。只做个人备忘,不适合入门学习用。可能会保持更新。

NSThread

很简单是不是?就是单纯的线程,这与计算机中的概念是一致的。NSThread能够很方便地控制单个线程中执行的代码,但是管理多个线程非常困难。

GCD

在编程中,其实更多关系的,还是任务的调度。一个任务要不要异步执行,要不要并发执行…而”异步”、”并发”等概念其实并不等同于Thread,可以将GCD看成是对Thread的一层包装,而这层包装使得开发者可以关系并发编程中上层的概念,代码块根据需求在不同的线程中执行,而开发者不需关系究竟在哪个线程中执行,也不需关心自己需要创建多少线程。GCD使得创建任务调度分外简单。 无论什么队列和什么任务,线程的创建和回收都不需要程序员参与.线程的创建回收工作是由队列负责的. GCD在后端管理着一个线程池,GCD不仅决定着代码块将在哪个线程被执行,它还根据可用的系统资源对这些线程进行管理,从而让开发者从线程管理的工作中解放出来;通过集中的管理线程,缓解大量线程被创建的问题.

dispath中的文件及功能:

  • <dispatch/base.h>:定义dispatch_function_t
  • <dispatch/block.h>:定义dispatch_block_t结构体, dispatch_block_create等方法
  • <dispatch/data.h>:定义dispatch_data_create等方法
  • <dispatch/group.h>:与队列组有关, dispatch_group_create, dispatch_group_async, dispatch_group_wait, dispatch_group_notify, dispatch_group_enter, dispatch_group_leave等方法<什么时候用group?>
  • <dispatch/io.h>:与io有关
  • <dispatch/object.h>:定义dispatch_object_t,结构体, dispatch_release/dispatch_retain, dispatch_suspend/dispatch_resume, dispatch_wait, dispatch_notify, dispatch_cancel, dispatch_debug 等方法
  • <dispatch/once.h>:一次性初始化
  • <dispatch/queue.h>:dispatch_apply, dispatch_get_main_queue, dispatch_get_global_queue, dispatch_queue_create, dispatch_after, dispatch_barrier_async, dispatch_barrier_sync, dispatch_queue_set_specific, dispatch_queue_get_specific, dispatch_get_specific等方法.
  • <dispatch/semaphore.h>:创建使用等待信号量
  • <dispatch/source.h>:<源?>
  • <dispatch/time.h>:时间, NSEC_PER_SEC, NSEC_PER_MSEC, USEC_PER_SEC, NSEC_PER_USEC等数值定义.

dispatch_async表示函数立刻返回, dispatch_sync则表示需要等待执行完block才返回。所以dispatch_async会在当前线程之外使用另外一个线程执行block操作, 而dispatch_sync很可能使用当前线程执行block,但似乎也不一定如此. dispatch_barrier_async提交一个代码块, 函数立刻返回. 被提交到队列中的代码块不会立刻执行, 而是要等到队列中先前提交的正在执行的代码块(在并发队列中,凡是先前提交的代码块,应该都处于正在执行或者已经执行完了状态中吧)都执行完了才执行; 如果是串行队列或global并发队列,dispatch_barrier_async会退化成类似dispatch_async的行为. dispatch_barrier_sync要等此提交的代码块执行完了才返回.

 dispatch_group

dispatch_group_async 与 dispatch_group_enter/dispatch_group_leave组完成的功能是相同的,都是将代码块放到group中,使其能够受到group的控制。dispatch_group_async的使用会更方便一些,不过一些情况限制下,只能通过dispatch_group_enter/dispatch_group_leave将代码块放归到组中。可参考:Wait until multiple networking requests have all executed – including their completion blocks

NSOperation/NSOperationQueue

GCD是一系列C中的底层函数,而NSOperation将其封装了起来,并提供了GCD中不太容易完成的功能,比如取消队列,限制最大并发数量,操作之间的依赖关系.NSOperation有两个子类: NSBlockOperation, NSInvocationOperation. NSOperation通过设置maxConcurrentOperationCount属性来表示并发或者串行. 通过addDependency方法来表示任务之间的依赖关系.

锁的使用

一份资源如果可能在多个线程中读写, 则面临加锁的问题.

Cocoa中的锁

包含:NSLock、NSRecursiveLock、NSConditionLock(MAC OS中还有NSDistributedLock)。注意这几个类并非是继承关系,而是实现了同一协议NSLocking的不同的类,他们的父类都是NSObject.

TestMethod = ^(int value){
  [theLock lock];
  if (value > 0){
    [aObject method1];
    sleep(5);
    TestMethod(value-1);
  }
  [theLock unlock];
};
TestMethod(5);

上面的代码会产生死锁,因为迭代调用了包含lock的代码,线程会尝试获取已经被自己获取的锁。解决此问题的方法是使用NSRecursiveLock。如文档所述,NSRecursiveLock defines a lock that may be acquired multiple times by the same thread without causing a deadlock。

可以使用:@synchronized

指令@synchronized()通过对一段代码的使用进行加锁。其他试图执行该段代码的线程都会被阻塞,直到加锁线程退出执行该段被保护的代码段,也就是说@synchronized()代码块中的最后一条语句已经被执行完毕的时候。指令@synchronized()需要一个参数。该参数可以使任何的Objective-C对象,包括self。这个对象就是互斥信号量。他能够让一个线程对一段代码进行保护,避免别的线程执行该段代码。针对程序中的不同的关键代码段,我们应该分别使用不同的信号量。

在此需要注意效率的问题. 尽量将表示信号量的对象范围控制得得当一些,否则可能造成修改不同的变量却必须要等待的问题,有点像数据库中不同粒度的加锁问题。比如,一个类中有三个函数A,B,C都使用了@synchronized,A,B会修改同一个私有变量,但是C与之无关。那么当线程在A中捕获信号量之后,C中的代码就无法被其他线程处理了,C无故躺枪。在此,A,B可以使用同一个信号量对象,而C应该使用另一个不同的。另外,iOS默认没有Java中的internString,所以即使是相同的字符串,所构成的NSString对象可能是不同的。若要用NSString作为信号量对象,则需要把相同的字符串统一到相同对象地址,可使用NSString-intern扩展。Objective-C中的同步特性是支持递归的。一个线程是可以以递归的方式多次使用同一个信号量的。

除了用专门的锁,也可以用GCD中的队列解决并行访问资源问题

用串行同步队列,将读写操作全部放在序列化的队列里执行。看代码:

- (NSString *)someString{
  __block NSString *localSomeString;
  dispatch_sync(_syncQueue, ^{
    localSomeString = _someString;
  });
  return localSomeString;
}
- (void)setSomeString:(NSString *)someString{
  dispatch_sync(_syncQueue, ^{
    _someString = someString;
  });
}

上述代码中,把 dispatch_sync 改成 dispatch_async不一定更好。因为dispatch_async会拷贝block,如果拷贝耗时超过了block代码块的运行时间,整体效率反而会降低,所以使用的时候需要做好衡量。

GCD中的group

dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block); 方法会在group中的所有queue执行完之后就执行。

信号量

主要适用于生产-消费问题。也可以被当做锁使用(个人认为不太好).

1. 创建信号量,可以设置信号量的资源数。0表示没有资源,调用dispatch_semaphore_wait会立即等待。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
2. 等待信号,可以设置超时参数。该函数返回0表示得到通知,非0表示超时。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
3. 通知信号,如果等待线程被唤醒则返回非0,否则返回0。
dispatch_semaphore_signal(semaphore);
最后,还是回到生成消费者的例子,使用dispatch信号量是如何实现同步:

dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //消费者队列
  while (condition) {
    if (dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 10*NSEC_PER_SEC)){ //等待10秒
      continue; //得到数据
    }
  }
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //生产者队列
  while (condition) {
    if (!dispatch_semaphore_signal(sem)){
      sleep(1); //通知成功
      continue;
  }
});

队列

下图反映了GCD中涉及的队列结构:

GCD队列示意图
GCD队列示意图

在自定义队列中被调度的所有Block最终都将被放入到系统的全局队列中和线程池中.

void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));

重复执行block,等到所有block执行完毕才返回,如需异步返回则嵌套在dispatch_async中来使用。多个block的运行是否并发或串行执行也依赖queue的是否并发或串行。

void dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t queue);

它会把需要执行的任务对象指定到不同的队列中去处理,这个任务对象可以是dispatch队列,也可以是dispatch对象,可以实现队列的动态调度。

dispatch_set_target_queue(dispatchA, dispatchB);

那么dispatchA上还未运行的block会在dispatchB上运行。这时如果暂停dispatchA运行:dispatch_suspend(dispatchA); 则只会暂停dispatchA上原来的block的执行,dispatchB的block则不受影响。而如果暂停dispatchB的运行,则会暂停dispatchA的运行。

 

RunLoop

在研究RunLoop的时候,可以将GCD都忘掉,因为RunLoop归根到底是线程中的产物。RunLoop是消息机制的处理模式。可以认为一个RunLoop对象创建之后就在不停地检测系统事件(源),并把这些事件归类好放到不同Mode的池子中。线程中可能会有代码去检测RunLoop中有没有时间,如果有事件的话则处理,否则就休眠。iOS中表示RunLoop的类是NSRunLoop。每一个线程都会有一个默认的NSRunLoop对象,只不过在主线程中,NSRunLoop对象是被不断检测并处理的,而在用户自己创建的线程中,NSRunLoop中检测到的事件并没有被处理。如果想让子线程处理NSRunLoop中的事件,则需要有如下代码:

while (YES) {
if([[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeInterval:5 sinceDate:startTime]])
{
NSLog(@"%@",@"runloop test thread end.");
break;
}
}

下面剖析一下NSRunLoop类中的相关概念或函数。

- (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode;

将定时器添加到RunLoop中,指定时间之后,”到时间了”这个事件就会被添加到RunLoop中,而RunLoop执行时,就会检测到这个事件源,并执行Timer中描述的事件处理方法,所以RunLoop会retain这个Timer。如果”到时间了”这个事件被添加的时候,RunLoop中恰好有很多事情要做,消耗的时间超过Timer的周期,下一次”到时间了”这个事件交给RunLoop的时候,RunLoop中还有同样的事件没有处理,那么RunLoop就会抛弃第二次”到时间了”这个事件。即在一个周期内只会触发一次。如果需要移除一个Timer,则需要发送invalidate消息给Timer。

- (void)addPort:(NSPort *)aPort forMode:(NSString *)mode;
- (void)acceptInputForMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

Mode有两种:

  • NSDefaultRunLoopMode
  • NSRunLoopCommonModes

在将事件源添加到RunLoop时,有可能指定(或默认)一个mode,在执行RunLoop中的事件时,也可指定(或默认)一个mode,只有指定的mode中的事件才会得到处理。NSDefaultRunLoopMode处理除NSConnection对象以外的输入源, NSDefaultRunLoopMode: Objects added to a run loop using this value as the mode are monitored by all run loop modes that have been declared as a member of the set of “common” modes; see the description of CFRunLoopAddCommonMode for details.

- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

NSObject (NSDelayedPerforming)是写在NSRunLoop.h文件中的,可见延时处理与NSRunLoop密切相关。

NSTimer

初始化方法如下:

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep
- (void)fire;
@property (copy) NSDate *fireDate;

Timer就是一个能在从现在开始的后面的某一个时刻或者周期性的执行我们指定的方法的对象。为保证Timer中指定的事件处理方法在未来某时刻有效,timer对象会retain target对象,不管是重复性的timer还是一次性的timer都会对它的方法的接收者进行retain,这两种timer的区别在于“一次性的timer在完成调用以后会自动将自己invalidate,而重复的timer则将永生,直到你显示的invalidate它为止”。如果target对象恰好也retain了timer,那么就会造成循环引用。在target与timer互相retain的情况下, 常出现的一种错误写法是在target的dealloc方法中invalidate timer,自然是不可以的; 因为target永远无法进入执行dealloc 方法.即使将target的weak类型传入也无法解决此问题,有效的处理方法是对target外包一层传入timer[参:Weak Reference to NSTimer Target To Prevent Retain Cycle].

NSTimer不是一个实时系统,假设你添加了一个timer指定2秒后触发某一个事件,但是签好那个时候当前线程在执行一个连续运算(例如大数据块的处理等),这个时候timer就会延迟到该连续运算执行完以后才会执行。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会和后面的触发进行合并,即在一个周期内只会触发一次。但是不管该timer的触发时间延迟的有多离谱,他后面的timer的触发时间总是倍数于第一次添加timer的间隙。

其他一些概念

Critical Section 临界区
就是一段代码不能被并发执行,也就是,两个线程不能同时执行这段代码。这很常见,因为代码去操作一个共享资源,例如一个变量若能被并发进程访问,那么它很可能会变质(译者注:它的值不再可信)。

Race Condition 竞态条件
这种状况是指基于特定序列或时机的事件的软件系统以不受控制的方式运行的行为,例如程序的并发任务执行的确切顺序。竞态条件可导致无法预测的行为,而不能通过代码检查立即发现。

Thread Safe 线程安全
线程安全的代码能在多线程或并发任务中被安全的调用,而不会导致任何问题(数据损坏,崩溃,等)。线程不安全的代码在某个时刻只能在一个上下文中运行。一个线程安全代码的例子是 NSDictionary 。你可以在同一时间在多个线程中使用它而不会有问题。另一方面,NSMutableDictionary 就不是线程安全的,应该保证一次只能有一个线程访问它。Apple提供了一份摘要来说明哪些类是线程安全的, 哪些不是。

Context Switch 上下文切换
一个上下文切换指当你在单个进程里切换执行不同的线程时存储与恢复执行状态的过程。这个过程在编写多任务应用时很普遍,但会带来一些额外的开销。

延时操作

iOS中有以下几类方式可以实现延时操作:

perform系列, 来自NSObject(NSDelayPerforming)分类,具体API如下:
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;

perform方法将在一段时间后于当前线程执行以modes(或默认)修饰的方法执行aSelector. 此方法将会在当前线程的RunLoop中创建一个timer. 由于默认情况下,只有主线程才有RunLoop, 所以如果当前线程没有启用RunLoop的话perform方法将会无效. 故使用perform编写的模块可能在非主线程中运行异常. 可参考Threading Programming Guide中RunLoop部分. <计时过程中如果销毁对象会不会引起内存泄露?>. 静态方法cancelPreviousPerformRequestsWithTarget会取消加入RunLoop的尚未执行的selector。

dispatch_after系列, 来自GCD
dispatch_after(dispatch_time_t when,dispatch_queue_t queue,dispatch_block_t block); // 执行方法块
	dispatch_after_f(dispatch_time_t when,dispatch_queue_t queue,void *context,dispatch_function_t work); // 执行方法函数

dispatch_after方法是延时将方法块添加到队列中. 所以方法块真正的执行时间很可能会晚于when 所描述的时间. <是用什么计时的>.

sleep相关函数

包括NSThread的

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

以及c库中的sleep(秒)、usleep(微秒)函数.这些函数都是在阻塞当前线程,当然会延迟一个方法块的执行,不过在延时期间,当前线程中的其他方法块也是得不到CPU时间的.

使用dispatch source
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC, 5 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
    // event handler
});
dispatch_resume(timer);

有些类似NSTimer与RunLoop. GCD可以监听系统事件与时间事件源,当到了一定时间之后便执行handler. 上面的代码里,先生成了时间事件源并添加到队列里(类似RunLoop), 然后设置源的相关属性(类似NSTimer),在绑定事件源的handler,源创建之后是挂起的,所以最后再调用dispatch_resume方法启动源。当下一次源事件被触发之后,handler里的方法就会执行了。

参考资料

Tagged with: , , , , ,

发表评论

邮箱地址不会被公开。 必填项已用*标注

*