下载图片
新建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:
卖票
共享资源的问题
<li> 共享资源
1.资源 : 一个全局的对象、一个全局的变量、一个文件.
2. 共享 : 可以被多个对象访问.
3. 共享资源 :可以被多个对象访问的资源.比如全局的对象,变量,文件.
</li>
<li> 在多线程的环境下,共享的资源可能会被多个线程共享,也就是多个线程可能会操作同一块资源.</li>
<li> 当多个线程操作同一块资源时,很容易引发数据错乱和数据安全问题,数据有可能丢失,有可能增加,有可能错乱.</li>
<li> 经典案例 : 卖票.</li>
<li> 线程安全</li>
同一块资源,被多个线程同时读写操作时,任然能够得到正确的结果,称之为线程是安全的.
开发提示
<li> 实际开发中确定开发思路逻辑比及时的写代码更重要.</li>
<li> 多线程开发的复杂度相对较高,在开发时可以按照以下套路编写代码
1.首先确保单个线程执行正确
2.然后再添加线程 </li>
代码实现卖票逻辑
<li>先定义共享资源</li>
@interface ViewController ()
/// 总票数(共享的资源)
@property (nonatomic,assign) int tickets;
@end
<li>初始化余票数(共享资源)</li>
- (void)viewDidLoad {
[super viewDidLoad];
// 设置余票数
self.tickets = 10;
}
<li>卖票逻辑实现</li>
- (void)saleTickets
{
// while 循环保证每个窗口都可以单独把所有的票卖完
while (YES) {
// 模拟网络延迟
[NSThread sleepForTimeInterval:1.0];
// 判断是否有票
if (self.tickets>0) {
// 有票就卖
self.tickets--;
// 卖完一张票就提示用户余票数
NSLog(@"剩余票数 => %zd %@",self.tickets,[NSThread currentThread]);
} else {
// 没有就提示用户
NSLog(@"没票了");
// 此处要结束循环,不然会死循环
break;
}
}
}
单线程
<li>先确保单线程中运行正常</li>
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 在主线程中卖票
[self saleTickets];
}
多线程
<li>如果单线程运行正常,就修改代码,实现多线程环境</li>
- (void)touchesBegan:(NSSet<UITouch *> *)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];
}
资源抢夺结果
<li> 数据错乱,数据增加.</li>
出错原因分析
<li> 问题分析</li>
<li> 问题解决</li>
解决多线程操作共享资源的问题
解决办法 : 使用互斥锁/同步锁.
<li> 添加互斥锁</li>
- (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是最方便使用的锁对象.
互斥锁锁定的范围应该尽量小,但是一定要锁住资源的读写部分.
加锁后程序执行的效率比不加锁的时候要低.因为线程要等待解锁.
牺牲了性能保证了安全性.
原子属性
<li>nonatomic : 非原子属性</li>
<li>atomic : 原子属性</li>
线程安全的,针对多线程设计的属性修饰符,是默认值.
保证同一时间只有一个线程能够写入,但是同一个时间多个线程都可以读取.
单写多读 : 单个线程写入write,多个线程可以读取read.
atomic 本身就有一把锁,自旋锁.<li>nonatomic和atomic对比</li>
nonatomic : 非线程安全,适合内存小的移动设备.
atomic : 线程安全,需要消耗大量的资源.性能比非原子属性要差.<li>iOS开发的建议</li>
所有属性都声明为nonatomic,性能更高.
尽量避免多线程抢夺同一块资源.
尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力.
模拟原子属性
<li>定义属性</li>
/// 非原子属性
@property (nonatomic,strong) NSObject *obj1;
/// 原子属性:内部有"自旋锁"
@property (atomic,strong) NSObject *obj2;
/// 模拟原子属性
@property (atomic,strong) NSObject *obj3;
<li>重写非原子属性的setter和getter方法</li>
重写了原子属性的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<UITouch *> *)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);
}
测试结果
两种锁NSCondition ,NSLock
除了使用指令@synchronized , 还有两种锁,一种NSCondition ,一种是:NSLock。
#import <UIKit/UIKit.h>
@class ViewController;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
{
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等等,可以自己看官方文档学习
互斥锁和自旋锁对比
<li>共同点</li>
都能够保证同一时间,只有一条线程执行锁定范围的代码
<li>不同点</li>
互斥锁:如果发现有其他线程正在执行锁定的代码,线程会进入休眠状态,等待其他线程执行完毕,打开锁之后,线程会重新进入就绪状态.等待被CPU重新调度.
自旋锁:如果发现有其他线程正在执行锁定的代码,线程会以死循环的方式,一直等待锁定代码执行完成.
开发建议
所有属性都声明为nonatomic,原子属性和非原子属性的性能几乎一样.
尽量避免多线程抢夺同一块资源.
要实现线程安全,必须要用到锁.无论什么锁,都是有性能消耗的.
自旋锁更适合执行非常短的代码.死循环内部不适合写复杂的代码.
尽量将加锁,资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力.
为了流畅的用户体验,UIKit类库的线程都是不安全的,所以我们需要在主线程(UI线程)上更新UI.
所有包含NSMutable的类都是线程不安全的.在做多线程开发的时候,需要注意多线程同时操作可变对象的线程安全问题.