十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
创建信号量的方式:
成都网站建设、网站制作的关注点不是能为您做些什么网站,而是怎么做网站,有没有做好网站,给创新互联建站一个展示的机会来证明自己,这并不会花费您太多时间,或许会给您带来新的灵感和惊喜。面向用户友好,注重用户体验,一切以用户为中心。
(1)dispatch_semaphore_creat SignalCount = dispatch_semaphore_creat(10).
这个地方后面的这个10,是一个整数,可以是1,2,3,。。。表示在信号等待的时候,下一次收到的的信号量,说白了,就是这个数字控制的最大并发数。
(2)dispatch_semaphore_signal( ),这是一句表示信号通知。表示在信号等待的时候,收到的下一个信号量。一般是一个“信号量对象”。
(3)dispatch_semaphore_wait(参数一,参数二 ),这一句表示信号等待。
一般参数一会放一个信号对象,就是我们建立的那个,如果这个对列的信号量小于0的时候,就会一直等待下去。
参数二的值一般是 DISPATCH_TIME_FOREVER 和 DISPATCH_TIME_NOW
下面我们写一段代码来说明一下。(注:当然是参考别人的)
这个地方,解释一下,新建一个信号量为10的对象,就是将队列的最大并发数控制在10。
第一次打印的结果是 0,1,2,3,4,5,6,7,8,9。十个数字。
最关键的是dispatch_semaphore_signal(semphore),如果打印完第一轮十个数字,不再进行信号通知的话,线程就永远阻塞下去咯。剩下的10~99就不会再打印下去。
(注:以上是看了一个大神地思路以后,自己摆弄了一下,然后给大家解释一下,感谢那位大神,有些公司去面试的时候,会问NSOperation这个类通过调用setMaxConcurrentOperationCount这个方法设置最大的并发数,多线程技术GCD可以吗?答案是可以的,还有之前看过另外一个大神的,通过信号量控制,将异步的线程变成同步线程的,有兴趣的同学可以去参阅以下)。
线程和进程在我们开发中,跟我们一直形影不离,那么什么是进程,什么是线程,它们又有什么关系,这篇文章将为您简单介绍。
线程概念
进程概念
地址空间:同⼀进程的线程共享本进程的地址空间( TLS是本地的线程栈存空间,线程的局部空间是某些操作系统为线程提供的私有空间,只具备有限的容量,并不属于线程,由操作系统单独安排的 ),⽽进程之间则是独⽴的地址空间。
资源拥有:同⼀进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独⽴的。
优点:
缺点:
时间⽚的概念:CPU在多个任务直接进⾏快速的切换,这个时间间隔就是时间⽚。
多线程同时执行
如果线程非常多
互斥锁⼩结
互斥锁参数
nonatomic⾮原⼦属性
atomic原⼦属性(线程安全),针对多线程设计的,默认值,保证同⼀时间只有⼀个线程能够写⼊(但是同⼀个时间多个线程都可以取值)
atomic本身就有⼀把锁(⾃旋锁)
单写多读:单个线程写⼊,多个线程可以读取
atomic:线程安全,需要消耗⼤量的资源
nonatomic:⾮线程安全,适合内存⼩的移动设备
iOS开发建议
所有属性都声明为nonatomic
尽量避免多线程抢夺同一块资源
尽量将加锁,资源抢夺的业务逻辑交给服务器处理,减少APP的压力
这篇文章简单介绍了线程与进程的概念,烦请大家不吝赐教。
在多线程开发中,我们常用到GCD,这里探讨一下GCD任务的取消:
1.在iOS 8以后,系统给我们提供了这样的取消函数 dispatch_block_cancel,不过这个也只能用于dispatch_block_create创建的dispatch_block_t,我们试验一下:
这时肯定是任务都会执行的
接下来,把注释的那一行 dispatch_block_cancel(block1);打开,看看效果:
我们发现block1确实被取消掉了。这是dispatch_block_cancel的用法。
2.很多时候,我们的场景不会去用dispatch_block_create创建dispatch_block_t,这个时候我们若想取消一个任务,可以考虑用一个条件来做,满足条件则执行此任务,不满足则不执行,举个例子:
效果如下:
写到这里,这儿其实还隐藏了一个知识点,就是block的变量捕获,有兴趣或是不理解的朋友可以研究一下。(如下,为何输出不是20而是10)
3.过渡到NSOperation
NSOperation是对GCD的封装,底层也是GCD。
NSOperation给我们封装了更多的api,这是我在Xcode中提出来的:
我们可以发现它有状态属性,有取消方法,也有添加依赖方法等...这里我们还是先说取消吧,下面来给大家写个demo:
这时输出是:
因为正在执行的任务,NSOperation也是不能取消的,所以也是需要将cancel在start前调用的(就如同满足一个条件是否需要cancel一样,也可以满足条件不调用start)
概念:队列只负责任务的调度,而不负责任务的执行,任务是在线程中执行的。(可以理解成任务是放在队列里面的,要被调度到线程中去执行)
特点:队列先进先出,排在前面的任务最先执行。
分类:队列分为串行、并行、主队列、全局队列。
任务的执行是在线程上去执行的。分为同步和异步。
所以就可以分成:串行队列同步执行、串行队列异步执行、并行队列同步执行、并行队列异步执行。
GCD实现原理:
GCD有一个底层线程池,这个池中存放的是一个个的线程。之所以称为“池”,是因为这个“池”中的线程是可以重用的,当一段时间后没有任务在这个线程上执行的话,这个线程就会被销毁。注意:开多少条线程是由底层线程池决定的(线程建议控制再3~5条),池是系统自动来维护,不需要我们程序员来维护。
我们只关心的是向队列中添加任务,队列调度即可。
定义:调用方法(viewDidLoad)的队列(主队列)恰好是同步操作(dispatch_sync)所针对的队列(dispatch_get_main_queue)。
示例1:
输出结果:
dispatch_sync 和 dispatch_async 区别:
dispatch_async(queue,block) async 异步队列,dispatch_async 函数会立即返回, block会在后台异步执行。
dispatch_sync(queue,block) sync 同步队列,dispatch_sync 函数不会立即返回,及阻塞当前线程,等待 block同步执行完成。
以上例子就会死锁,因为viewDidLoad的这个任务是被主队列调用的的,而dispatch_sync不会立即返回,而是先阻塞当前的主线程,直到这个block执行完毕,因为主线程被阻碍了,啥也干不了了(只有一个线程还被阻塞了,就会造成死锁),所以这个block就永远没有机会执行了,所以就会造成死锁。
示例2:
输出结果:
示例2就不会造成死锁,因为dispatch_async会立即返回,所以会先输出3,而异步会创建一个新的线程来执行block块,所以2最后输出。但是2和3的顺序不一定。
示例3:
输出结果:
示例3也不会造成死锁,因为dispatch_sync不会立即返回,而是先阻塞主线程,再将任务2加入到一个全局队列的一个线程上去执行,执行完之后返回到主队列,此时主线程不在阻塞,再继续执行任务3。
示例4:
输出结果:
因为dispatch_async不会等待,所以顺序是1-4-2-3-5或1-2-4-3-5,其中任务1和4是在主线程执行的,而2是在全局队列上被调用的,执行完2之后,会阻塞当前的线程(全局队列上的),紧接着会回到主队列上的主线程上执行任务3,任务3执行完之后,会继续执行5,此时全局队列上的线程也不堵塞了。
注意:线程同步阻塞后不一定能造成死锁,还要看看还有没有其他线程去执行那个block,如果能有,就能解锁阻塞的线程,继续执行任务。如果没有,那就是死锁了。
示例5:
输出结果:
最终结果还是会导致死锁,因为dispatch_queue_create创建队列的时候传入NULL默认是串行队列,所以执行任务2之后,会阻塞掉当前线程,直到任务3的block执行完成,又因为当前线程被阻塞掉了,block也无法执行,导致相互等待造成死锁
示例6:
输出结果:=5
因为self.num++操作是异步的,不一定能立马返回结果,所以在进入下次while循环的时候,self.num(主线程)可能还是0,所以循环肯定至少5次,最理想的情况下,5次全部都返回结果,而NSLog是会等待异步结果返回才会打印,所以输出结果=5
示例7:
输出结果为:1000
因为总共循环1000次,并不是每次结果都有返回,所以最终打印的self.num肯定小于1000
参考链接
大家都知道,在开发过程中应该尽可能减少用户等待时间,让程序尽可能快的完成运算。可是无论是哪种语言开发的程序最终往往转换成汇编语言进而解释成机器码来执行。但是机器码是按顺序执行的,一个复杂的多步操作只能一步步按顺序逐个执行。改变这种状况可以从两个角度出发:对于单核处理器,可以将多个步骤放到不同的线程,这样一来用户完成UI操作后其他后续任务在其他线程中,当CPU空闲时会继续执行,而此时对于用户而言可以继续进行其他操作;对于多核处理器,如果用户在UI线程中完成某个操作之后,其他后续操作在别的线程中继续执行,用户同样可以继续进行其他UI操作,与此同时前一个操作的后续任务可以分散到多个空闲CPU中继续执行(当然具体调度顺序要根据程序设计而定),及解决了线程阻塞又提高了运行效率。苹果从iPad2 开始使用双核A5处理器(iPhone中从iPhone 4S开始使用),A7中还加入了协处理器,如何充分发挥这些处理器的性能确实值得思考。今天将重点分析iOS多线程开发:
多线程
简介
iOS多线程
NSThread
解决线程阻塞问题
多线程并发
线程状态
扩展-NSObject分类扩展
NSOperation
NSInvocationOperation
NSBlockOperation
线程执行顺序
GCD
串行队列
并发队列
其他任务执行方法
线程同步
NSLock同步锁
@synchronized代码块
扩展--使用GCD解决资源抢占问题
扩展--控制线程通信
总结
目 录
多线程
简介
当用户播放音频、下载资源、进行图像处理时往往希望做这些事情的时候其他操作不会被中断或者希望这些操作过程中更加顺畅。在单线程中一个线程只能做一件事情,一件事情处理不完另一件事就不能开始,这样势必影响用户体验。早在单核处理器时期就有多线程,这个时候多线程更多的用于解决线程阻塞造成的用户等待(通常是操作完UI后用户不再干涉,其他线程在等待队列中,CPU一旦空闲就继续执行,不影响用户其他UI操作),其处理能力并没有明显的变化。如今无论是移动操作系统还是PC、服务器都是多核处理器,于是“并行运算”就更多的被提及。一件事情我们可以分成多个步骤,在没有顺序要求的情况下使用多线程既能解决线程阻塞又能充分利用多核处理器运行能力。
下图反映了一个包含8个操作的任务在一个有两核心的CPU中创建四个线程运行的情况。假设每个核心有两个线程,那么每个CPU中两个线程会交替执行,两个CPU之间的操作会并行运算。单就一个CPU而言两个线程可以解决线程阻塞造成的不流畅问题,其本身运行效率并没有提高,多CPU的并行运算才真正解决了运行效率问题,这也正是并发和并行的区别。当然,不管是多核还是单核开发人员不用过多的担心,因为任务具体分配给几个CPU运算是由系统调度的,开发人员不用过多关心系统有几个CPU。开发人员需要关心的是线程之间的依赖关系,因为有些操作必须在某个操作完成完才能执行,如果不能保证这个顺序势必会造成程序问题。
iOS多线程
在iOS中每个进程启动后都会建立一个主线程(UI线程),这个线程是其他线程的父线程。由于在iOS中除了主线程,其他子线程是独立于Cocoa Touch的,所以只有主线程可以更新UI界面(新版iOS中,使用其他线程更新UI可能也能成功,但是不推荐)。iOS中多线程使用并不复杂,关键是如何控制好各个线程的执行顺序、处理好资源竞争问题。常用的多线程开发有三种方式:
1.NSThread
2.NSOperation
3.GCD
三种方式是随着iOS的发展逐渐引入的,所以相比而言后者比前者更加简单易用,并且GCD也是目前苹果官方比较推荐的方式(它充分利用了多核处理器的运算性能)。做过.Net开发的朋友不难发现其实这三种开发方式 刚好对应.Net中的多线程、线程池和异步调用,因此在文章中也会对比讲解。
NSThread
NSThread是轻量级的多线程开发,使用起来也并不复杂,但是使用NSThread需要自己管理线程生命周期。可以使用对象方法+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument直接将操作添加到线程中并启动,也可以使用对象方法- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 创建一个线程对象,然后调用start方法启动线程。
解决线程阻塞问题
在资源下载过程中,由于网络原因有时候很难保证下载时间,如果不使用多线程可能用户完成一个下载操作需要长时间的等待,这个过程中无法进行其他操作。下面演示一个采用多线程下载图片的过程,在这个示例中点击按钮会启动一个线程去下载图片,下载完成后使用UIImageView将图片显示到界面中。可以看到用户点击完下载按钮后,不管图片是否下载完成都可以继续操作界面,不会造成阻塞。
//
// NSThread实现多线程
// MultiThread
//
// Created by Kenshin Cui on 14-3-22.
// Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//
#import "KCMainViewController.h"
@interface KCMainViewController (){
UIImageView *_imageView;
}
@end
@implementation KCMainViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}
#pragma mark 界面布局
-(void)layoutUI{
_imageView =[[UIImageView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame];
_imageView.contentMode=UIViewContentModeScaleAspectFit;
[self.view addSubview:_imageView];
UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
button.frame=CGRectMake(50, 500, 220, 25);
[button setTitle:@"加载图片" forState:UIControlStateNormal];
//添加方法
[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
#pragma mark 将图片显示到界面
-(void)updateImage:(NSData *)imageData{
UIImage *image=[UIImage imageWithData:imageData];
_imageView.image=image;
}
#pragma mark 请求图片数据
-(NSData *)requestData{
//对于多线程操作建议把线程操作放到@autoreleasepool中
@autoreleasepool {
NSURL *url=[NSURL URLWithString:@""];
NSData *data=[NSData dataWithContentsOfURL:url];
return data;
}
}
#pragma mark 加载图片
-(void)loadImage{
//请求数据
NSData *data= [self requestData];
/*将数据显示到UI控件,注意只能在主线程中更新UI,
另外performSelectorOnMainThread方法是NSObject的分类方法,每个NSObject对象都有此方法,
它调用的selector方法是当前调用控件的方法,例如使用UIImageView调用的时候selector就是UIImageView的方法
Object:代表调用方法的参数,不过只能传递一个参数(如果有多个参数请使用对象进行封装)
waitUntilDone:是否线程任务完成执行
*/
[self performSelectorOnMainThread:@selector(updateImage:) withObject:data waitUntilDone:YES];
}
#pragma mark 多线程下载图片
-(void)loadImageWithMultiThread{
//方法1:使用对象方法
//创建一个线程,第一个参数是请求的操作,第二个参数是操作方法的参数
// NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
// //启动一个线程,注意启动一个线程并非就一定立即执行,而是处于就绪状态,当系统调度时才真正执行
// [thread start];
//方法2:使用类方法
[NSThread detachNewThreadSelector:@selector(loadImage) toTarget:self withObject:nil];
}
@end
运行效果:
程序比较简单,但是需要注意执行步骤:当点击了“加载图片”按钮后启动一个新的线程,这个线程在演示中大概用了5s左右,在这5s内UI线程是不会阻塞的,用户可以进行其他操作,大约5s之后图片下载完成,此时调用UI线程将图片显示到界面中(这个过程瞬间完成)。另外前面也提到过,更新UI的时候使用UI线程,这里调用了NSObject的分类扩展方法,调用UI线程完成更新。
多个线程并发
上面这个演示并没有演示多个子线程操作之间的关系,现在不妨在界面中多加载几张图片,每个图片都来自远程请求。
大家应该注意到不管是使用+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument、- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 方法还是使用- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait方法都只能传一个参数,由于更新图片需要传递UIImageView的索引和图片数据,因此这里不妨定义一个类保存图片索引和图片数据以供后面使用。
KCImageData.h
//
// KCImageData.h
// MultiThread
//
// Created by Kenshin Cui on 14-3-22.
// Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//
#import Foundation/Foundation.h
@interface KCImageData : NSObject
#pragma mark 索引
@property (nonatomic,assign) int index;
#pragma mark 图片数据
@property (nonatomic,strong) NSData *data;
@end
接下来将创建多个UIImageView并创建多个线程用于往UIImageView中填充图片。
KCMainViewController.m
//
// NSThread实现多线程
// MultiThread
//
// Created by Kenshin Cui on 14-3-22.
// Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//
#import "KCMainViewController.h"
#import "KCImageData.h"
#define ROW_COUNT 5
#define COLUMN_COUNT 3
#define ROW_HEIGHT 100
#define ROW_WIDTH ROW_HEIGHT
#define CELL_SPACING 10
@interface KCMainViewController (){
NSMutableArray *_imageViews;
}
@end
@implementation KCMainViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}
#pragma mark 界面布局
-(void)layoutUI{
//创建多个图片控件用于显示图片
_imageViews=[NSMutableArray array];
for (int r=0; rROW_COUNT; r++) {
for (int c=0; cCOLUMN_COUNT; c++) {
UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)];
imageView.contentMode=UIViewContentModeScaleAspectFit;
// imageView.backgroundColor=[UIColor redColor];
[self.view addSubview:imageView];
[_imageViews addObject:imageView];
}
}
UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
button.frame=CGRectMake(50, 500, 220, 25);
[button setTitle:@"加载图片" forState:UIControlStateNormal];
//添加方法
[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
#pragma mark 将图片显示到界面
-(void)updateImage:(KCImageData *)imageData{
UIImage *image=[UIImage imageWithData:imageData.data];
UIImageView *imageView= _imageViews[imageData.index];
imageView.image=image;
}
#pragma mark 请求图片数据
-(NSData *)requestData:(int )index{
//对于多线程操作建议把线程操作放到@autoreleasepool中
@autoreleasepool {
NSURL *url=[NSURL URLWithString:@""];
NSData *data=[NSData dataWithContentsOfURL:url];
return data;
}
}
#pragma mark 加载图片
-(void)loadImage:(NSNumber *)index{
// NSLog(@"%i",i);
//currentThread方法可以取得当前操作线程
NSLog(@"current thread:%@",[NSThread currentThread]);
int i=[index integerValue];
// NSLog(@"%i",i);//未必按顺序输出
NSData *data= [self requestData:i];
KCImageData *imageData=[[KCImageData alloc]init];
imageData.index=i;
imageData.data=data;
[self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES];
}
#pragma mark 多线程下载图片
-(void)loadImageWithMultiThread{
//创建多个线程用于填充图片
for (int i=0; iROW_COUNT*COLUMN_COUNT; ++i) {
// [NSThread detachNewThreadSelector:@selector(loadImage:) toTarget:self withObject:[NSNumber numberWithInt:i]];
NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]];
thread.name=[NSString stringWithFormat:@"myThread%i",i];//设置线程名称
[thread start];
}
}
@end
线程的不安全是由于多线程访问和修改共享资源而引起的不可预测的结果。
ios多线程开发中为保证线程的安全常用到的几种锁: NSLock 、 dispatch_semaphore 、 NSCondition 、 NSRecursiveLock 、 @synchronized 。
WEAKSELF typeof(self) __weak weakSelf = self;
NSLock 是OC层封装底层线程操作来实现的一种锁,继承NSLocking协议。不能迭代加锁,如果发生两次lock,而未unlock过,则会产生死锁问题。
以车站购票为例,多个窗口同时售票(同步),每个窗口有人循环购票:
原子操作
原子操作是指不可打断的操作,也就是说线程在执行操作的过程中,不会被操作系统挂起,而是一定会执行完,
变量属性Property中的原子定义
一般我们定义一个变量@property (nonatomic ,strong)NSLock *lock;nonatomic:非原子性,不会为setter方法加锁,适合内存小的移动设备;atomic:原子性,默认为setter方法加锁(默认就是atomic),线程安全。
PS: 在iOS开发过程中,一般都将属性声明为nonatomic,尽量避免多线程抢夺同一资源,尽量将加锁等资源抢夺业务交给服务器。
NSCondition常用于生产者-消费者模式,它继承了NSLocking协议,同样有lock和unlock方法。条件变量有点像信号量,提供了线程阻塞和信号机制,因此可以用来阻塞某个线程,并等待数据就绪再唤醒程序。
信号量主要有3个函数,分别是:
注意: 正常的使用顺序是先降低然后提高,这两个函数通常都是成对出现。
本文主要参考了这篇文章(
),并对其中所能理解的部分进行一一验证,以前没怎么写过类似的,如果有什么做的不好的地方还请大家多多见谅!