关闭

iOS多线程

标签: 多线程NSThreadGCDNSOperation
281人阅读 评论(0) 收藏 举报
分类:

iOS 多线程

iOS程序启动时,在创建一个进程的同时,会开启一个线程,该线程被称为主线程。

在一般情况下,我们所做的操作都在主线程执行,特别强调的是UI元素的更新、显示等操作必须在主线程中完成(只有主线程有直接修改UI的能力)。然而,在应用程序中,往往存在比较耗时的操作,如请求网络图片、歌曲等资源。这些操作若在主线程中执行,由于耗时比较长,将会影响用户的其他交互(如点击按钮,无反应等)。

针对上述情况,我们应该单独开启辅助线程来执行这些耗时的操作,以保证主线程的流畅。

在iOS中提供了三种多线程技术,分别为:

1)     NSThread

NSThread比其他两个轻量级,使用简单,但是需要自己管理线程的生命周期、睡眠以及唤醒等。

2)     NSOperation、NSOperationQueue

不需要关心线程管理,可以把精力放在自己需要执行的操作上。

3)     GCD(Crand Central Dispatch)

基于C语言的框架,可以充分利用多核,是官方推荐的多线程技术。

4)     此外,NSObject也提供了对多线程的简单支持;

 

1、       NSObject

在使用多线程时,需要注意多线程的内存泄露问题。每个线程都维护它自己的 NSAutoreleasePool的栈对象Cocoa希望在每个当前线程的栈里面有一个可用的自动释放池。如果一个自动释放池不可用,对象将不会给释放,从而造成内存泄露对于 Application Kit 的主线程通常它会自动创建并消耗一个自动释放池,但是辅助线程(和其他只有 Foundationd 的程序)在使用Cocoa前必须自己手工创建。如果你的线程是长时间运行的,那么有可能潜在产生很多自动 释放的对象,你应该周期性的销毁它们并创建自动释放池(就像Application Kit 对 主线程那样)。否则,自动释放对象将会积累并造成内存大量占用。如果你的脱离线 程没有使用Cocoa,你不需要创建一个自动释放池。

所以在使用多线程执行的方法前要嵌套@autoreleasepool,否则可能会出现内存泄露问题。


NSObject提供了对多线程的支持,如下

1)  performSelectorInBackground:withObject:

通常,由于线程管理相对比较繁琐,而耗时的任务又无法知道其准确的完成时间,因此可以使用该方法直接新建一个后台线程。例如,在大型交互式游戏中,通常使用此方法播放音效。

2)     performSelectorOnMainThread:withObject:waitUntilDone:

在后台线程或辅助线程中,需要更新UI时,可以使用此方法,如下

        …

        //在主线程更新UI

        //imageView.image=[UIImageimageNamed:imageName];

[imageView performSelectorOnMainThread:@selector(setImage:)withObject:[UIImageimageNamed:imageName]waitUntilDone:YES];

2、       NSThread

NSThread创建线程的方法有:

1)       + (void)detachNewThreadSelector:toTarget: withObject:

该方法会创建一个线程,并立即启动该线程;

2)       - (id)initWithTarget:selector:object:

该方法只是创建一个线程,需要调用start方法才能执行线程,如下

//多线程,随机更新UIImageView的图片

-(void) freshImages

{

    for (UIImageView *imageViewin _imagesViews)

    {

        //创建新的线程,并立即执行

        //[NSThreaddetachNewThreadSelector:@selector(freshImage:) toTarget:selfwithObject:imageView];

     

        //创建新的线程,但并不会立即执行,需要调用start方法

        NSThread * thread=[[NSThreadalloc] initWithTarget:selfselector:@selector(freshImage:)object:imageView];

        [thread start];

    }

}

 

-(void) freshImage:(UIImageView *) imageView;

{

    @autoreleasepool

    {

        int index=arc4random_uniform(17)+1;

        NSString * imageName=nil;

        if (index<10)

        {

            imageName=[NSStringstringWithFormat:@"NatGeo0%d",index];

        }

        else

        {

            imageName=[NSStringstringWithFormat:@"NatGeo%d",index];

        }

        //在主线程更新UI

        [imageView performSelectorOnMainThread:@selector(setImage:)withObject:[UIImageimageNamed:imageName] waitUntilDone:YES];

    }

}

 

3、       NSOperation和NSOperationQueue

NSOperation和NSOperationQueue工作原理:    

