多线程编程NSThread的使用

Posted 泛彼无垠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程编程NSThread的使用相关的知识,希望对你有一定的参考价值。

ios 支持多个层次的多线程编程,层次越高的抽象程度越高,使用也越方便。实现多线程的方式有很多,我们主要学习其中的三种实现方式:NSThread,NSOpreationQueue,GCD,这三种编程方式从上到下,抽象度层次是从低到高的,抽象度越高的使用越简单,GCD是抽象程度最高的一种,也是 Apple 最推荐使用的方法。
   虽然实现的方式不同,但是操作的对象都是线程,都是对线程的管理。所以,从根本上讲,三者并没有什么不同,可以混合使用,但是并不推荐,因为可能会造成不必要的麻烦。三种多线程的实现方式各有利弊,我们在学习完三种实现方式之后会对它们做出比较。

在本文中,我们主要介绍NSThread的使用。需要掌握以下几个知识点

  • 在一个进程里面创建新线程
  • 对一个线程的属性进行修改
  • 在线程里面控制另一个同进程里面的线程
  • 线程之间在进程里面进行同步数据访问
  • 查看线程在运行过程中所处的状态

1. 线程开销

学习多线程的实现方式之前,我们先来了解一下一个非常重要的概念,线程开销。创建一个新线程的线程开销明显比创建一个新进程的要小得多(尽管linux在创建新进程方面特别高效),这是我们多线程开发的一个重要原因。

线程开销:线程是需要内存和性能开销的,内存开销包括系统内核内存和应用程序内存:用来管理和协调线程的内核结构存储在内核,线程的栈空间和每个线程的数据存储在程序的内存空间,占用内存的这些结构大部分是在线程创建的时候生成和初始化的。因为线程要和内核交互,这个过程是非常耗时的,这是线程的性能开销的主要原因。线程创建大概的开销如下(其中第二线程的栈空间是可以配置的):

  • 内核数据结构:大约1KB
  • 栈空间:主线程大约1MB,第二线程大约512KB
  • 线程创建时间:大约90毫秒
  • 另外一个开销就是程序内线程同步的开销。

2. NSThread的简介

NSThread是对pthread的上层封装,把线程处理为面向对象的逻辑。一个NSThread即代表一个线程。

NSThread的优点在于NSThread 是一种轻量级的多线程的实现方式。但是NSThread的缺点同样明显,需要自己管理线程的生命周期,线程同步。同时,使用NSThread实现线程同步对数据的加锁会有一定的系统开销。
NSThread实现的技术有下面三种:

TechnologyDescription
Cocoa threadsCocoa implements threads using the NSThread class. Cocoa also provides methods on NSObject for spawning new threads and executing code on already-running threads. For more information, see “Using NSThread” and “Using NSObject to Spawn a Thread.”Cocoa实现线程使用NSThread类。cocoa还提供了方法NSObject生成新线程和正在运行的线程上执行代码。有关更多信息,请参见“使用NSThread”和“使用NSObject生成一个线程”。
POSIX threadsPOSIX threads provide a C-based interface for creating threads. If you are not writing a Cocoa application, this is the best choicefor creating threads. The POSIX interface is relatively simple to use and offers ample flexibility for configuring your threads. For more information, see “Using POSIX Threads”POSIX线程提供一个基于c的接口来创建线程。如果你不写Cocoa应用程序中,这是最好的选择来创建线程。POSIX接口是相对简单的使用和配置线程提供了充足的灵活性。有关更多信息,请参见“使用POSIX线程”
Multiprocessing ServicesMultiprocessing Services is a legacy C-based interface used by applications transitioning from older versions of Mac OS.This technology is available in OS X only and should be avoided for any new development. Instead, you should use the NSThread class or POSIX threads. If you need more information on this technology, see Multiprocessing Services Programming Guide.多处理服务是一个遗留基于c的接口所使用的应用程序,从老版本的Mac OS过渡。这种技术可以在OS X只,应避免任何新的发展。相反,你应该使用NSThread类或POSIX线程。在这个技术如果你需要更多信息,请参见多处理服务编程指南。

