多线程之NSThread的简单使用

来源:互联网 发布:jquery 表单数据 编辑:程序博客网 时间:2024/05/17 22:34

  NSThread是基于Objective-C的,相比pthread而言,它使用起来更简单和方便。下面我们就新建一个工程,来看一下NSThread的简单使用。

一、NSThread的基本使用

  
  NSThread有三种开启子线程的方法,分别是- initWithTarget: selector: object:、- detachNewThreadSelector: toTarget: withObject:和- performSelectorInBackground: withObject:。下面就简单的演示一下这三种方法如何使用。

  1、使用- initWithTarget: selector: object:创建子线程

- (void)viewDidLoad {    [super viewDidLoad];    // 创建子线程    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test:) object:@"我是参数"];    // 给子线程命名    thread.name = @"我是子线程";    // 设置子线程的优先级    thread.threadPriority = 0.5;  // 取值范围是(0.0 ~ 1.0), 默认为0.5    // 启动线程    [thread start];}- (void)test:(id)argument {    NSLog(@"%@, %@", [NSThread currentThread], argument);}

  通过- initWithTarget: selector: object:方法来创建的子线程,它默认是处于暂停状态的,必须拿到线程对象调用- start方法来开启。另外,selector:后面传入的方法是可以带参数的,而这个参数就是通过object:后面的参数进行传递。

  再补充一点,关于线程的优先级,它的取值范围是从0.0 ~ 1.0,子线程创建完成以后,系统默认它的优先级是0.5。在同时创建有多个子线程的场合中,优先级高的子线程被系统执行的概率要高于优先级低的子线程。

  2、使用- detachNewThreadSelector: toTarget: withObject:创建子线程

- (void)viewDidLoad {    [super viewDidLoad];    // 创建子线程    [NSThread detachNewThreadSelector:@selector(test:) toTarget:self withObject:@"我是参数"];}- (void)test:(id)argument {    NSLog(@"%@, %@", [NSThread currentThread], argument);}

  - detachNewThreadSelector: toTarget: withObject:的使用方法跟- initWithTarget: selector: object:方法差不多,只不过,通过这种方式创建出来的子线程,你无法拿到它,这样一来,你就没办法给它设置优先级了。

  3、使用- performSelectorInBackground: withObject:创建子线程

- (void)viewDidLoad {    [super viewDidLoad];    // 创建子线程    [self performSelectorInBackground:@selector(test:) withObject:@"我是参数"];  // 开启一条后台线程}- (void)test:(id)argument {    NSLog(@"%@, %@", [NSThread currentThread], argument);}

  通过- performSelectorInBackground: withObject:创建的是一条隐式线程。它的使用方法跟- detachNewThreadSelector: toTarget: withObject:差不多,通过这种方式创建出来的子线程,你同样拿不到,你无法对线程进行更详细的设置。

二、使用NSThread创建的线程的生命周期

  
  我们来看一下NSThread的生命周期。新建一个继承自NSThread的ESThread,然后在这个类中重写- dealloc方法,最后用我们自定义的这个ESThread来替换NSThread:

- (void)viewDidLoad {    [super viewDidLoad];    // 创建子线程    ESThread *thread = [[ESThread alloc] initWithTarget:self selector:@selector(test:) object:nil];    // 给子线程命名    thread.name = @"我是子线程";    // 设置子线程的优先级    thread.threadPriority = 0.5;  // 取值范围是(0.0 ~ 1.0), 默认为0.5    // 启动线程    [thread start];}- (void)test:(id)argument {    for (int i; i < 1000; i++) {        NSLog(@"%d, %@", i, [NSThread currentThread]);    }}

  运行程序,看一下控制台打印信息:


NSThread的生命周期.png

  从控制台打印出来的消息看,NSThread是在子线程中的代码执行完毕以后才会被销毁的。

三、线程的状态

  
  线程是分状态的,这个在操作系统的基础知识中有学过。通常情况下,线程的运行分为三种状态:执行(运行)状态、就绪状态和阻塞状态。下面我们就分别来演示一下这三种状态。

  要想设置NSThread子线程的状态,首先就必须要拿到它。从上面的讲解可知,只有通过- initWithTarget: selector: object:方法创建的子线程才能拿得到:

- (void)viewDidLoad {    [super viewDidLoad];    // 创建子线程    ESThread *thread = [[ESThread alloc] initWithTarget:self selector:@selector(test:) object:nil];    // 给子线程命名    thread.name = @"我是子线程";    // 设置子线程的优先级    thread.threadPriority = 0.5;  // 取值范围是(0.0 ~ 1.0), 默认为0.5    // 启动线程(此时,线程处于就绪状态或者运行状态)    [thread start];}// MARK:- 阻塞线程- (void)test:(id)argument {    for (int i; i < 10; i++) {        NSLog(@"%d, %@", i, [NSThread currentThread]);    }    // 阻塞线程    [NSThread sleepForTimeInterval:3.0];  // 让线程进入阻塞状态以后,它是不能执行任何操作的    NSLog(@"%s, Line = %d", __FUNCTION__, __LINE__);    // 阻塞线程    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:3.0]];}

  创建完NSThread子线程对象以后,调用- start方法时,子线程就处于就绪或者运行状态。在子线程执行代码的过程中,通过调用- sleepForTimeInterval:方法或者- sleepUntilDate:方法就可以让它处于阻塞状态。

  如果要想让子线程在执行代码的中途退出,只需要调用- exit方法:

- (void)viewDidLoad {    [super viewDidLoad];    // 创建子线程    ESThread *thread = [[ESThread alloc] initWithTarget:self selector:@selector(test) object:nil];    // 给子线程命名    thread.name = @"我是子线程";    // 设置子线程的优先级    thread.threadPriority = 0.5;  // 取值范围是(0.0 ~ 1.0), 默认为0.5    // 启动线程(此时,线程处于就绪状态或者运行状态)    [thread start];}// MARK:- 退出线程- (void)test {    for (int i = 0; i < 100; i++) {        NSLog(@"%d", i);        if (i == 50) {            // 退出线程            [NSThread exit];  // 当i == 50 时,退出当前线程,有点类似于for循环里面的break,不过含义还是不一样的        }    }}

四、线程安全

  
  在iOS开发中,我们可以创建多个子线程,并且每个子线程都可以访问同一块资源,这样一来,就很容易引发数据错乱,甚至是数据安全问题:


多个子线程在同时访问同一块资源时可能存在的安全问题.png

  如上图所示,Thread A和Thread B都可以访问Integer,并且在访问过程中,它们都对Integer进行了加1操作,最终的结果应该是19。但是,可能在Thread A访问Integer并且将最终的数据18重新写入之前,恰好Thread B又对Integer进行了访问,它获得的数据还是原来的17(而不是Thread A重写后的18),此时Thread B对Integer进行加1操作以后就会得到错误的数据18。为了避免这种错误的发生,我们需要引入互斥锁技术:


利用互斥锁来解决安全隐患问题.png

  互斥锁技术可以保证Thread A对Integer独占,也就是在Thread A对Integer访问结束之前,其它子线程不可以访问Integer。当Thread A将新的数据18重新写入Integer以后,它会对Integer进行解锁,此时,其它子线程才可以访问Integer。同样,在Thread B对Integer进行访问期间,其它子线程也不可以访问Integer,除非Thread B对Integer的访问结束。下面,我们就通过代码来演示一下,如何在代码中使用互斥锁。

  在日常生活中,我们都有过买票的经历。我们都知道,同一趟列车,它的售票窗口可能有多个,但是每趟列车的总票数都是固定的。这个就好比多个子线程共享同一块资源一样。下面,我们就通过代码来模拟一下这种场景。首先,在类扩展中声明3个NSThread类型的属性,用于表示不同的售票员,声明一个NSInteger类型的属性,用于记录列车中的总票数:

@interface ViewController ()/** 售票员A */@property (strong, nonatomic) NSThread *threadA;/** 售票员B */@property (strong, nonatomic) NSThread *threadB;/** 售票员C */@property (strong, nonatomic) NSThread *threadC;/** 总票数 */@property (assign, nonatomic) NSInteger totalTickets;@end

  在- viewDidLoad方法中创建3个NSThread对象,用来表示3个不同的售票员。为了方便区别和描述,我们可以通过NSThread的name属性来给这3个售票员命名。另外,为了方便在票卖完以后顺利退出,我们在模拟售票的方法中使用了while循环:

- (void)viewDidLoad {    [super viewDidLoad];    // 创建子线程    self.threadA = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];    self.threadB = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];    self.threadC = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];    // 设置子线程名称    self.threadA.name = @"售票员A";    self.threadB.name = @"售票员B";    self.threadC.name = @"售票员C";    // 启动线程(此时,线程处于就绪状态或者运行状态)    [self.threadA start];    [self.threadB start];    [self.threadC start];    // 总票数    self.totalTickets = 100;}// MARK:- 模拟售票- (void)saleTicket {    while (1) {        NSInteger tickets = self.totalTickets;        if (tickets > 0) {            self.totalTickets = tickets - 1;            NSLog(@"%@卖出了一张票,还剩%zd张票。", [NSThread currentThread].name, self.totalTickets);        } else {            NSLog(@"卖完了,没有余票了。");            break;  // 退出循环        }    }}

  运行程序,看一下控制台打印出来的消息:


在没有加互斥锁的情况下,同一张票被卖了3次.png

  在上面的代码中,我们没有对子线程加互斥锁。也就是说,子线程threadA、threadB和threadC可以根据CPU的调度情况来自由访问总票数。从控制台的打印结果来看,我们可以很清楚的发现两个问题:1、余票信息比较混乱(第一次显示余票还有97张、第二次显示余票还有98张……这个显然不符合逻辑);2、这个问题也是最严重的,第85张车票分别被三个不同的售票员卖出了3次,在现实生活中,这种情况是会打架的!

  上面的结果很好的说明了,在多线程编程环境中,如果不对子线程加互斥锁,是很容易造成数据错乱,甚至是严重影响数据安全的。下面,我们就来给个子线程加一个互斥锁。加互斥锁的方式非常简单,只需要将子线程执行任务的代码写在关键字@synchronized(锁对象) { // 需要锁定的代码 }里面就可以了:

// MARK:- 模拟售票- (void)saleTicket {    while (1) {        // 加互斥锁(互斥锁必须是全局唯一)        @synchronized (self) {            NSInteger tickets = self.totalTickets;            if (tickets > 0) {                self.totalTickets = tickets - 1;                NSLog(@"%@卖出了一张票,还剩%zd张票。", [NSThread currentThread].name, self.totalTickets);            } else {                NSLog(@"卖完了,没有余票了。");                break;  // 退出循环            }        }    }}

  在家互斥锁的时候一定要注意:1、互斥锁必须加在合适的位置,要保证每个子线程都能顺利执行相应的打码;2、所对象必须具有全局唯一性,通常情况下,我们用self来做所对象。运行程序,看一下控制台打印的情况:


加了互斥锁以后的售票情况.png

  从运行的结果来看,在加了互斥锁以后,数据错乱的问题得到了很好的控制,并且一票多卖的情况也得到了有效控制。下面,我们来简单总结一下加互斥锁的注意点:

1、加锁的位置是有讲究的,必须保证每一个子线程都能有效执行相应的代码;
2、加锁的前提是,代码中存在多线程共享同一块资源;
3、加锁会耗费额外的CPU性能,不能随便乱加;
5、锁定一份代码只需一把锁就够了(多加是无用功);
4、没加锁时,线程默认是异步并发的执行任务。加锁以后,线程是同步执行,即按顺序执行任务。

  加锁的好处是,可以防止多个线程之间抢夺资源,从而导致数据错乱和不安全,缺点是需要消耗大量的CPU资源。所以,是否需要加锁要视具体情况而定。

五、atomic还是nonatomic

  
  在之前的开发过程中,我们在声明一个属性时,经常会碰到要求写nonatomic的情况。那么,为什么要这样写呢?nonatomic和atomic是相对的,其中,atomic表示原子属性,它会给setter方法加锁,如果不写的话,默认就是atomic;而nonatomic是非原子属性,即不会为setter方法加锁。

  那么,什么时候用atomic,什么时候用nonatomic呢?atomic可以保证线程安全,但是需要消耗大量的资源;nonatomic表示非线程安全,适合内存小的移动设备。在iOS开发过程中,为了避免多线程抢夺同一块资源,建议将所有的属性都声明为nonatomic,将加锁、资源抢夺的业务逻辑交给服务器端处理,以减小移动客户端的压力。

  以上就是NSThread的简单使用,后续会继续整理多线程的相关知识。详细代码参见NSThreadExercise。

原创粉丝点击