多线程开发

来源:互联网 发布:电脑能写小说的软件 编辑:程序博客网 时间:2024/05/19 14:53

多线程开发

1. 概览

开发过程中应该尽可能减少用户等待时间,将耗时操作放到后台去执行,给用户更好的体验;但是无论何种语言开发的程序最终都是转换成汇编语言进而解释成机器码执行;而机器码是按顺序执行的,一个复杂的多步操作只能一步步按顺序逐个执行;

要改变这种状况可以从两个角度出发:

  1. 对于单核处理器:可以将多个步骤放到不同的线程,这从而可以在同一时间,执行多个任务;**原理是CPU在多个线程之间快速切换,(一个单核CPU,同一时间,只能处理一条线程;)**_(如果线程数非常多,cpu在线程之间切换操作更多,会消耗大量的CPU资源,每个线程被调度的次数会降低,效率更低;所以对于移动端一般使用3-5个线程.)_
  2. 对于多核处理器,如果用户在UI线程中完成操作后,此操作后续任务在别的线程中继续执行,用户同样可以继续进行其他UI操作,于此同时前一个操作的后续任务可以分散到多个空闲CPU中继续执行

名词解释:

  • 进程:系统中正在运行的应用程序.每个进程都运行在其专用且受保护的内存空间,不同的进程之间是相互独立,互不干扰的
  • 线程:进程的基本执行单元,一个进程的所有任务都是在线程中执行的.一个线程,同一时间,只能执行一个任务,即线程的任务执行是串行
  • ——
  • 执行任务的方式:
  • 同步:按顺序在当前线程执行任务. – dispatch_sync
  • 异步:同时在多个线程执行任务. —dispatch_async
  • ——
  • 队列类型:
  • 串行队列:一个任务执行完毕,再执行下一个
  • 并行队列:多任务并发执行

2. 多线程

2.1 简介

当用户播放音频,下载视频,进行图像处理时往往希望做这些事的时候其他的操作不会被中断或是不流畅.而在单线程中一个线程只能做一件事,没做完之前另一件事就不能开始,很是影响用户体验.早在单核处理器事情就有了多线程,用于解决线程阻塞,提高程序的执行效率.不过运算处理能力并没有明显变化.

如今包括移动操作系统大都使用多核处理器,可以进行多CPU的并行运算,因此一件事情我们可以分成多个步骤,在没有顺序要求的情况下使用多线程既能解决线程阻塞又能充分利用多核处理器运行能力。


多线程具体要解决的问题:

  1. 线程阻塞,一般在此线程内部还要返回主线程进行UI操作;
  2. 线程同步:不论单核多核,任务的具体分配都是由系统调度,我们需要关心的是线程之间的依赖关系,因为有些操作必须在某个操作完成之后才能执行;
  3. 资源抢夺–线程不安全的;

下面会分别从iOS三种线程的主要使用方法一一举例说明;


多线程优点:
1. 适当提高程序执行效率
2.适当提高资源(CPU,内存)的利用率.
3.线程上的任务执行完成后,线程会自动销毁
缺点:
1. 每开一个子线程消耗512kb , 消耗90ms.
2.大量线程耗性能
3.程序设计更加复杂.
代码运行速度:
1. 循环的速度非常快的,也非常消耗CPU.
2. 操作栈空间的速度非常快,
3. 操作堆区速度有点慢;
4. I/O操作(设备) 非常慢.
5. 网络比上面都慢,

2.2 iOS多线程

一个iOS程序运行后,默认会开启一条线程,这条线程就称为“主线程”或“UI线程”,主要用于显示/刷新UI界面和处理UI事件;

iOS中多线程使用并不复杂,关键是如何控制好各个线程的执行顺序、资源抢夺问题。常用的多线程开发有三种方式

  1. NSThread :
    • OC的
    • 更加面向对象,可以直接操作线程对象
    • 线程生命周期:由程序员管理
    • 使用频率:偶尔使用
  2. GCD :
    • C语言的
    • 可以充分利用设备的多核,目的是替代NSThread
    • 线程生命周期:系统自动管理
    • 使用频率:经常使用
  3. NSOperation
    • OC的
    • 基于GCD,更加面对对象
    • 线程生命周期:系统自动管理
    • 使用频率:经常使用,苹果推荐使用

