iOS多线程总结——多线程相关概念及NSObject/NSThread的使用

Posted SSIrreplaceable

tags:

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

一. 多线程的相关概念

1. 什么是进程?

在操作系统发展的早期,为了提高资源利用率,使程序在多道程序下能并发执行,并对并发执行的程序加以控制和描述,在操作系统中引入了进程的概念。多道程序技术最早用于多道批处理系统,系统内可以同时存在多道作业,但同一时刻,系统只处理一道作业,作业根据系统的调度算法执行,每一个作业又由若干个程序组成,每个程序都可以完成独立的任务,且一个作业里面的程序是按顺序先后执行的,一个正在运行的程序称作一个进程,一个作业相当于系统中的一条执行任务的路径。在现代操作系统中,一个进程是指操作系统中正在运行的一个应用程序,是系统进行资源分配和调度的基本单位,每一个进程之间都是独立的,每一个进程均运行在其专用且受保护的内存空间内。

2. 什么是线程?

在60年代操作系统中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。因此在80年代,即通用计算机系统发展的年代,出现了能独立运行的基本单位——线程(Threads)。在现代操作系统中,一个进程想要执行任务,必须得有线程,进程的所有任务都是在线程中执行,一个线程中任务的执行是串行的(即是按顺序的),一个线程要执行多个任务,只能一个一个地按顺序执行这些任务(指令代码),一个线程相当于进程中的一条执行任务的路径;一个进程可以有多条线程,但不能没有线程,至少要有一条线程(主线程)。线程是进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。

3. 什么是多线程?

在60年代操作系统,进程是系统拥有资源和独立运行的基本单位,它的创建、撤消与切换存在较大的时空开销,在80年代后的操作系统,为提高提高程序的执行效率,降低时空开销,引进线程的概念。多进程技术也是在这个时候出现的,具有多线程能力的计算机系统能够在同一时间执行多条线程。线程是系统独立调度和分派CPU的基本单位,在单处理机计算机系统中,多个线程是并发执行的,即系统中的多个线程根据线程调度算法快速地调度执行,如果调度的时间足够快,就造成多个线程并发执行的假象,但在同一时间,只有一条线程在执行;在有多个处理机或者有多核心处理器的计算机系统中,如果线程的数目小于处理机的数目(或小于处理器的核心数),多个线程一般是并行的(以线程调度算法而定),反之,线程一部分是并行一部分并发执行。

4. 多线程的优缺点

(1). 优点
能适当提高程序的执行效率
能适当提高资源利用率(CPU、内存利用率)

(2). 缺点
创建线程是有开销的,ios下主要成本包括:内核数据结构(大约1KB)、栈空间(子线程512KB、主线程1MB,也可以使用 -setStackSize: 方法设置,但必须是4K的倍数,而且最小是16K),创建线程大约需要90毫秒的创建时间,如果开启大量的线程,每条线程被调度执行的频次会降低(线程的执行效率降低),会降低程序的性能,且线程越多CPU在调度线程上的开销就越大,程序设计也更加复杂(比如线程之间的通信、多线程的数据共享更复杂)。

结论:线程不是开启越多越好,而是合适就好,一般一个程序开3条左右比较合理。

5. 什么是主线程?

在有多线程技术的操作系统中,当一个程序启动时,就有一个进程被操作系统创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(Main Thread),在iOS中称为“UI线程”。在iOS中主线程的主要作用是显示\\刷新UI界面,处理UI事件(比如点击事件、滚动事件、拖拽事件等)。一般开发中不把耗时的操作放到主线程,因为耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验。主线程关系着整个应用程序的开始和结束,一般主线程终止了,进程也就随之终止。

6. 什么是子线程?

一个进程中,如果有多条线程,处了主线程外,都称为子线程(也叫后台线程或非主线程)。在单核CPU下,子线程和主线程会并发执行,在多核CPU下,可能会并行执行。子线程和主线程的执行没有先后,会根据系统的算法调度执行。iOS中子线程的作用一般用来处理一些耗时操作,或者可以通过多条子线程进行快速遍历。

注意:子线程在创建时只是申请了内存程序空间,但还并没有真正分配给二级线程,只有当子线程执行代码需要空间时才会真正分配。

7. iOS中多线程的实现方案

二. 多线程的实现

这里主要介绍:NSObject\\NSThread\\GCD\\NSOperation,因为在iOS中pthread几乎不用,所以这里不进行介绍。

(一). NSObject

NSObject也可以创建多线程,因为Apple给NSObject实现了一个关于线程的分类 NSObject (NSThreadPerformAdditions)

@interface NSObject (NSThreadPerformAdditions)

// 用于线程通信,子线程传到主线程
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;

// 用于线程通信,子线程传到主线程,默认的运行时模式:kCFRunLoopCommonModes
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

// 用于线程之间的通信
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);

// 用于线程之间的通信
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);

// 隐式创建线程,默认的运行时模式:kCFRunLoopCommonModes
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);

@end

(二). NSThread

1. 线程的创建与启动

