进程与线程:NSThread,Runloop

29 8月

IOS中通常一个APP就一个进程,绝大部分情况都是使用多线程进行通信。

线程分主线程其他线程,主线程在APP存活时一直存在,通常就是UI操作,所以也被称为UI线程。IOS最核心的需求就是要保证用户操作的流畅,所以可能会影响UI体验的动作,例如网络操作,读取本地I/O等,都应该放到其他线程里,避免影响主线程。

现在几乎所有的GUI框架都是单线程的,原因可以点击这里。虽然是篇年代比较久远的文章,但原理不过时。很多人都尝试用多线程来渲染UI,让界面显示的速度更快,但均告失败,实在无法处理渲染时线程间的冲突。最终所有框架都宁可牺牲渲染效率,采用单线程来渲染UI,iOS也不例外。

  • NSThread
  • Runloop
  • 线程间通信
  • 线程间同步
  • GCD
  • NSOperation

NSThread

用于创建线程,线程创建并执行完操作后会自动销毁。有两种方式创建NSThread:

  • 使用detachNewThreadSelector来启动一个线程
  • 自己创建一个NSThread对象,然后调用它的start方法
// 方式一:使用detachNewThreadSelector来启动一个线程
- (void)createThread1
{
    [NSThread detachNewThreadSelector:@selector(myThreadMainMethod:) toTarget:self withObject:nil];
}

// 方式二:自己创建一个NSThread对象,然后调用它的start方法
- (void)createThread2
{
    NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                                 selector:@selector(myThreadMainMethod:) object:nil];
    [myThread start];
}

- (void)myThreadMainMethod:(id)arg
{
    NSLog(@"%@", @"multiple thread");
}

创建NSThread后,可以修改NSThread的默认配置项:

  • 设置线程的堆栈大小:setStackSize
  • 配置线程本地变量:threadDictionary
  • 设置线程优先级:setThreadPriority:设置0~1间的浮点数
- (void)myThreadMainMethod 
{
    self.thread = [[NSThread alloc] initWithTarget:self 
                                          selector:@selector(threadWithStackSize) object:nil];
    [self.thread setStackSize:204800000];  // 没这行会报错。每个线程的默认栈大小是有上限的,如果超过限制,需要手动提高上限
    [self.thread start];
}

- (void)threadWithStackSize
{
    int array[10240020];
    array[1] = 100;
}

创建NSThread后,可以用Runloop管理线程,Runloop是不能脱离线程独立存在的。

Runloop

创建NSThread后,系统为每个线程都提供了Runloop,包括主线程。

Runloop用于管理线程(/source/timer source),是不能脱离线程独立存在的。当线程启动runloop后,没有处理消息时休眠线程以避免占用资源,当有消息来时唤醒线程。

主线程默认启动runloop,其他线程默认是不会常驻内存的,需要启动runloop才能常驻内存。所以当我们不操作APP时,主线程因为有Runloop,不会被销毁而是进入睡眠等待。当用户有手势触发APP时,主线程会重新开始执行。

源码就是个简单的do-while循环:

要让线程常驻内存,以便再次执行任务,需要启动runloop,并且runloop中有source或timer,如果runloop里没有source或timer的话,runloop会立即关闭。

- (void)startRunloopHandleTaskInThread
{
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)run
{
    // 默认线程是不会常驻的,要让线程常驻内存,需要启动runloop,并且runloop中有source或timer
    NSRunLoop *rl =  [NSRunLoop currentRunLoop];
    
    // 如果runloop里没有source或timer的话,runloop会立即关闭。在runLoop中添加一个timer
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self 
                                                    selector:@selector(timerRun) userInfo:nil repeats:YES];
    [rl addTimer:timer forMode:NSRunLoopCommonModes];

    // 启动runloop
    [rl run];  
    
    // 如果线程成为了常驻线程,你会发现,不会执行到这行代码,即这个方法不会执行完
    NSLog(@"end");
}

-(void)timerRun
{
    NSLog(@"%s",__func__);
}

创建和销毁线程也是有开销的,通常我们不会创建个只执行一次的线程,常见的第三方库(如AFNetworking)会在创建线程执行任务时,在线程内启用runloop常驻内存。

可以使用:CFRunLoopGetMain获取主线程的runloop,CFRunLoopGetCurrent获取当前线程的runloop。

线程间通信

线程间通信方式有以下几种:

  • 直接通信
  • 条件变量
  • 全局变量,共享内存
  • Run loop sources
  • Ports and sockets
  • 消息队列
  • Cocoa分布式对象

直接通信:

