iOS多线程-线程安全&线程通讯

139 阅读5分钟

线程安全

  • 资源共享:可以被多个对象访问的资源;比如全局的对象、变量、文件
    • 在多线程的环境下,共享的资源可能会被多个线程共享,也就是多个线程可能会操作同一块资源
    • 当多个线程操作同一块资源时,很容易引发数据错乱和数据安全问题,数据有可能丢失,有可能增加,有可能错乱
  • 线程安全:同一块资源,被多个线程同时读写操作时,任然能够得到正确的结果,称之为线程是安全的

买票案例

@implementation ViewController{
    NSInteger _ticket;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _ticket = 10;
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSThread *th1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    th1.name = @"th1";
    [th1 start];
    
    NSThread *th2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    th2.name = @"th2";
    [th2 start];
}

-(void)saleTicket{
    while (1) {
        if (_ticket>0) {
            _ticket--;
            NSLog(@"%@ 余票%zd张",[NSThread currentThread].name,_ticket);
        }else{   
            NSLog(@"%@ 票卖完了",[NSThread currentThread].name);
            break;
        }
    }
}
  • 资源抢占结果:数据错乱

  • 原因:CPU在第一个线程进行到一半还没打印的时候,调度到第二个线程,第二个线程打印后,重新调度回第一个线程,此时会根据第一个线程之前保存的代码执行行数和变量数据继续执行打印

  • 解决方法:添加互斥锁(同步锁)

    • 互斥锁,就是使用了线程同步技术
    • 互斥锁锁定的范围应该尽量小,但是一定要锁住资源的读写部分
    • 加锁后程序执行的效率比不加锁的时候要低,因为线程要等待解锁
    • 此时线程进入休眠,CPU不会调度该线程
    • 牺牲了性能保证了安全性
/*
 互斥锁/同步锁
 加了锁以后,能保证一个线程来的时候必须先把锁包住的代码执行完,其他线程才能进入
 @synchronized代表加一把锁,被它包住的代码,只有等某个加锁的线程完全执行完,才能让其他线程进入
 self是锁对象,任何继承自NSObject的都可以称为锁对象
 只不过只有锁对象是全局的才能锁住,局部的锁不住(*此处视频有误,锁不住并非因为obj被释放,并且信号量obj必须为线程执行方法外的全局变量)
 写self是因为是最方便拿到的全局对象
 加锁这个代码,一般不会写在客户端,只写在服务器端,因为数据一般都是在服务器上保存,只有服务器才会存在数据共享的问题
 而且还有个原因:加锁很耗资源,所以客户端也不会用,毕竟客户端以流畅为主
 */     
 -(void)saleTicket{
    while (1) {
        @synchronized (self) {
            if (_ticket>0) {
                _ticket--;
                NSLog(@"%@ 余票%zd张",[NSThread currentThread].name,_ticket);
            }else{  
                NSLog(@"%@ 票卖完了",[NSThread currentThread].name);
                break;
            }
        }
    }
}

互斥锁和自旋锁

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

原子属性

  • nonatomic:非原子属性
  • atomic:原子属性
    • 线程安全的,针对多线程设计的属性修饰符,是默认值
    • 特点:单写多读
    • 单写多读:保证同一时间,只有一个线程能够执行setter方法,但是可以有多个线程执行getter方法
    • atomic属性的setter里面里面有一把锁,叫做自旋锁
    • 原子属性的setter方法是线程安全的;但是,getter方法不是线程安全的

模拟原子属性

@interface ViewController ()

@property(atomic,assign)int age;

@end

@implementation ViewController
//重写setter和getter方法后,系统就不会自动生成带下划线的成员变量
@synthesize age = _age;

-(void)setAge:(int)age{
    //互斥锁模拟自旋锁
    @synchronized (self) {
        _age = age;
    }
    //自旋锁
    //static OSSpinLock lock = OS_SPINLOCK_INIT;
    //OSSpinLockLock(&lock);
    //_age = age;
    //OSSpinLockUnlock(&lock);
}

-(int)age{
    return _age;
}

拓展

  • 苹果所有属性都声明为nonatomic,原子属性和非原子属性的性能几乎一样
  • 尽量将加锁,资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力
  • 为了流畅的用户体验,UIKit类库的线程都是不安全的,所以我们需要在主线程(UI线程)上更新UI
  • 所有包含NSMutable的类都是线程不安全的;在做多线程开发的时候,需要注意多线程同时操作可变对象的线程安全问题

线程间通讯(异步加载网络图片)

@interface ViewController ()

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

@end

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //子线程加载网络数据,主线程里更新UI或者更新其他NSMutable的数据
    [self performSelectorInBackground:@selector(loadImg) withObject:nil];
}

-(void)loadImg{
    //url:统一资源定位符
    //设置为网络路径图片
    NSURL *url = [NSURL URLWithString:@"http://www.gaibar.com/uploads/allimg/160218/40528-16021R35336-lp.jpg"];
    //去根据路径去网络读取图片数据
    NSData *data = [NSData dataWithContentsOfURL:url];
    
    NSLog(@"读取前%@",[NSThread currentThread]);
    //主线程执行
    //参数3:代表是否等主线程的方法执行完再执行这句代码后面的代码
    [self performSelectorOnMainThread:@selector(updateUI:) withObject:data waitUntilDone:NO];
    
    NSLog(@"读取后%@",[NSThread currentThread]);
}

-(void)updateUI:(NSData *)data{
    NSLog(@"更新%@",[NSThread currentThread]);
    _imgView.image = [UIImage imageWithData:data];
}