文章

多线程:NSThread

特点

  • 简单易用,可以直接操作线程对象
  • 一个 NSThread 对象就是一个线程
  • 线程的生命周期需要程序员手动管理
  • 线程在执行完任务后就会退出并销毁

NSThread 简单使用


  • 先创建线程,再启动线程
1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)useNSThread {
    // 1. 创建线程
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    
    // 2. 启动线程
    [thread start];

}

// 新线程调用方法,里边为需要执行的任务
- (void)run {
     NSLog(@"%@", [NSThread currentThread]);
}


  • 创建线程后自动启动线程
1
2
3
4
5
6
// 创建线程后自动启动线程
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];

- (void)run {
     NSLog(@"%@", [NSThread currentThread]);
}


  • 隐式创建并启动线程
1
2
3
4
5
6
7
8
// 隐式创建并启动线程
[self performSelectorInBackground:@selector(run) withObject:nil];

// 在新线程执行方法
- (void)run {
     NSLog(@"%@", [NSThread currentThread]);
}


线程相关用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 获得主线程
+ (NSThread *)mainThread;    

// 判断是否为主线程(对象方法)
- (BOOL)isMainThread;

// 判断是否为主线程(类方法)
+ (BOOL)isMainThread;    

// 获得当前线程
NSThread *current = [NSThread currentThread]; 

// 线程的名字
- (NSString *)name; 

// 启动线程,线程状态:新建状态 -> 就绪状态。当线程任务执行完毕,自动进入死亡状态
- (void)start;

// 阻塞线程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

// 强制立即终止当前执行的任务,线程进入死亡状态
+ (void)exit;

// 取消线程,调用该方法会设置 cancelled 属性为 YES,但并不退出线程
- (void)cancel;


  • 线程之间的通信 在开发中,我们经常会在子线程进行耗时操作,执行完再回到主线程更新 UI。
1
2
3
4
5
6
7
8
9
10
11
12
// 在主线程上执行操作
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array;

// 在指定线程上执行操作
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;

// 在当前线程上执行操作
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;


  • 下面通过一个经典的下载图片 DEMO 代码来展示线程之间的通信:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
 * 创建一个线程下载图片
 */
- (void)downloadImageOnSubThread {
    // 在创建的子线程中调用downloadImage下载图片
    [NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];
}

/**
 * 下载图片,下载完之后回到主线程进行 UI 刷新
 */
- (void)downloadImage {
    NSLog(@"current thread -- %@", [NSThread currentThread]);
    
    // 1. 获取图片 imageUrl
    NSURL *imageUrl = [NSURL URLWithString:@"https://ysc-demo-1254961422.file.myqcloud.com/YSC-phread-NSThread-demo-icon.jpg"];
    
    // 2. 从 imageUrl 中读取数据(下载图片) -- 耗时操作
    NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
    // 通过二进制 data 创建 image
    UIImage *image = [UIImage imageWithData:imageData];
    
    // 3. 回到主线程,YES 表示立即进行图片赋值和界面刷新
    [self performSelectorOnMainThread:@selector(refreshOnMainThread:) withObject:image waitUntilDone:YES];
}

/**
 * 回到主线程进行图片赋值和界面刷新
 */
- (void)refreshOnMainThread:(UIImage *)image {
    NSLog(@"current thread -- %@", [NSThread currentThread]);
    
    // 赋值图片到imageview
    self.imageView.image = image;
}



NSThread 线程安全

线程安全的解决方案就是加锁。iOS 实现线程加锁有很多种方式:@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) set/get 等等各种方式。为了简单起见,这里不对各种锁的使用方式和性能做分析,只用最简单的 @synchronized 来保证线程安全,从而解决线程同步问题。

使用 @synchronized 包围的代码即为同步代码块,同步代码块需要一个监听器,我们使用 self 本身作为监听器,因为是 self 对象产生的竞争条件,当执行同步代码块时需要先获取监听器,如果获取不到则线程会被阻塞,当同步代码块执行完成则释放监听器。

考虑线程安全的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * 售卖火车票(线程安全)
 */
- (void)saleTicketSafe {
    while (1) {
        // 互斥锁
        @synchronized (self) {
            //如果还有票,继续售卖
            if (self.ticketSurplusCount > 0) {
                self.ticketSurplusCount --;
                NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]);
                [NSThread sleepForTimeInterval:0.2];
            } else {
                NSLog(@"所有火车票均已售完");
                break;
            }
        }
    }
}



NSCondition 锁的使用