// 创建线程
- (void)createThread 

    // 创建线程,NSThread的创建方法
    // 一个NSThread对象就是一个线程
    SJMNSThread *thread = [[SJMNSThread alloc] initWithTarget:self selector:@selector(run:) object:@"sjm"];

    // 设置线程的名字
    thread.name = @"线程";
    NSLog(@"%@",thread);

    // 启动线程
    // 线程一启动,就会在线程thread中执行self的run方法
    [thread start];



// 线程任务
- (void)run:(NSString *)string 

    // 获取当前线程
    NSThread *thread = [NSThread currentThread];
    thread.name = @"子线程";

    NSLog(@"%@-----%@",string,[NSThread currentThread]);

2. 其他创建线程的方法

// 创建线程后自动启动线程
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];

// 隐式创建并启动线程,此方法来自NSObject的分类 NSObject (NSThreadPerformAdditions)
[self performSelectorInBackground:@selector(run) withObject:nil];

上述2种创建线程方式的优缺点

  • 优点:简单快捷
  • 缺点:无法在线程创建时对线程进行更详细的设置,只能在run方法中进行设置,即获取当前线程对象进行设置。

3. 退出线程

退出线程的方式有cancle和exit方法

(1). 两者的区别:

  • 使用cancle方法退出线程,线程不会马上退出,会在某些线程堵塞的情况下退出,但也不一定,它的退出是未知的。
  • 使用exit方法退出线程,线程会强制马上退出,exit方法没有给线程清理自己并释放资源的时间,可能会造成资源泄露。

(2). 退出线程的做法:

  • 调用cancel方法,并把线程变量赋值为nil。
  • 也可以cancle和exit结合使用,使用cancle进行标记,使用exit退出。

4. NSThread的接口

@interface NSThread : NSObject  
@private
    id _private;
    uint8_t _bytes[44];


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

// 创建新线程
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

// 是否是多线程
+ (BOOL)isMultiThreaded;

/** 
*   每个线程都维护了一个“键-值”的字典,它可以在线程里面的任何地方被访问,
*   可以使用该字典来保存一些信息,这些信息在整个线程的执行过程中都保持不变。
*   比如,可以使用它来存储在整个线程过程中RunLoop里面多次迭代的状态信息。
*   使用:通过threadDictionary方法获取一个NSMutableDictionary对象,然后添加需要的字段和数据
*/
@property (readonly, retain) NSMutableDictionary *threadDictionary;

// 设置线程睡眠/堵塞
+ (void)sleepUntilDate:(NSDate *)date;

// 设置线程睡眠/堵塞
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

// 结束/退出进程
+ (void)exit;

// 获取线程的优先级
+ (double)threadPriority;

// 设置线程优先级,取值范围0.0~1.0
+ (BOOL)setThreadPriority:(double)p;

// 线程优先级,iOS8以后推荐使用qualityOfService属性,通过量化的优先级枚举值来设置
@property double threadPriority; 

/** 线程优先级
    qualityOfService的枚举值如下:
    NSQualityOfServiceUserInteractive:最高优先级,用于用户交互事件
    NSQualityOfServiceUserInitiated:次高优先级,用于用户需要马上执行的事件
    NSQualityOfServiceDefault:默认优先级,主线程和没有设置优先级的线程都默认为这个优先级
    NSQualityOfServiceUtility:普通优先级,用于普通任务
    NSQualityOfServiceBackground:最低优先级,用于不重要的任务
*/
@property NSQualityOfService qualityOfService;

// 返回当前线程在栈中所占的地址所组成的数组
+ (NSArray<NSNumber *> *)callStackReturnAddresses NS_AVAILABLE(10_5, 2_0);

// 返回栈空间的符号表
+ (NSArray<NSString *> *)callStackSymbols NS_AVAILABLE(10_6, 4_0);

// 线程名称
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

// 栈的所占空间大小
@property NSUInteger stackSize NS_AVAILABLE(10_5, 2_0);

// 是否是主线程
@property (readonly) BOOL isMainThread NS_AVAILABLE(10_5, 2_0);

// 判断当前线程是否是主线程
+ (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0); // reports whether current thread is main

// 获取主线程
+ (NSThread *)mainThread NS_AVAILABLE(10_5, 2_0);

// 初始化线程
- (instancetype)init NS_AVAILABLE(10_5, 2_0) NS_DESIGNATED_INITIALIZER;

// 初始化线程
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);

// 是否正在执行
@property (readonly, getter=isExecuting) BOOL executing NS_AVAILABLE(10_5, 2_0);

// 是否执行完毕
@property (readonly, getter=isFinished) BOOL finished NS_AVAILABLE(10_5, 2_0);

// 是否已经取消/中止
@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);

// 取消线程,不能再开始
- (void)cancel NS_AVAILABLE(10_5, 2_0);

// 开始线程
- (void)start NS_AVAILABLE(10_5, 2_0);