1)     NSOperation封装要执行的操作;

2)     将创建好的NSOperation对象放在NSOperationQueue中;

3)     启动NSOperationQueue开始新的线程执行队列中的操作;

NSOperation子类:

1)     NSInvocationOperation;

2)     NSBlockOperation;

注意:使用多线程时,通常需要控制多线程的并发数,因为线程会消耗系统资源,同时运行的线程过多,系统会变慢。NSOperationQueue可以设置线程的最大并发数 setMaxConcurrentOperationCount

//多线程,随机更新UIImageView的图片,需要在主队列中更新UI

-(void)operationFreshImages

{

    //操作队列可以控制线程的并发数量,操作队列也可以设置操作间的依赖关系,而控制操作的执行顺序,但要避免循环依赖

    //[_operationQueuesetMaxConcurrentOperationCount:4];

    for (UIImageView  *imageView in _imagesViews)

    {

        /*

        NSOperation * op=[[NSInvocationOperationalloc] initWithTarget:self selector:@selector(operationFreshImage:)object:imageView];

         //[op start]方法,将会使该op在主线程队列上运行,并不会开启新的线程,需要配合操作队列使用

        [_operationQueue addOperation:op];

         */

        NSOperation * op=[NSBlockOperationblockOperationWithBlock:^{

            [self blockOperationFreshImage:imageView];

        }];

        //将操作添加到操作队列,就会开启新的线程

        [_operationQueue addOperation:op];

    }

}

 

-(void) operationFreshImage:(UIImageView *) imageView

{

    @autoreleasepool

    {

        int index=arc4random_uniform(17)+1;

        NSString * imageName=nil;

        if (index<10)

        {

            imageName=[NSStringstringWithFormat:@"NatGeo0%d",index];

        }

        else

        {

            imageName=[NSStringstringWithFormat:@"NatGeo%d",index];

        }

        //在主线程更新UI

        //imageView.image=[UIImageimageNamed:imageName];

        //[imageViewperformSelectorOnMainThread:@selector(setImage:) withObject:[UIImageimageNamed:imageName] waitUntilDone:YES];

        [[NSOperationQueuemainQueue] addOperationWithBlock:^{

            imageView.image=[UIImageimageNamed:imageName];

        }];

    }

}

 

-(void) blockOperationFreshImage:(UIImageView *) imageView

{

    @autoreleasepool

    {

        int index=arc4random_uniform(17)+1;

        NSString * imageName=nil;

        if (index<10)

        {

            imageName=[NSStringstringWithFormat:@"NatGeo0%d",index];

        }

        else

        {

            imageName=[NSStringstringWithFormat:@"NatGeo%d",index];

        }

        //在主线程更新UI

        //imageView.image=[UIImageimageNamed:imageName];

        //[imageViewperformSelectorOnMainThread:@selector(setImage:) withObject:[UIImageimageNamed:imageName] waitUntilDone:YES];

        //NSOperation实例为NSBlockOperation,需要在主队列中更新UI

        [[NSOperationQueue mainQueue] addOperationWithBlock:^{

            imageView.image=[UIImageimageNamed:imageName];

        }];

    }

}

 

 

 

 

4、       GCD

GCD是基于C语言的框架,在GCD中线程的执行主要依靠执行线程的队列,在GCD中,有三中队列:

1)     全局队列;所有添加到主队列中的任务都是并发执行的;dispatch_get_global_queue(QOS_CLASS_DEFAULT,0);

2)     串行队列:所有添加到该队列中的任务都是顺序执行的;

dispatch_queue_create("sq",DISPATCH_QUEUE_SERIAL);

3)     主队列:所有添加到该队列中的任务都是在主线程中执行的;

        dispatch_get_main_queue();

由此可知,任务是否能够并发执行,关键在于任务所在的队列,而只有全局队列中的任务才能并发执行

工作原理:

1)     让程序平行排队任务,根据可用的处理资源,安排他们在任何可用处理器上执行任务;

2)     要执行的任务可以是一个函数或者一个block;

3)     dispatch_async(异步操作),dispatch_sync(同步操作);

4)     dispatch_notify可以实现监听一组任务是否完成,完成后可以得到通知。

GCD的优点:

1)     充分利用多核;

2)     所有的多线程代码集中在一起,便于维护;

3)     GCD中无需使用@autoreleasepool;

注意:在主队列中更新UI,最好使用同步方法

//多线程,随机更新UIImageView的图片,需要在主队列中更新UI