我们一般推荐使用cocoa thread 技术。

  • Cocoa threads: 使用NSThread 或直接从 NSObject 的类方法 performSelectorInBackground:withObject: 来创建一个线程。如果你选择thread来实现多线程 ,那么 NSThread 就是官方推荐优先选用的方式。
  • POSIX threads: (可移植操作系统接口)基于 C 语言的一个多线程库,是一种跨平台的多线程实现方式。Pthread是一套通用的线程库,它广泛的被各种Unix所支持,是由POSIX提出的。因此, 它具有很好的可移植性。很多OS都兼容Posix thread,如Linux/Windows等,甚至嵌入式系统上(如rt-thread)都支持posix thread API。

3. NSThread的使用

3.1 NSThread的创建与运行

NSThread的初始化方式一共有4中方法:

  • 显示创建的方法
    这里写图片描述
    参数意义:
    selector :线程执行的方法,这个selector只能有一个参数,而且不能有返回值。我们知道每个线程都是有main方法的,selector就是该线程的入口方法,相当于该线程的main函数,这里相当于在main函数中self调用了selector的方法。
    target :selector消息发送的对象,
    argument:传输给target的唯一参数,也可以是nil
    代码示例:
    这里写图片描述

  • 隐式创建方式
    这里写图片描述
    参数意义:
    selector :线程执行的方法,这个selector只能有一个参数,而且不能有返回值。
    target :selector消息发送的对象
    argument:传输给target的唯一参数,也可以是nil
    代码示例
    这里写图片描述

    上面两种方法是NSTread类提供的直接创建线程的方式。第一种方式会先创建线程对象,然后再调用start方法运行线程操作;第二种方式是直接创建线程并且开始运行线程。同时第一种创建方式可以得到一个线程对象,在运行线程操作前可以设置线程的优先级等线程信息,这点我们会在下面做具体的讲解。
    
  • 使用NSObject的线程扩展方法:
    在NSThread中创建了一个类目 (NSThreadPerformAdditions),为NSObject拓展了多线程的实现方法,使用和上一个方法类似。
    这里写图片描述
    代码示例:
    这里写图片描述

  • 创建一个NSThread子类

    基于NSThread类创建一个子类,然后实例化并调用start方法运行线程操作,注意在初始化子类是,需要复写main函数,作为线程的入口。
    这里写图片描述
    这里写图片描述
    需要强调的是, 复写main函数后,线程的入口已经有了,不需要再指定线程的入口函数。所以下面的使用方式是没有意义的,因为当我们复写main函数后,线程的入口就改变了,制定selecter 参数没有意义。

3.2 配置线程参数

  • stackSize:配置线程栈空间

    栈空间是用来存储为线程创建的本地变量的,栈空间的大小必须在线程的创建之前设定,即在调用NSThread的start方法之前通过setStackSize: 设定新的栈空间大小。

  • threadDictionary:配置线程的本地存储

    每个线程都维护一个在线程任何地方都能获取的字典。 我们可以使用NSThread的 threadDictionary方法获取一个NSMutableDictionary对象,然后添加我们需要的字段和数据。

  • threadPriority:设置线程的优先级

    可以通过NSThread的setThreadPriority:方法设置线程优先级,优先级为0.0到1.0的double类型,1.0为最高优先级。iOS 8更新中被qualityOfService替代。每一个新的线程都有一个默认的优先级。系统的内核调度算法根据线程的优先级来决定线程的执行顺序。通常情况下我们不要改变线程的优先级,提高一些线程的优先级可能会导致低优先级的线程一直得不到执行,如果在我们的应用内存在高优先级线程和低优先级线程的交互的话,因为低优先级的线程得不到执行可能阻塞其他线程的执行。这样会对应用造成性能瓶颈。

  • 设置线程的Detached、Joinable状态

  • 脱离线程(Detach Thread)—线程完成后,系统自动释放它所占用的内存空间

  • 可连接线程(Joinable Thread)—线程完成后,不回收可连接线程的资源

    在应用程序退出时,脱离线程可以立即被中断,而可连接线程则不可以。每个可连接线程必须在进程被允许可以退出的时候被连接。所以当线程处于周期性工作而不允许被中断的时候,比如保存数据到硬盘,可连接线程是最佳选择。
    当然,在iOS开发过程中,很少需要我们创建可连接的线程。通过NSThread创建的线程都是脱离线程的。如果你想要创建可连接线程,唯一的办法是使用 POSIX 线程。POSIX 默认创建的线程是可连接的。通过 pthread_attr_setdetachstate函数设置是否脱离属性。

