NSThread 实例

下载图片

新建singeView app
新建项目,并在xib文件上放置一个imageView控件。按住control键拖到viewControll
er.h文件中创建imageView IBOutlet
ViewController.m中实现:

// 
//  ViewController.m 
//  NSThreadDemo 
// 
//  Created by rongfzh on 12-9-23. 
//  Copyright (c) 2012年 rongfzh. All rights reserved. 
// 
 
#import "ViewController.h" 
#define kURL @"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg" 
@interface ViewController () 
 
@end  
@implementation ViewController 
 
-(void)downloadImage:(NSString *) url{ 
    NSData *data = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:url]]; 
    UIImage *image = [[UIImage alloc]initWithData:data]; 
    if(image == nil){ 
         
    }else{ 
        [self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES]; 
    } 
} 
 
-(void)updateUI:(UIImage*) image{ 
    self.imageView.image = image; 
} 
 
 
- (void)viewDidLoad 
{ 
    [super viewDidLoad]; 
     
//    [NSThread detachNewThreadSelector:@selector(downloadImage:) toTarget:self withObject:kURL]; 
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(downloadImage:) object:kURL]; 
    [thread start]; 
} 
 
- (void)didReceiveMemoryWarning 
{ 
    [super didReceiveMemoryWarning]; 
    // Dispose of any resources that can be recreated. 
} 

@end  

线程下载完图片后通知主线程更新界面

[self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];

performSelectorOnMainThread是NSObject的方法,除了可以更新主线程的数据外,还可以更新其他线程的比如:

performSelector:onThread:withObject:waitUntilDone: 

卖票