/** main是线程入口
*  - (void)main的使用:
*  1. 一般创建线程会子类化NSThread,重写main方法,把关于线程执行的方法都写在里面,这样可以在任何需要这个线程方法的地方直接使用。
*  2. 把线程执行的方法写在main里,是因为线程的操作应该属于线程的本身,而不是每次使用都通过initWithTarget:selector:object:方法,且再一次实现某个方法。
*  3. 当重写了main方法后,同时使用initWithTarget:selector:object:方法初始化,调用某个方法执行任务,系统默认只执行main方法里面的任务。
*  4. 如果直接使用NSThread创建线程,线程内执行的方法都是在当前的类文件里面的。
*/
- (void)main NS_AVAILABLE(10_5, 2_0);

@end

5. 多线程的状态

计算机中的线程在内存中得可调度线程池中,每一条线程都有5个状态。

注意:一旦线程停止(死亡)了,就不能再次开启任务。


(1). 启动线程

// 进入就绪状态或直接进入运行状态。当线程任务执行完毕,自动进入死亡状态
- (void)start;

(2). 阻塞(暂停)线程

// 进入阻塞状态
// 堵塞到遥远未来,系统会自动杀死线程,[NSThread sleepUntilDate:[NSDate distantFuture]];
// 堵塞到遥远的过去,线程不会堵塞,[NSThread sleepUntilDate:[NSDate distantPast]];
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

(3). 强制停止线程

// 进入死亡状态
+ (void)exit;

6. 多线程的安全隐患

  • 线程同步:多条线程在同一条线上执行(按顺序地执行任务)
  • 互斥锁:就是使用了线程同步技术

(1). 资源共享 - 数据错乱

1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,比如多个线程访问同一个对象、同一个变量、同一个文件,当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。

(2). 安全隐患解决 – 互斥锁

  • 互斥锁使用格式

    // 注意:锁定1份代码只用1把锁,用多把锁是无效的。
    @synchronized(锁对象) // 需要锁定的代码

    提示: “锁对象”也被称为一个“信号量”。当一个线程要执行这段代码时,它会检查其他的线程是否也在访问“锁对象”,如果没有线程在访问“锁对象”,这块代码会被执行;否则这段线程会被限制访问直到这个互斥锁解除为止。

  • 互斥锁的优缺点:

    优点:能有效防止因多线程抢夺资源造成的数据安全问题
    缺点:需要消耗大量的CPU资源

  • 互斥锁的使用前提:多条线程抢夺同一块资源

补充: iOS中有两种加锁方式,即NSLock与NSCondition,之后又通过@synchronized替代了NSLock复杂的书写方式,确保线程同步。

(3). 原子和非原子属性

  • OC在定义属性时有nonatomicatomic两种选择

    atomic:原子属性,为setter方法加锁(默认就是atomic)
    nonatomic:非原子属性,不会为setter方法加锁

  • nonatomic和atomic对比

    atomic:线程安全,需要消耗大量的资源
    nonatomic:非线程安全,适合内存小的移动设备

  • iOS开发的建议

    所有属性都声明为nonatomic,尽量避免多线程抢夺同一块资源,尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力。

7. 多线程的通信

(1). 什么叫做线程间通信

在1个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信

(2). 线程间通信的体现

  • 1个线程传递数据给另1个线程
  • 在1个线程中执行完特定任务后,转到另1个线程继续执行任务

(3). 线程间通信常用方法

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;

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

(4). 以前常用线程通信

  • NSPort
  • NSMessagePort
  • NSMachPort

8. 线程的通信实例

实例:点击按钮,添加网络图片

注意:在Xcoed7后,使用dataWithContentsOfURL加载网络数据为出错

App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.

解决方法:是在Info.plist中添加参数

  • 在 Info.plist 中添加 NSAppTransportSecurity,类型为 Dictionary 。
  • 在 NSAppTransportSecurity 下添加 NSAllowsArbitraryLoads, 类型为 Boolean,值设为 YES。

实例界面(加载图片后):

具体代码:

#import "ViewController.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@end

@implementation ViewController

- (void)viewDidLoad 
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.


- (IBAction)btnClick:(id)sender 

    // 创建线程
    [NSThread detachNewThreadSelector:@selector(imageLoad) toTarget:self withObject:nil];



// 线程执行任务
- (void)imageLoad 

    // 从网络下载图片
    NSURL *url = [NSURL URLWithString:@"http://img.51ztzj.com/upload/image/20140618/sj201406181009_279x419.jpg"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];


    // 线程通信,子线程把image传给主线程
    // waitUntilDone:是否等主线程执行完setImage:后子线程才往下执行
//    [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
//    [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];

    // setImage:是self.imageView属性image的setter方法
//    [self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO];
    [self.imageView performSelector:@selector(setImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:NO];



    NSLog(@"---%s---",__func__);


- (void)showImage:(UIImage *)image 

    _imageView.image = image;

    NSLog(@"%s---%@",__func__,[NSThread currentThread]);


@end

以上是关于iOS多线程总结——多线程相关概念及NSObject/NSThread的使用的主要内容,如果未能解决你的问题,请参考以下文章

iOS多线程总结——NSOperation与NSOperationQueue的使用

C++11多线程第一篇:并发基本概念及实现,进程线程基本概念

C++11多线程第一篇:并发基本概念及实现,进程线程基本概念

多线程的概念及实现

线程概念及多线程控制

C++11多线程 原子操作概念及范例