3.3 NSThread 的常用方法

  NSThread类给我们提供了一下几个常用方法

这里写图片描述

3.4 线程间的通信

    在一个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信。例如,执行下载网络图片的任务时,为了避免由于网络延迟造成主线程的卡死,我们开辟一条线程执行下载任务,但是由于操作UI的任务必须在主线程中进行,所以图片下载完成后需要回到主线程完成加载图片的任务。
   线程间通信的主要表现在:一个线程传递数据给另一个线程,或者是在一个线程中执行完特定任务后,转到另一个线程继续执行任务。

线程间通信常用方法:

这里写图片描述

参数解释:

  • @selector 定义我们要执行的方法。
  • withObject:arg 定义了我们执行方法时,传入的参数对象,类型是id。
  • waitUntilDone:YES 指定当前线程是否要被阻塞,直到主线程将我们制定的代码块执行完。
  • modes:array 指定时间运行的模式。

    (1)前两个方法的作用是在主线程中,执行指定的方法。该方法主要用来回调到主线程来修改页面UI的状态。当前线程为主线程的时候,waitUntilDone: 设置参数为YES无效。
    (2)后两个方法的作用是在指定线程中,执行指定的方法。注意,指定执行任务的线程必须有runloop。

3.5 示例练习

我们通过一个示例来联系一下NSThead的使用,当我们在加载网络图片时,由于网络延迟,图片可能很长时间才能完成加载,这时候,为了避免造成主线程的卡死,我们可以把下载网络图片的任务交给一个新的线程,当图片完成下载之后再回到主线程完成图像的加载显示。

   代码如下:
  • 给UIImageView类创建一个类目,增加一个方法:setimageWithURL:(NSString*)urlString;开辟子线程,完成下载图片的任务;
    这里写图片描述
  • 在子线程中下载网络图片,下载成功后回到主线程,完成图片的加载任务,因为系统要求所有与UI想过的任务都需要在主线程中完成。
    这里写图片描述
  • 图片下载完成后,完成图片的加载,该方法需要在主线程中完成。
    这里写图片描述

4. 完善线程的入口

4.1 Autorelease Pool

   如果新开辟的线程没有Autorelease Pool的话,那么在新线程中生成的Autorelease对象会存放到主线程的Autorelease Pool中,当新开辟的线程被终止时,线程中的Autorelease对象不会被最终释放掉,占用主线程资源。所以我们需要在线程的入口处我们需要创建一个Autorelease Pool,当线程退出的时候释放这个Autorelease Pool。这样在线程中创建的autorelease对象就可以在线程结束的时候释放,避免过多的延迟释放造成程序占用过多的内存。如果是一个长寿命的线程的话,应该创建更多的Autorelease Pool来达到这个目的。例如线程中用到了Runloop的时候,每一次的迭代都需要创建 Autorelease Pool。

4.2 设置 Run Loop

   当创建线程的时候我们有两种选择,一种是线程执行一个很长的任务然后再任务结束的时候退出。另外一种是线程可以进入一个循环,然后处理动态到达的任务,这时候就需要我们开启线程的RunLoop了。每一个线程默认都有一个 NSRunloop,主线程是默认开启的,其他线程要手动开启。

4.3 终止线程

   终止线程最好不要用POSIX接口直接杀死线程,这种粗暴的方法会导致系统无法回收线程使用的资源,造成内存泄露,还有可能对程序的运行造成影响。终止线程最好的方式是能够让线程接收取消和退出消息,这样线程在受到消息的时候就有机会清理已持有的资源,避免内存泄露。如果需要在子线程运行的时候让子线程结束操作,子线程每次Run Loop迭代中检查相应的标志位来判断是否还需要继续执行,可以使用threadDictionary以及设置Input Source的方式来通知这个子线程。这种方案的一种实现方式是使用NSRunloop的input source来接收消息,每一次的 NSRunloop循环都检查退出条件是否为YES,如果为YES退出循环回收资源,如果为NO,则进入下一次NSRunloop循环。