共享资源的问题

  • 共享资源
    1.资源 : 一个全局的对象、一个全局的变量、一个文件.
    2. 共享 : 可以被多个对象访问.
    3. 共享资源 :可以被多个对象访问的资源.比如全局的对象,变量,文件.
  • 在多线程的环境下,共享的资源可能会被多个线程共享,也就是多个线程可能会操作同一块资源.
  • 当多个线程操作同一块资源时,很容易引发数据错乱和数据安全问题,数据有可能丢失,有可能增加,有可能错乱.

  • 经典案例 : 卖票.

  • 线程安全

  • 同一块资源,被多个线程同时读写操作时,任然能够得到正确的结果,称之为线程是安全的.

    NSThread 实例_第1张图片
    卖票系统的简单逻辑

    开发提示

  • 实际开发中确定开发思路逻辑比及时的写代码更重要.

  • 多线程开发的复杂度相对较高,在开发时可以按照以下套路编写代码
    1.首先确保单个线程执行正确
    2.然后再添加线程
  • 代码实现卖票逻辑

  • 先定义共享资源
  • @interface ViewController ()
    
    /// 总票数(共享的资源)
    @property (nonatomic,assign) int tickets;
    
    @end
    

  • 初始化余票数(共享资源)
  • - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 设置余票数
        self.tickets = 10;
    }
    

  • 卖票逻辑实现
  • -  (void)saleTickets
    {
        // while 循环保证每个窗口都可以单独把所有的票卖完
        while (YES) {
    
            // 模拟网络延迟
            [NSThread sleepForTimeInterval:1.0];
    
            // 判断是否有票
            if (self.tickets>0) {
                // 有票就卖
                self.tickets--;
                // 卖完一张票就提示用户余票数
                NSLog(@"剩余票数 => %zd %@",self.tickets,[NSThread currentThread]);
            } else {
                // 没有就提示用户
                NSLog(@"没票了");
                // 此处要结束循环,不然会死循环
                break;
            }
        }
    }
    

    单线程

  • 先确保单线程中运行正常
  • - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
        // 在主线程中卖票
        [self saleTickets];
    }
    

    多线程

  • 如果单线程运行正常,就修改代码,实现多线程环境
  • - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
        // 在主线程中卖票
        // [self saleTickets];
    
        // 售票口 A
        NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
        thread1.name = @"售票口 A";
        [thread1 start];
    
        // 售票口 B
        NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
        thread2.name = @"售票口 B";
        [thread2 start];
    }
    

    资源抢夺结果

  • 数据错乱,数据增加.
  • NSThread 实例_第2张图片
    数据出错

    出错原因分析

  • 问题分析
  • NSThread 实例_第3张图片
    问题分析

  • 问题解决
  • NSThread 实例_第4张图片
    问题解决

    解决多线程操作共享资源的问题

    解决办法 : 使用互斥锁/同步锁.

  • 添加互斥锁
  • - (void)saleTickets
    {
        // while 循环保证每个窗口都可以单独把所有的票卖完
        while (YES) {
            // // 模拟休眠网络延迟
            [NSThread sleepForTimeInterval:1.0];
    
            // 添加互斥锁
            @synchronized(self) {
                // 判断是否有票
                if (self.tickets>0) {
                    // 有票就卖
                    self.tickets--;
                    // 卖完一张票就提示用户余票数
                    NSLog(@"剩余票数 => %zd",self.tickets);
                } else {
                    // 没有就提示用户
                    NSLog(@"没票了");
                    // 此处要结束循环,不然会死循环
                    break;
                }
            }
        }
    }
    

    互斥锁小结

    互斥锁,就是使用了线程同步技术.

    同步锁/互斥锁:可以保证被锁定的代码,同一时间,只能有一个线程可以操作.

    self :锁对象,任何继承自NSObject的对像都可以是锁对象,因为内部都有一把锁,而且默认是开着的.

    锁对象 : 一定要是全局的锁对象,要保证所有的线程都能够访问,self是最方便使用的锁对象.

    互斥锁锁定的范围应该尽量小,但是一定要锁住资源的读写部分.

    加锁后程序执行的效率比不加锁的时候要低.因为线程要等待解锁.

    牺牲了性能保证了安全性.

    原子属性

  • nonatomic : 非原子属性

  • atomic : 原子属性

  • 线程安全的,针对多线程设计的属性修饰符,是默认值.
    保证同一时间只有一个线程能够写入,但是同一个时间多个线程都可以读取.
    单写多读 : 单个线程写入write,多个线程可以读取read.
    atomic 本身就有一把锁,自旋锁.

  • nonatomic和atomic对比

  • nonatomic : 非线程安全,适合内存小的移动设备.
    atomic : 线程安全,需要消耗大量的资源.性能比非原子属性要差.

  • iOS开发的建议

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

    模拟原子属性

  • 定义属性
  • /// 非原子属性
    @property (nonatomic,strong) NSObject *obj1;
    /// 原子属性:内部有"自旋锁"
    @property (atomic,strong) NSObject *obj2;
    /// 模拟原子属性
    @property (atomic,strong) NSObject *obj3;
    

  • 重写非原子属性的setter和getter方法
  • 重写了原子属性的setter方法之后,会覆盖原子属性内部的自旋锁,使其失效.然后我们加入互斥锁,来模拟但写多读.

    重写了属性的setter和getter方法之后,系统就不会再帮我们生成待下划线的成员变量.使用合成指令@synthesize,就可以手动的生成带下划线的成员变量.

    // 合成指令
    @synthesize obj3 = _obj3;
    
    /// obj3的setter方法
    - (void)setObj3:(NSObject *)obj3
    {
        @synchronized(self) {
            _obj3 = obj3;
        }
    }
    
    /// obj3的getter方法
    - (NSObject *)obj3
    {
        return _obj3;
    }
    
    性能测试
    /// 测试"非原子属性","互斥锁","自旋锁"的性能
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
        NSInteger largeNum = 1000*1000;
    
        NSLog(@"非原子属性");
        CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
        for (int i = 0; i < largeNum; i++) {
            self.obj1 = [[NSObject alloc] init];
        }
        NSLog(@"非原子属性 => %f",CFAbsoluteTimeGetCurrent()-start);
    
        NSLog(@"原子属性");
        start = CFAbsoluteTimeGetCurrent();
        for (int i = 0; i < largeNum; i++) {
            self.obj2 = [[NSObject alloc] init];
        }
        NSLog(@"原子属性 => %f",CFAbsoluteTimeGetCurrent()-start);
    
        NSLog(@"模拟原子属性");
        start = CFAbsoluteTimeGetCurrent();
        for (int i = 0; i < largeNum; i++) {
            self.obj3 = [[NSObject alloc] init];
        }
        NSLog(@"模拟原子属性 => %f",CFAbsoluteTimeGetCurrent()-start);
    }
    
    测试结果
    NSThread 实例_第5张图片

    两种锁NSCondition ,NSLock

    除了使用指令@synchronized , 还有两种锁,一种NSCondition ,一种是:NSLock。

        #import   
          
        @class ViewController;  
          
        @interface AppDelegate : UIResponder   
        {  
            int tickets;  
            int count;  
            NSThread* ticketsThreadone;  
            NSThread* ticketsThreadtwo;  
            NSCondition* ticketsCondition;  
            NSLock *theLock;  
        }  
        @property (strong, nonatomic) UIWindow *window;  
          
        @property (strong, nonatomic) ViewController *viewController;  
          
        @end  
    
        - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions  
        {  
              
            tickets = 100;  
            count = 0;  
            theLock = [[NSLock alloc] init];  
            // 锁对象  
            ticketsCondition = [[NSCondition alloc] init];  
            ticketsThreadone = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];  
            [ticketsThreadone setName:@"Thread-1"];  
            [ticketsThreadone start];  
              
              
            ticketsThreadtwo = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];  
            [ticketsThreadtwo setName:@"Thread-2"];  
            [ticketsThreadtwo start];  
              
            self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];  
            // Override point for customization after application launch.  
            self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];  
            self.window.rootViewController = self.viewController;  
            [self.window makeKeyAndVisible];  
            return YES;  
        }  
          
        - (void)run{  
            while (TRUE) {  
                // 上锁  
        //        [ticketsCondition lock];  
                [theLock lock];  
                if(tickets >= 0){  
                    [NSThread sleepForTimeInterval:0.09];  
                    count = 100 - tickets;  
                    NSLog(@"当前票数是:%d,售出:%d,线程名:%@",tickets,count,[[NSThread currentThread] name]);  
                    tickets--;  
                }else{  
                    break;  
                }  
                [theLock unlock];  
        //        [ticketsCondition unlock];  
            }  
        }  
    

    NSCondition已经注释了。

    两种锁都可以通过 [ticketsCondition signal]; 发送信号的方式,在一个线程唤醒另外一个线程的等待。
    比如:

        #import "AppDelegate.h"  
          
        #import "ViewController.h"  
          
        @implementation AppDelegate  
          
        - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions  
        {  
              
            tickets = 100;  
            count = 0;  
            theLock = [[NSLock alloc] init];  
            // 锁对象  
            ticketsCondition = [[NSCondition alloc] init];  
            ticketsThreadone = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];  
            [ticketsThreadone setName:@"Thread-1"];  
            [ticketsThreadone start];  
              
            ticketsThreadtwo = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];  
            [ticketsThreadtwo setName:@"Thread-2"];  
            [ticketsThreadtwo start];  
              
            NSThread *ticketsThreadthree = [[NSThread alloc] initWithTarget:self selector:@selector(run3) object:nil];  
            [ticketsThreadthree setName:@"Thread-3"];  
            [ticketsThreadthree start];      
            self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];  
            // Override point for customization after application launch.  
            self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];  
            self.window.rootViewController = self.viewController;  
            [self.window makeKeyAndVisible];  
            return YES;  
        }  
          
        -(void)run3{  
            while (YES) {  
                [ticketsCondition lock];  
                [NSThread sleepForTimeInterval:3];  
                [ticketsCondition signal];  
                [ticketsCondition unlock];  
            }  
        }  
          
        - (void)run{  
            while (TRUE) {  
                // 上锁  
                [ticketsCondition lock];  
                [ticketsCondition wait];  
                [theLock lock];  
                if(tickets >= 0){  
                    [NSThread sleepForTimeInterval:0.09];  
                    count = 100 - tickets;  
                    NSLog(@"当前票数是:%d,售出:%d,线程名:%@",tickets,count,[[NSThread currentThread] name]);  
                    tickets--;  
                }else{  
                    break;  
                }  
                [theLock unlock];  
                [ticketsCondition unlock];  
            }  
        }  
    

    wait是等待,添加了 线程3 去唤醒其他两个线程锁中的wait

    还有其他的一些锁对象,比如:循环锁NSRecursiveLock,条件锁NSConditionLock,分布式锁NSDistributedLock等等,可以自己看官方文档学习

    互斥锁和自旋锁对比

  • 共同点
  • 都能够保证同一时间,只有一条线程执行锁定范围的代码

  • 不同点
  • 互斥锁:如果发现有其他线程正在执行锁定的代码,线程会进入休眠状态,等待其他线程执行完毕,打开锁之后,线程会重新进入就绪状态.等待被CPU重新调度.
    自旋锁:如果发现有其他线程正在执行锁定的代码,线程会以死循环的方式,一直等待锁定代码执行完成.

    开发建议

    所有属性都声明为nonatomic,原子属性和非原子属性的性能几乎一样.

    尽量避免多线程抢夺同一块资源.

    要实现线程安全,必须要用到锁.无论什么锁,都是有性能消耗的.

    自旋锁更适合执行非常短的代码.死循环内部不适合写复杂的代码.

    尽量将加锁,资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力.

    为了流畅的用户体验,UIKit类库的线程都是不安全的,所以我们需要在主线程(UI线程)上更新UI.

    所有包含NSMutable的类都是线程不安全的.在做多线程开发的时候,需要注意多线程同时操作可变对象的线程安全问题.

    你可能感兴趣的:(NSThread 实例)