还有一种pthread方式,已经很少使用了;虽然可以跨平台,但是用纯C语言,涉及到一些操作系统相关的东西,需要手动管理内存,几乎不用;使用pthread_create 创建子线程;C语言中的 void * 等同于 OC 中的 id;在 ARC 开发中,如果在对 C 语言的数据类型和 OC 的数据类型进行转换时,需要使用 __bridge 进行桥接


3. NSThread

轻量级的多线程开发,使用起来也并不复杂.但是需要自己管理线程生命周期. 是OC的一个类,一个对象就表示一个线程;
创建方法常规两种:

  1. +(void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument直接将操作添加到线程中并启动
  2. -(instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 创建一个线程对象,注意:此方法创建并返回了线程对象,因此我们可以在外部获得线程对象,从而对线程进行更详细的设置,所以系统把此线程状态交还我们操作:
    • 需调用start方法来将线程对象加入可调度线程池,等待CPU调度(CPU只会调度可调度线程池里面的线程对象)
    • 其他属性:threadPriority优先级:默认0.5,取值0-1; (优先级高,只是表示CPU调度的频率相对较高,并不表示先调度完优先级高的,才会调度优先级低的)

线程扩展–NSObject分类扩展方法

另外,为了简化多线程开发过程,苹果官方对NSObject进行分类扩展(本质还是创建NSThread),对于简单的多线程操作可以 直接使用这些扩展方法 。

  • -(void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg://常用在后台执行一个操作,本质就是重新创建一个线程执行当前方法。

  • (void)performSelector:(SEL)aSelector onThread:(NSThread * ) thread withObject:(id)arg waitUntilDone:(BOOL)wait 在指定的线程上执行一个方法,需要用户创建一个线程对象

  • -(void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait:在主线程上执行一个方法;

  • 以上两个可以作为线程通信;

3.1 解决线程阻塞问题

在资源下载过程中,由于网络原因有时候很难保证下载时间,如果不使用多线程,用户需要等长时间才能进行其他操作.例:

//用scrollView替换控制器view.- (void)loadView {//加载视图,作用和 storyborad/xib 等价,只要写了这个方法,sotryboard就不会工作----负责创建界面上所有的视图层次结构    //初始化scrollviewself.scrollView = [[UIScrollView alloc] initWithFrame:[UIScreen mainScreen].bounds];self.scrollView.backgroundColor = [UIColor whiteColor];self.view = self.scrollView;//初始化imageViewself.imageView = [[UIImageView alloc] init];[self.scrollView addSubview:self.imageView];}- (void)viewDidLoad {[super viewDidLoad];// Do any additional setup after loading the view, typically from a nib.//创建子线程NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(downLoadImage) object:nil];//注意要开始线程[thread start];}//下载网络图片- (void)downLoadImage {NSURL *url = [NSURL URLWithString:@"http://img04.tooopen.com/images/20130701/tooopen_20083555.jpg"];NSData *data = [NSData dataWithContentsOfURL:url];UIImage *image = [UIImage imageWithData:data];//注意: 需要在主线程上更新UI控件,因为线程不安全的.[self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES]; //是否线程任务完成再执行后面代码}-(void)updateUI:(UIImage *)image {self.imageView.image = image;//让imageview的大小和图片的大小一致[self.imageView sizeToFit];//设置scrollView滚动范围self.scrollView.contentSize = image.size;}

注意:

  1. scrollView和imageView这样创建需要使用强指针;
  2. UI控件只能由主线程操作,从而避免子线程资源抢夺,保证线程安全;
  3. 使用子线程加载图片,解决的线程阻塞问题,用户在加载图片时可以进行其他操作;

3.2 线程同步-解决线程依赖

多个子线程操作之间的关系:通过[NSThread currentThread] 获取当前线程,其中会记录线程的name和变化number(主线程变化永远为1); 使用NSThread在进行多线程开发中操作比较简单,轻量级,但是却很难解决线程依赖(顺序)的问题,例如下面只能使用了线程等待sleep来实现近似的线程同步; 另外,NSThread不能限制线程数,循环几次就会创建多个线程,耗性能;不推荐使用;

for (int i = 0; i < 10; i++) {    NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]];    thread.name=[NSString stringWithFormat:@"myThread%i",i];//设置线程名称    [thread start];}//子线程中-(void)loadImage:(NSNumber *)index {   if(index != 9) {//对非最后一张图片延迟两秒执行,使最后一张最后执行    [NSThread sleepForTimeInterval:2.0]; }    //获取下标对应图片    image = self.images[index]//注意,传进来的index是无序的.    //再转到主线程来操作UI显示.    [self performSelectorOnMainThread:@selector(updateImage:) withObject:image waitUntilDone:YES];}

多个线程虽然按顺序启动,但是实际执行未按照顺序加载照片;因为线程启动后仅仅处于就绪状态,实际是否执行要由CPU根据当前状态调度。
具体原因是:

  • NSThread每个线程的实际执行顺序并不一定按顺序执行(虽然是按顺序启动的);
  • 另外每个线程执行时网络状态可能不一致;可以用线程优先级属性threadPriority来设置优先级;

资源抢夺–(线程是不安全的)

多线程操作过程中往往多个线程是并发执行的,同一个资源可能被多个线程同时访问(读-写),造成资源抢夺,出现数据错乱;

iOS中对于解决资源抢夺两种方式:

  1. NSLock同步锁
  2. @synchronized互斥锁—常用

两种方式实现原理相似,不过代码块使用更加简单.–给可能抢占资源的读取和修改代码加锁,同时一时间只允许一条线程执行加锁代码;

//NSLock//1. 创建锁   self.lock = [[NSLock alloc] init];//2. 给读写代码加锁解锁[_lock lock];    name=[_imageNames lastObject];    [_imageNames removeObject:name];[_lock unlock];//@synchronized @synchronized (self)  {    name=[_imageNames lastObject];    [_imageNames removeObject:name];}

注意:

  • 互斥锁就是使用了线程同步技术(多条线程按顺序执行任务).
  • 使用锁会消耗大量CPU资源,影响并发效率.
  • 需要同时给抢夺资源设为atomic原子性(set方法自旋锁);保证同时只有一个线程在修改数据(互斥锁以外的线程),防止数据错误;
  • iOS开发建议:尽量避免多线程抢夺一块资源或是放到服务器端中抢夺(为了性能),如果实在无法避免,要同时使用原子性和互斥锁(为了安全);
  • 互斥锁与自旋锁区别:都保证线程同步,但是互斥锁是让线程会进入休眠(就绪状态),等其它线程执行完锁定的代码,线程就会被唤醒(执行状态);而自旋锁是以死循环方式; 所以自旋锁效率更高,也更耗CPU;
  • UIKit控件必须都让主线程操作;必须让别的线程执行到有关UI控件的代码,都跳到主线程中执行,这样是保证效率和安全的最佳模式;

扩展:也可以使用GCD来解决线程不安全问题:
在GCD中提供了一种信号机制,信号量dispatch_semaphore_t类型,支持信号通知和信号等待。每当发送一个信号通知,则信号量+1,发送信号等待时-1;如果信号量为0,那么信号会处于等待状态,直到信号量大于0开始执行;—–根据这个原理,我们可以初始化一个信号量变量,默认为1;每当有线程进入”加锁代码”,发送信号等待;代码执行完后发送信号通知;

//初始化信号量self.semaphore=dispatch_semaphore_create(1);//"加锁": 发送等待信号dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);    name=[_imageNames lastObject];    [_imageNames removeObject:name];dispatch_semaphore_signal(_semaphore);//发送信号通知

4 线程状态

线程状态分为isExecuting(正在执行)、isFinished(已经完成)、isCancellled(已经取消)三种。 调用cancel来在外部终止线程执行(这个方法仅改变了线程状态,并不能终止线程,只是使线程的isCancelled属性=YES;我们若要退出,还需在线程执行的方法中对其加以判断,如果为YES,那么再调用exit方法主动退出线程;)

图片名称

以NSThread为例,线程完整生命周期如下:

  1. 新建: 调用 alloc initWithTarget
  2. 就绪: 向线程对象发送start消息,线程对象加入”调度线程池”,等待CPU调度;(detach 方法和 performSelectorInBackground 方法会直接实例化一个线程对象并加入“可调度线程池”)
  3. 运行: 线程执行完成前,状态会在就绪和运行之间来回切换;有CPU负责;
  4. 阻塞: 可以设置条件,使线程休眠或是锁来阻塞线程执行:sleep… @synchronized(self);
  5. 死亡: 1> 正常执行完毕; 2>线程内调用[NSThread exit]强制中止线程;(可以在线程外用cancel来调控,需自己编写代码) 死亡后内存空间被释放 stackSize == 0;

5 运行循环(RunLoop)

这里写图片描述

消息循环Runloop:(运行循环)
每一个线程内部都有一个消息循环,而只有主线程的是默认开启,子线程默认关闭的.

关于自动释放池
自动释放池每一次主线程消息循环开始的时候创建自动释放池, 运行循环结束前,释放之.

使用多线程开发时,需要线程调度方法中手动添加自动池,不过目前主要使用的三种多线程方式已经不需要我们管理内存了;系统帮我们调用了释放池;不需要手动添加了;

所以目前只有使用循环创建大量的对象时,在循环内部手动创建自动释放池.( 否则只会在主线程一次循环结束释放 )

// 问题:(1)以下代码是否存在问题?(2)如果有,怎么修改?/**网上的解决办法: @autoreleasepool 放在内部,每一次循环之后,都倾倒一次自动释放池,内存管理是最好的,但是性能不好! */for (int i = 0; i < largeNumber; i++) {@autoreleasepool {    NSString *str = [NSString stringWithFormat:@"Hello "];    str = [str uppercaseString];    str = [str stringByAppendingString:@" - World"];}}

事件来源

运行循环的作用是保证程序不退出,处理输入事件,在没有事件发生时,会进入休眠状态;事件来源有两个:

  1. 输入源:提供异步事件,通常是来自其他线程或其他应用的消息;
    • 基于端口的输入源: 由内核自动标志.
    • 自定义输入源:必须手动从其他线程标志.
    • 当你创建一个输入源时,你需要给它分配给一个或多个运行循环的模式;用来监听对应模式下的运行循环, 所以只有当运行循环的模式和输入事件的模式相匹配时才会执行
  2. 定时源提供同步事件,在预定的事件或者重复间隔发生;

常见运行循环模式

  • Default默认模式: NSDefaultRunLoopMode (Cocoa),默认模式可以用于大部分的操作,大多数情况下,你应该使用这种模式来启动运行循环和设置你的输入源。
  • Event tracking(事件追踪模式): NSEventTrackingRunLoopMode (Cocoa). 在拖拽循环 或者 其他类型的用户界面 追踪循环 期间
  • Common modes(通用模式): NSRunLoopCommonModes (Cocoa);是一个可配置组,默认包含了以上所有模式.

例:图片轮播器的定时器触发问题.

NSTimer *timer = [NSTimer timerWithTimeInterval:1 arget:self selector:@selector(demo) userInfo:nil repeats:YES];//把定时器添加到当前线程消息循环中[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];// 当滚动scrollView的时候,消息循环的模式自动改变UITrackingRunLoopMode ,所以滚动时,定时器不触发;// 解决 把默认模式换成NSRunLoopCommonModes通用模式就可以在拖动时也触发定时器;

子线程的运行循环

    //开启一个子线程NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];[thread start];//往子线程的消息循环中添加输入源[self performSelector:@selector(demo1) onThread:thread withObject:nil waitUntilDone:NO];}- (void)demo {NSLog(@"I'm running %@",[NSThread currentThread]);//开启子线程的消息循环,如果开启,消息循环一直运行 ,不执行后面代码.//当消息循环中没有添加输入事件,消息循环会立即结束[[NSRunLoop currentRunLoop] run];//2秒钟之后消息循环结束//    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];NSLog(@"end");}//执行在子线程的消息循环中- (void)demo1 {NSLog(@"I'm running on runloop %@",[NSThread currentThread]);}
0 0
原创粉丝点击