5. 线程同步

有时候需要我们设置线程同步,但是线程同步往往会产生很多问题:
1. 线程的死锁。即较长时间的等待或资源竞争以及死锁等多线程症状。
2. 对公有变量的同时读或写。当多个线程需要对公有变量进行写操作时,后一个线程往往会修改掉前一个线程存放的数据,从而使前一个线程的参数被修改;另外 ,当公用变量的读写操作是非原子性时,在不同的机器上,中断时间的不确定性,会导致数据在一个线程内的操作产生错误,从而产生莫名其妙的错误,而这种错误是程序员无法预知的。

5.1 数据同步锁

   在多个线程访问相同的数据时,有可能会造成数据的冲突。比如常见的售票问题。
  • 通过使用加锁的方式解决该问题是最常见的方式。Foundation框架中提供了NSLock对象来实现锁。

    [lock lock];//加锁
    [lock unlock];// 解锁

  • 通过设置属性的原子性同样可以解决的该问题

    atomic:默认是有该属性的,这个属性是为了保证程序在多线程情况下,编译器会自动生成一些互斥加锁代码,避免该变量的读写不同步问题。

    nonatomic:如果该对象无需考虑多线程的情况,请加入这个属性,这样会让编译器少生成一些互斥加锁代码,可以提高效率。

    atomic的意思就是setter/getter这个函数,是一个原子操作。如果有多个线程同时调用setter的话,不会出现某一个线程执行完setter全部语句之前,另一个线程开始执行setter情况。相当于函数头尾加了锁一样,可以保证数据的完整性。nonatomic不保证setter/getter的原语行,所以 你可能会取到不完整的东西。因此,在多线程的环境下原子操作是非常必要的,否则有可能会引起错误的结果。比如setter函数里面改变两个成员变量,如果你用 nonatomic的话,getter可能会取到只更改了其中一个变量时候的状态,这样取到的东西会有问题,就是不完整的。当然如果不需要多线程支持的话,用nonatomic就够了,因为不涉及到线程锁的操作,所以它执行率相对快些。

    一般iOS程序中,所有属性都声明为nonatomic。这样做的原因是:在iOS中使用同步锁的开销比较大, 这会带来性能问题。一般情况下并不要求属性必须是“原子的”,因为这并不能保证“线程安全”(thread safety),若要实现“线程安全”的操作,还需采用更为深层的锁定机制才醒。例如:一个线程在连续多次读取某个属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为atomic,也还是会读取到不同的属性值。因此,iOS程序一般都会使用nonatomic属性。但是在Mac OS X程序时, 使用atomic属性通常都不会有性能瓶颈

5.2 同步等待

多线程中经常遇到一种问题,A线程需要等待B线程执行后的某个结果继续执行,也就是同步问题,这时就会需要A等待B,这里说说使用NSCondition实现多线程同步的问题,也就是解决生产者消费者问题(如收发同步等等)。

问题流程如下:

  • 消费者取得锁,取产品,如果没有,则wait,这时会释放锁,直到有线程唤醒它去消费产品;
  • 生产者制造产品,首先也要取得锁,然后生产,再发signal,这样可唤醒wait的消费者。

    这里需要注意wait和signal的问题:

    1. 其实,wait函数内部悄悄的调用了unlock函数(猜测,有兴趣可自行分析),也就是说在调用wati函数后,这个NSCondition对象就处于了无锁的状态,这样其他线程就可以对此对象加锁并触发该NSCondition对象。当NSCondition被其他线程触发时,在wait函数内部得到此事件被触发的通知,然后对此事件重新调用lock函数(猜测),而在外部看起来好像接收事件的线程(调用wait的线程)从来没有放开NSCondition对象的所有权,wati线程直接由阻塞状态进入了触发状态一样。这里容易造成误解。
    2. wait函数并不是完全可信的。也就是说wait返回后,并不代表对应的事件一定被触发了,因此,为了保证线程之间的同步关系,使用NSCondtion时往往需要加入一个额外的变量来对非正常的wait返回进行规避。
    3. 关于多个wait时的调用顺序,测试发现与wait执行顺序有关。