NSCondition 实现了NSLocking协议,所以它同样具有锁的功能,与NSLock一样可以获取锁与释放锁的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NS_CLASS_AVAILABLE(10_5, 2_0)
@interface NSCondition : NSObject <NSLocking> {

/*
阻塞线程,直到其他线程调用该对象的 signal 方法或 broadcast 方法来唤醒
唤醒后该线程从阻塞态变为就绪态,交由系统进行线程调度
调用 wait 方法时内部会自动执行 unlock 方法释放锁,并阻塞线程
*/
- (void)wait;

// 同上,只是该方法是在 limit 到达时唤醒线程
- (BOOL)waitUntilDate:(NSDate *)limit;

// 随机唤醒一个在当前 NSCondition 对象上阻塞的一个线程,使其从阻塞态进入就绪态
- (void)signal;

// 唤醒在当前 NSCondition 对象上阻塞的所有线程
- (void)broadcast;

// 设置名称
@property (nullable, copy) NSString *name;

@end


使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// 取钱
- (void)draw:(id)money {
    // 设置消费者取钱20次
    NSUInteger count = 0;
    
    while (count < 20) {
        // 上锁,如果其他线程已经上锁则阻塞
        [self.condition lock];
        
        // 判断是否有钱
        if (self.haveMoney) {
            // 有钱则进行取钱的操作,并设置 haveMoney 为 NO
            self.balance -= [money doubleValue];
            self.haveMoney = NO;
            count += 1;
            NSLog(@"%@ draw money %lf %lf", [[NSThread currentThread] name], [money doubleValue], self.balance);
            
            // 取钱操作完成后唤醒其他在等待的所有线程
            [self.condition broadcast];
        } else {
            // 如果没有钱则在此等待,并阻塞
            [self.condition wait];
            // 如果阻塞的线程被唤醒后会继续执行代码
            NSLog(@"%@ wake up", [[NSThread currentThread] name]);
        }
        
        // 释放锁
        [self.condition unlock];
    }
}

// 存钱
- (void)deposite:(id)money {
    // 创建了三个取钱线程,每个取钱20次,则存钱60次
    NSUInteger count = 0;
    
    while (count < 60) {
    
        // 上锁,如果其他线程上锁了则阻塞
        [self.condition lock];
        
        // 判断如果没有钱则进行存钱操作
        if (!self.haveMoney) {
            // 进行存钱操作,并设置 haveMoney 为 YES
            self.balance += [money doubleValue];
            self.haveMoney = YES;
            count += 1;
            NSLog(@"Deposite money %lf %lf", [money doubleValue], self.balance);
            
            // 唤醒其他所有在等待的线程
            [self.condition broadcast];
        } else {
            // 如果有钱则等待
            [self.condition wait];
            NSLog(@"Deposite Thread wake up");
        }
        
        // 释放锁
        [self.condition unlock];
    }
}

- (void)useNSCondition {
    
    Account *account = [[Account alloc] init];
    account.accountNumber = @"1603121434";
    account.balance = 0;
    
    // 消费者线程1,每次取 1000 元
    NSThread *thread1 = [[NSThread alloc] initWithTarget:account selector:@selector(draw:) object:@(1000)];
    [thread1 setName:@"consumer1"];
    
    // 消费者线程2,每次取 1000 元
    NSThread *thread2 = [[NSThread alloc] initWithTarget:account selector:@selector(draw:) object:@(1000)];
    [thread2 setName:@"consumer2"];
    
    // 生产者线程3,每次存 1000 元
    NSThread *thread3 = [[NSThread alloc] initWithTarget:account selector:@selector(deposite:) object:@(1000)];
    [thread3 setName:@"productor"];
    
    [thread1 start];
    [thread2 start];
    [thread3 start];
}



线程的状态转换

线程的几种状态:新建 - 就绪 - 运行 - 阻塞 - 死亡

  • 新建一条线程时,线程是新建状态。
  • 调用 [thread start]; 后,系统把线程对象放入可调度线程池中,线程对象进入就绪状态。
  • 如果 CPU 正在调度当前线程对象,则当前线程对象进入运行状态,如果调度其他线程,则当前线程又回到就绪状态。
  • 如果正在运行的线程调用了sleep方法 \ 等待同步锁,则当前线程就进入了阻塞状态,等到sleep到时 \ 得到同步锁,就会回到就绪状态。
  • 如果正在运行的线程 任务执行完 \ 异常退出 \ 调用exit方法,则当前线程对象进入死亡状态。


thread_state

本文由作者按照 CC BY 4.0 进行授权