- (void)testDirectMessaging
{
    self.thread =  [[NSThread alloc] initWithTarget:self selector:@selector(startThreadWithRunloop) object:nil];
    [self.thread start];  // 创建并启动线程,并启动runloop
    [self performSelector:@selector(printInOtherThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}

- (void)startThreadWithRunloop
{
    NSPort* workPort = [NSMachPort port];
    [[NSRunLoop currentRunLoop] addPort:workPort forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
}

- (void)printInOtherThread
{
    NSLog(@"%@", @"printInOtherThread");
}

条件变量(NSCondition)(lock,wait,waitUntilDate,signal,broadcast,unlock)主要解决生产者/消费者的问题。两个线程间用条件变量的同步线程的方式,一图胜千言:

@property (strong, nonatomic) NSMutableArray *products;
@property (strong, nonatomic) NSCondition *condition;

-(void)testCondition
{
    NSLog(@"开始生产者消费者模式");
    //创建一个生产者
    [NSThread detachNewThreadSelector:@selector(createProducer) toTarget:self withObject:nil];
    
    //创建三个消费者
    for (int i = 0; i < 3; i++) {
        NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(createConsuser) object:nil];
        thread.name = [NSString stringWithFormat:@"第%d个消费者",i];
        [thread start];
    }
    
}
- (void)createProducer {
    for (int i = 0; i < 9; i++) {
        [NSThread sleepForTimeInterval:arc4random() % 5];
        [self.condition lock];
        NSObject *product = [[NSObject alloc] init];
        [self.products addObject:product];
        NSLog(@"生产了一个产品");
        [self.condition signal];  // 发送信号通知消费者
        [self.condition unlock];
    }
}

- (void)createConsuser {
    while (true) {
        [self.condition lock];
        while (self.products.count == 0) {
            [self.condition wait];
        }
        [self.products removeObjectAtIndex:0];
        NSLog(@"%@消费了一个产品",[NSThread currentThread].name);
        [self.condition unlock];
    }
}

线程间同步

线程同步方式有以下几种:

  • 原子操作(Atomic Operations)
  • 内存栅栏(Memory Barriers and Volatile Variables)
  • 锁(NSLocking)
  • 条件变量(NSCondition)
  • Ports and sockets
  • Perform Selector Routines

锁(NSLocking)是最常见的同步方式。包括互斥量(Mutex),读写锁(Read-Write lock),重入锁(Recursive lock),分布锁(Distributed lock),自旋锁(Spin lock)等。

因为NSLocking是个协议,开发中常用NSLockNSRecursiveLockNSConditionLockNSDistributedLock等。也可以直接使用@synchronized关键字来加锁。

NSLock:常规锁

- (void)testNSLock
{
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadWithNSLock) object:nil];
    [self.thread start];
}

- (void)threadWithNSLock
{
    static int count = 0;
    if (count == 5) {
        return;
    }
    if (!self.lock) {
        self.lock = [NSLock new];
    }
    [self.lock lock];
    [NSThread sleepForTimeInterval:1];
    NSLog(@"%@-%d", @"threadWithNSLock-", count);  // 只执行1次,count为1
    count++;
    [self threadWithNSLock]; // 递归调用自己,因为未解锁,所以count永远为1,不会自增
    [self.lock unlock];      // 因为上面递归的存在,这行永远不会被执行到
}

NSRecursiveLock:重入锁

- (void)testNSRecursiveLock
{
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadWithNSRecursiveLock) object:nil];
    [self.thread start];
}

- (void)threadWithNSRecursiveLock
{
    static int count = 0;
    if (count == 5) {
        return;
    }
    if (!self.recursiveLock) {
        self.recursiveLock = [NSRecursiveLock new];
    }
    [self.recursiveLock lock];
    [NSThread sleepForTimeInterval:1];
    NSLog(@"%@-%d", @"threadWithNSRecursiveLock-", count);
    count++;
    [self threadWithNSRecursiveLock];  // 同样是递归,同样未解锁,但NSRecursiveLock可以重入,所以count累加了5次,为5
    [self.recursiveLock unlock];       // 锁重入5次后,即count累加5次后,这行解锁会被执行5次
}

@synchronized有以下特性:

  • 只有传同样的对象给synchronized,才能起到加锁作用。传nil是无法起到加锁作用的。
  • 可以重入
  • synchronized不会持有传给它的对象,所以在synchronized里面销毁了这个对象,也不会有问题。
  • synchronized内部有异常处理,所以不用担心异常产生的时候不会unlcok
@property (nonatomic) NSMutableArray *testArray;

- (void)testSynchronize
{    
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(synchronizedAdd) object:nil];
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(synchronizedRemove) object:nil];
    [thread1 start];
    [thread2 start];

    // 依次打印出 thread 1 add: 0~4
    // 再依次打印出thread remove count 5~1
}

- (void)synchronizedAdd
{
    for (NSInteger i = 0; i < 5; i++) {
        @synchronized (self) {
            NSLog(@"thread 1 add: %ld", (long)i);
            [self.testArray addObject:@(i)];
            sleep(1);
        }
    }
}

- (void)synchronizedRemove
{
    while (1) {
        @synchronized (self) {
            if (self.testArray.count > 0) {
                NSLog(@"thread remove count %lu", self.testArray.count);
                [self.testArray removeObjectAtIndex:(self.testArray.count-1)];
            }
            sleep(1);
        }
    }
}

GCD

点这里

NSOperation

点这里

发表评论

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