-(void) gcd

{

    /*

       1.全局队列,所有任务是并发(异步)执行的;

       2.主队列,主线程队列,串行执行

       3.串行队列需要创建,而不行get,dispatch_queue_tqueue=dispatch_queue_create("sq", DISPATCH_QUEUE_SERIAL);

     */

   

    //1. 获取全局队列,所有任务是并发执行的

    dispatch_queue_t queue=dispatch_get_global_queue(QOS_CLASS_DEFAULT,0);

    //2. 在全局队列上,异步执行

    for (UIImageView * imageViewin _imagesViews)

    {

        dispatch_async(queue, ^{

            NSLog(@"%@",[NSThreadcurrentThread]);

            int index=arc4random_uniform(17)+1;

            NSString * imageName=nil;

            if (index<10)

            {

                imageName=[NSStringstringWithFormat:@"NatGeo0%d",index];

            }

            else

            {

                imageName=[NSStringstringWithFormat:@"NatGeo%d",index];

            }

            //3. 在主队列中更新UI

            dispatch_sync(dispatch_get_main_queue(), ^{

                 NSLog(@"%@",[NSThreadcurrentThread]);

                imageView.image=[UIImageimageNamed:imageName];

            });

           

        });

    }

}

5、       多线程同步

当多个线程对同一个资源出现争夺的时候,就会出现线程安全问题。

5.1、volatile

为了更好的理解多线程竞争资源所可能出现的问题,首先要了解下“指令重排”和“内存可见性”。

为了达到最佳的性能,编译器通常会对汇编基本的指令进行重新排序来尽可能保持处理器的指令流水线。作为优化的一部分,编译器有可能会对访问内存的指令(不存在依赖关系)进行重新排序。不幸的是,靠编译器检测到所有的内存依赖的操作几乎总是不大可能,那么编译器调整指令顺序,将导致不正确的结果。

内存屏障是一个用来确保内存操作按照正确顺序工作的非阻塞同步工具(由硬件实现)。内存屏障的作用就像 一个栅栏,迫使处理器来完成位于障碍掐面的任何加载和存储操作,才允许它执行位于屏障之后的加载和存储操作。

至于“内存可见性”,将涉及到线程的内存空间。线程之间共享的变量存储在主内存中,除此之外,每个线程都有一个私有的本地内存,本地内存存储线程间共享变量的副本。此后,每个线程之间都直接操作本地内存空间中的副本(至于何时刷新到主内存是不确定的),若线程之间需要交互则通过主内存进行数据交互,也即线程刷新副本至主内存或从主内存重新读取。


“内存可见性”是指当一个变量值改变时,改变后的值确保对其他线程可见的(也即立即刷新会主内存)。

而volatile修饰的变量,只是表明该变量任何时候对其他线程都是可见的,也即volatile修改的变量值的存储都会直接在主内存进行。

5.2、NSLock

锁是最常用的同步工具。在Cocoa程序中NSLock中实现了一个简单的互斥锁。所有锁的接口实际上都是通过NSLocking协议定义的,它定义了lock和unlock方法,来获取或释放锁。

除了标准的锁行为,NSLock类还增加了tryLock和lockBeforeData:方法。Cocoa还提供了其他类型的锁,如NSRecursiveLock等,在此不再详述。

 

5.3、@synchronized

@synchronized指令是在OC代码中创建一个互斥锁非常方便的方法。@synchronized指令做和互斥锁一样的工作。然而,在这种情况下,不需要显示创建一个互斥锁或锁对象。相反,只需要简单的使用OC对象作为锁的令牌即可。

创建给@synchronized指定的令牌是一个用来区别保护块的唯一标识符。如果有两个不同的线程,每次在一个线程传递了一个不同的对象给 @synchronized 参数,那么每次都将会拥有它的锁,并持续处理,中间不被其他线程阻塞。然 而,如果你传递的是同一个对象,那么多个线程中的一个线程会首先获得该锁,而其它线程将会被阻塞知道第一个线程退出临界区。

作为一种预防措施,@synchronized块还隐式的添加一个异常处理来保护代码,该异常处理会在异常抛出的时候自动的释放互斥锁。

5.4、案例

下面是一个简单的多线程卖票程序案例。

/*

  数据模型,单例

 */

 

 

 

#import <Foundation/Foundation.h>

 

@interface Ticket : NSObject

