多线程(一):NSThread

iOS三种多线程之NSThread

Posted by Ted on May 22, 2016

一、前言

线程术语

  • 线程(Thread):用于指代独立执行的代码段
  • 进程(Process):用于指代一个正在进行的可执行程序,它可以包含多个线程
  • 任务(task):用于指代抽象的概念,表示需要执行工作。

线程状态

线程建立(create)之后,

就进入三个状态中的任何一个:运行(running)、就绪(ready)、阻塞(blocked),

直到取消(cancled)或者退出(exited)。

新建线程

每个线程都代表一个代码的执行路径。每个应用程序启动时候都是一个线程,它执行程序的 main 函数。应用程序可以生成额外的线程,其中每个线程执行一个特定功能的代码。

当应用程序生成一个新的线程的时候,该线程变成应用程序进程空间内的一个实体。每个线程都拥有它自己的执行堆栈,由内核调度独立的运行时间片。一个线程可以和其他线程或其他进程通信,执行 I/O 操作,甚至执行任何你想要它完成的任务。因为它们处于相同的进程空间,所以一个独立应用程序里面的所有线程共享相同的虚拟内存空间,并且具有和进程相同的访问权限。

多线程问题

iOS主线程是1MB,子线程是512KB,花费时间 90 ms

多个线程更新相同的资源会导致数据的不一致(数据竞争)、停止等待事件的线程会导致多个线程相互持续等待(死锁)、使用太多线程会消耗大量的内存

与Run loop的关系

一个 run loop 是用来在线程上管理事件异步到达的基础设施。

一个 run loop 为 线程监测一个或多个事件源。当事件到达的时候,系统唤醒线程并调度事件到 run loop,然后分配给指定程序。如果没有事件出现和准备处理,run loop 把线程置于休 眠状态。你创建线程的时候不需要使用一个 run loop,但是如果你这么做的话可以给用户 带来更好的体验。Run Loops 可以让你使用最小的资源来创建长时间运行线程。因为 run loop 在没有任何事件处理的时候会把它的线程置于休眠状态,它消除了消耗 CPU 周期轮询,并防止处理器本身进入休眠状态并节省电源。

为了配置 run loop,你所需要做的是启动你的线程,获取 run loop 的对象引用, 设置你的事件处理程序,并告诉 run loop 运行。Cocoa 和 Carbon 提供的基础设施会 自动为你的主线程配置相应的 run loop。如果你打算创建长时间运行的辅助线程, 那么你必须为你的线程配置相应的 run loop。

与autoreleasepool的关系

每个Cocoa应用程序的线程都会维护一个它自己的autorelease pool栈,如果你显式地重新建立一个线程,你必须自己建立你自己的autorelease pool block。而GCD和NSOpearetion则会自动维护autoreleasepool,除非是大量的对象需要自己控制pool的释放时间。

因为自动释放池不会释放它的对象直到线程退出。长时运行的线程需求新建额外的自动释放池来更频繁的释放它的对象。比如,一个使用 run loop 的线程可能在每次运行完一次循环的时候创建并释放该自动释放池。更频繁的释放对象可以防止你的应用程序内存占用太大造成性能问题。也就是说一个普通线程的自动释放池在线程结束时才会把drain pool,而开启了run loop的线程会每次循环后释放并且重新建立。

线程同步

线程编程的危害之一是在多个线程之间的资源争夺。如果多个线程在同一个时间 试图使用或者修改同一个资源,就会出现问题。缓解该问题的方法之一是消除共享资 源,并确保每个线程都有在它操作的资源上面的独特设置。因为保持完全独立的资源 是不可行的,所以你可能必须使用锁,条件,原子操作和其他技术来同步资源的访问。 每当对象创建出来,它的生命就已经开始了,一直到操作系统释放了 该对象,对象的生命才结束。

线程优先级

你创建的任何线程默认的优先级是和你本身线程相同。内核调度算法在决定该运行那个线程时,把线程的优先级作为考量因素,较高优先级的线程会比较低优先级的线程具有更多的运行机会。较高优先级不保证你的线程具体执行的时间,只是相比较低优先级的线程,它更有可能被调度器选择执行而已。

二、Pthreads

POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程,这是一套在很多操作系统上都通用的多线程API,所以移植性很强(然并卵),当然在 iOS 中也是可以的。不过这是基于 c语言的框架;

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    pthread_t thread;
    //创建一个线程并自动执行
    pthread_create(&thread, NULL, start, NULL);
}
void *start(void *data) {
    NSLog(@"%@", [NSThread currentThread]);
    return NULL;
}

三、NSThread

我们应该避免显式地创建线程,你可以考虑使用异步 API,GCD 方式,或操作对象来实现并发,而不是自己创建一个线程。这些技术背后为你做了线程相关的工作,并保证是无误的。此外,比如 GCD 和操作对象技术被设计用来管理线程,比通过自己的代码根据当前的负载调整活动线程的数量更高效

1、创建线程的方式

NSThread实例方法:

- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument
- (void)start

NSThread类方法:

+ (void)detachNewThreadWithBlock:(void (^)(void))block 
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

Datach Thread(脱离线程),允许系统在线程完成时立即释放它的数据结构。

2、NSThread线程操作

//取消线程,并不是停止线程,这个只是一个标志位,对应isCanceled
- (void)cancel;

//启动线程
- (void)start;

//判断某个线程的状态的属性
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isCancelled) BOOL cancelled;

//设置和获取线程名字
-(void)setName:(NSString *)n;
-(NSString *)name;

//获取当前线程信息
+ (NSThread *)currentThread;

//退出线程 不推荐使用,杀死一个线程阻止了线程本身的清理工作。线程分配的内存可能造成泄露,并且其他线程当前使用的资源可能没有被正确清理干净,之后造成潜在的问题
+(void)exit

//获取主线程信息
+ (NSThread *)mainThread;

//使当前线程暂停一段时间,或者暂停到某个时刻
+ (void)sleepForTimeInterval:(NSTimeInterval)time;
+ (void)sleepUntilDate:(NSDate *)date;

3、设置优先级

较高优先级的线程会比较低优先级的线程具有更多的运行机会

img

4、线程间的通信

线程间通信分为两种,一个是线程间数据的传递,另外一种是一个线程完成后到另外一个线程继续执行任务,NSThread提供了两个方法来进行线程通信

第一种方式为performSelector, runloop是实现performOnThread的基础——目标thread必须有runloop在运行,因为该实现的原理是基于runloop源的

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait  

主线程比较特殊,有专门的方法

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait; //wait如果传入的是YES: 那么会等到主线程中的方法执行完毕, 才会继续执行下面其他行的代码

第2种为NSMachPort方式。NSPort有3个子类,NSSocketPort、NSMessagePort、NSMachPort,但在iOS下只有NSMachPort可用。

使用的方式为接收线程中注册NSMachPort,在另外的线程中使用此port发送消息,则被注册线程会收到相应消息,然后最终在主线程里调用某个回调函数。

可以看到,使用NSMachPort的结果为调用了其它线程的1个函数,而这正是performSelector所做的事情,所以,NSMachPort是个鸡肋。线程间通信应该都通过performSelector来搞定。

四、使用NSObject来异步编程

[obj performSelectorInBackground:@selector(backMethod) withObject:nil];
[obj performSelectorOnMainThread:@selector(toMain) withObject:nil waitUntilDone:NO];