6. 线程的状态

线程的创建和开启:

 self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(test) object:nil];
 [self.thread start];

线程的运行和阻塞:

   // 设置线程阻塞1,阻塞2秒
   [NSThread sleepForTimeInterval:2.0];
   // 第二种设置线程阻塞2,以当前时间为基准阻塞4秒
   NSDate *date=[NSDate dateWithTimeIntervalSinceNow:4.0];
   [NSThread sleepUntilDate:date];

线程处理阻塞状态时在内存中的表现情况:(线程被移出可调度线程池,此时不可调度)

线程的死亡:
当线程的任务结束,发生异常,或者是强制退出这三种情况会导致线程的死亡。
这里写图片描述
线程死亡后,线程对象从内存中移除。

代码示例1:

- (void)viewDidLoad {
     [super viewDidLoad];

     //创建线程
     self.thread=[[NSThread alloc]initWithTarget:self selector:@selector(test) object:nil];
    //设置线程的名称
     [self.thread setName:@"线程A"];
 }
 //当手指按下的时候,开启线程
 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
     //开启线程
     [self.thread start];
 }

 -(void)test {
     //获取线程
     NSThread *current=[NSThread currentThread];
     NSLog(@"test---打印线程---%@",self.thread.name);
     NSLog(@"test---线程开始---%@",current.name);
     //设置线程阻塞1,阻塞2秒
     NSLog(@"接下来,线程阻塞2秒");
     [NSThread sleepForTimeInterval:2.0];
     //第二种设置线程阻塞2,以当前时间为基准阻塞4秒
      NSLog(@"接下来,线程阻塞4秒");
     NSDate *date=[NSDate dateWithTimeIntervalSinceNow:4.0];
     [NSThread sleepUntilDate:date];
     for (int i=0; i<20; i++) {
         NSLog(@"线程--%d--%@",i,current.name);

     }
         NSLog(@"test---线程结束---%@",current.name);
 }

打印查看:
这里写图片描述
代码示例2(退出线程):

- (void)viewDidLoad {
   [super viewDidLoad];
     //创建线程
     self.thread=[[NSThread alloc]initWithTarget:self selector:@selector(test) object:nil];
     //设置线程的名称
     [self.thread setName:@"线程A"];
}
 //当手指按下的时候,开启线程
 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
     //开启线程
     [self.thread start];
 }
 -(void)test {
     //获取线程
     NSThread *current=[NSThread currentThread];
     NSLog(@"test---打印线程---%@",self.thread.name);
     NSLog(@"test---线程开始---%@",current.name);
     //设置线程阻塞1,阻塞2秒
     NSLog(@"接下来,线程阻塞2秒");
     [NSThread sleepForTimeInterval:2.0];

     //第二种设置线程阻塞2,以当前时间为基准阻塞4秒
      NSLog(@"接下来,线程阻塞4秒");
     NSDate *date=[NSDate dateWithTimeIntervalSinceNow:4.0];
     [NSThread sleepUntilDate:date];
     for (int i=0; i<20; i++) {
         NSLog(@"线程--%d--%@",i,current.name);
         if (5==i) {
            //结束线程
             [NSThread exit];
         }
     }
         NSLog(@"test---线程结束---%@",current.name);
 }

打印示例:
这里写图片描述
注意:如果在线程死亡之后,再次点击屏幕尝试重新开启线程,则程序会挂。
这里写图片描述

小结:

NSThread 作为多线程编程的重要的工具类,我们应该尝试使用并理解其中的方式。但是NSThread的缺点很明显,我们需要手动实现非常复杂的管理线程逻辑,自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销。当然他的优点同样明显比其他两种多线程方案较轻量级,更直观地控制线程对象。

以上是关于多线程编程NSThread的使用的主要内容,如果未能解决你的问题,请参考以下文章

多线程编程NSThread的使用

iOS多线程编程--NSThread

IOS开发 多线程编程 - NSThread

多线程编程1-NSThread

iOS开发开辟线程总结--NSThread

多线程NSThread