/*

   在多线程应用中,所有被抢夺的资源需要设置为原子属性,系统会在多线程抢夺时,保证该属性有且仅有一个线程能够访问。

   atomic必须与@synchronized一起使用

 */

@property(atomic,assign)NSUInteger ticketNum;

 

+(instancetype) sharedTicket;

@end

static Ticket * instance=nil;

 

@implementation Ticket

 

/*

   使用内存地址实例化对象,所有实例方法,最终都会调用此方法

 */

+ (instancetype)allocWithZone:(struct_NSZone *)zone

{

    //确保执行一次

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        //记录实例化对象

        instance=[superallocWithZone:zone];

    });

    return instance;

}

 

+(instancetype) sharedTicket

{

    //确保执行一次

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        instance=[[Ticketalloc] init];

    });

    return instance;

}

@end

 

/*

  控制器

 */

 

#import <UIKit/UIKit.h>

 

@interface TicketViewController :UIViewController

@end

 

#import "TicketViewController.h"

#import "Ticket.h"

@interface TicketViewController ()

{

    UITextView * _textView;

}

@end

 

@implementation TicketViewController

 

- (void)viewDidLoad

{

    [super viewDidLoad];

    self.title=@"卖票";

    self.view.backgroundColor=[UIColorwhiteColor];

    //

    _textView=[[UITextViewalloc] initWithFrame:self.view.frame];

    _textView.editable=NO;

    [self.viewaddSubview:_textView];

   

    //数据

    [Ticket sharedTicket].ticketNum=30;

    [self gcd];

}

 

-(void) appendText:(NSString *) text

{

    //1. 获取textView文本

    NSMutableString * str=[[NSMutableStringalloc] initWithString:_textView.text];

    //2. 追加文本到textView

    [str appendString:[NSStringstringWithFormat:@"%@\n",text]];

    //3. 更新textView文本

    _textView.text=str;

    //4.使textView滚动到文本底部

    NSRange range=NSMakeRange(str.length-1,1);

    [_textView scrollRangeToVisible:range];

}

 

 

-(void) sales:(NSString *) name

{

    while (true)

    {

        if ([name isEqualToString:@"gcd-1"])

        {

            [NSThread sleepForTimeInterval:1.0f];

        }

        else

        {

             [NSThread sleepForTimeInterval:0.2f];

        }

 

        //同步锁synchronized要锁的范围,对被抢夺的资源进行修改/读取的代码部分

        @synchronized(self)

        {

            if ([TicketsharedTicket].ticketNum>0)

            {

                [Ticket sharedTicket].ticketNum--;

                NSString * text=[NSStringstringWithFormat:@"剩余票数:%lu,线程:%@",[TicketsharedTicket].ticketNum,name];

                //在主队列,更新界面

                dispatch_async(dispatch_get_main_queue(), ^{

                   [self appendText:text];

                });

            }

            else

            {

                break;

            }

           

        }

    }

}

#pragma mark  - gcd

-(void) gcd

{

    //1.获取全局队列

    dispatch_queue_t queue=dispatch_get_global_queue(QOS_CLASS_DEFAULT,0);

    //2.异步执行

    /*

    dispatch_async(queue, ^{

        [self sales:@"gcd-1"];

    });

    dispatch_async(queue, ^{

        [self sales:@"gcd-2"];

    });

     */

   

    //3. gcd可以将关联的操作,定义到一个群组中,定义到群组中之后,当所有线程执行完,可以获得通知

    //3.1 建立群组

   

    dispatch_group_t group=dispatch_group_create();

    dispatch_group_async(group, queue, ^{

        [self sales:@"gcd-1"];

    });

    dispatch_group_async(group, queue, ^{

        [self sales:@"gcd-2"];

    });

    dispatch_group_async(group, queue, ^{

        [self sales:@"gcd-3"];

    });

 

    dispatch_group_notify(group, queue, ^{

        NSLog(@"售完!");

    });

   

}

@end

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:40001次
    • 积分:859
    • 等级:
    • 排名:千里之外
    • 原创:48篇
    • 转载:2篇
    • 译文:0篇
    • 评论:2条
    文章分类
    最新评论
  • 图片验证码

    qq_36652294: 楼主,你好,我想问一下,请求到controller之后,前台jap页面怎么接受这个图片并显示呢?我使...

  • 图片验证码

    qq_36652294: 楼主,你好,我想问一下,请求到controller之后,前台jap页面怎么接受这个图片并显示呢?我使...