术语
任务:准备执行的操作,通常就是一个block块
队列:queue,存放任务,管理任务
进程——苹果电脑里的活动监视器App里详细罗列了操作系统此时此刻正在运行的所有程序,同时标明了每一个进行的程序的所有线程。
线程——将一个程序都转换成汇编的CPU命令时,由一堆不分叉的Cpu指令组成的路径(主线程命令和分线程命令)
多线程编程——由多条不分叉的CPU指令所组成的路径就是多线程编程。优点:提高资源利用率和程序执行效率,用户体验 缺点:1、上下文切换频率太高会影响性能 2、资源竞争—>上锁(类似单例)3、死锁 4、开辟的线程其实类似于创建一个对象,会消耗大量内存(内核数据结构(大约1KB)、栈空间(子线程512KB、主线程1MB,也可以使用-setStackSize:设置,但必须是4K的倍数,而且最小是16K),创建线程大约需要90毫秒的创建时间)5、程序设计更加复杂:比如线程之间的通信、多线程的数据共享
CPU包括——1、物理CPU(硬件)2、CPU核 (一个物理CPU可以虚拟出多个CPU核,一个CPU核就相当于一个CPU的功能,可以分身,一个CPU核一个时刻只能执行一个CPU命令)
并发——虽然同一时间,CPU只能处理1条线程,只有1条线程在执行,但是CPU可以快速地在多条线程之间调度切换,如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。当然如果线程过多,会消耗大量的CPU资源,同时每条线程被调度执行的频次会降低(线程的执行效率降低)
多核就快
首先谈一谈就是程序执行的本质,我们写的代码都首先是转化成一行行的汇编代码才能被计算机所识别。这一行行的汇编代码其实也叫做CPU指令。这里面就涉及到CPU了,CPU就是读取这些指令的工具。当然,有一种汇编代码也就是CPU指令的结构是从头到尾不间断的,也就是这些CPU指令一点也没有分叉,从头到尾都是直路,就像高速公路一样,只有直路,没有岔路。可是程序的魅力就在于,遇到分岔路时或更复杂的十字路口时应该怎么办?肯定不能一根筋呀!于是CPU厂商就想到了一个方法,就是分身术,当遇到分叉路的时候就像孙悟空一样,复制一个自己来走新出现的分岔路。可是问题来了,如果第二次又遇到分叉路怎么办呢?两种思路,第一种再复制一个自己。第二种就是用那个复制的自己轮流前进。第一种方法主要受到CPU本身性能的局限。有的CPU只能复制一个自己,算上本身总共才两个。也有的CPU可以复制三个自己,算上本身总共有4个。这种具体去跑马路的车就叫做CPU核,所以你会发现,CPU越强大,也就是能够复制的自己越多,能够同时走的分叉路就越多,自然而然,四核就是比双核快。第二种方法就是让CPU的寄存器不断受到考验,当CPU分配那个复制的自己在那一条道路上奔跑时,必须时时记录下来这个复制的自己已经在每一条分叉路上已经跑了多远,并需要准确无误的保存在寄存器中,然后当CPU再一次将那个复制的自己放在相应地分岔路上继续跑时,就会读取寄存器里面的数据来继续前进。
主线程刷UI
因为分线程不能刷新UI,但是有的时候明明看见刷新UI的代码写在了分线程里,但这并不代表这些代码会被分线程(复制的CPU核)执行,分线程主要是负责数据的下载。(两种情况:1、下载完成后主动回到主线程,告诉主线程自己已经完成下载,可以让主线程执行分线程没有资格执行的代码 2、需要主线程不定期去主动检查也就是说,尽管你把刷新UI的代码写在了分线程,但分线程只会执行其中的下载代码,根本不会执行刷新UI的代码。刷新UI的代码会等到主线程不定期的主动检查分线程时才会被执行。所以这样会出现一些延时,所以应该在下载完成后主动回到主线程)
时间片
如果同一个CPU核去管理多个线程时,就会涉及到时间分配的问题,如果管理的是两个线程,线程一有10个命令,线程二有20个命令,CPU核会将时间片分配给两个线程,一一去执行完两个线程里面的所有命令,当一个线程切换到另一个线程时,系统会将当前线程的信息保存到寄存器中,等下次切换过来到时候唤醒寄存器,读取数据,从上一次的命令接着处理。这就是上下文切换。上下文切换——时间片分配(随机分配CPU核给那一个分线程,同时保存每一分线程上一次执行到的位置到寄存器中,告诉CPU核现在该从第几个CPU指令开始执行)
iOS多线程NSThread
- 创建、启动线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
- 线程相关用法
// 获得当前线程
NSThread *current = [NSThread currentThread];
// 线程名字
- (void)setName:(NSString *)name;
- (NSString *)name;
// 获得主线程
+ (NSThread *)mainThread;
// 判断是否主线程
- (BOOL)isMainThread;
+ (BOOL)isMainThread;
// 创建线程后自动启动
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
// 隐式创建并启动线程
[self performSelectorInBackground:@selector(run) withObject:nil];
// 阻塞(暂停)线程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 强制停止线程
+ (void)exit;
GCD全称Grand Central Dispatch
纯C语言函数,为多核并行运算而生,自动利用更多的CPU内核(比如双核、四核),且自动管理线程的生命周期(创建线程、调度任务、销毁线程),只需告诉GCD想要执行的任务,根本无需写任何管理线程生命周期的代码。队列管理任务执行顺序,同步异步决定任务执行线程。
- GCD创建并发队列
// 创建并发队列(队列名称,队列类型)
dispatch_queue_t myQueue = dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
// 获得默认并发队列(队列优先级,缺省参数0)
dispatch_queue_t systemQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 任务加入并发队列
dispatch_async(queue, ^{
NSLog(@"1-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"2-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"3-----%@", [NSThread currentThread]);
});
- GCD创建串行队列
// 创建串行队列(队列名称,队列类型)
dispatch_queue_t myQueue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
// 直接获取主队列 = 特殊串行队列
dispatch_queue_t systemQueue = dispatch_get_main_queue();
// 任务加入串行队列
dispatch_sync(queue, ^{
NSLog(@"1-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"2-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"3-----%@", [NSThread currentThread]);
});
- GCD同步执行任务
// 继续当前线程(队列,任务)
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
- GCD异步执行任务
// 开启新线程(队列,任务)
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
- GCD线程通信
// 从子线程回到主线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行耗时的异步操作...
dispatch_async(dispatch_get_main_queue(), ^{
// 回到主线程,执行UI刷新操作
});
});
// 对比performSelector的线程通信
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
- GCD延时执行
// GCD延时
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 2秒后异步执行这里的代码...
});
// 对比performSelector延时
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
// 对比NSTimer延时
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:NO];
- GCD单例执行
// 保证某段代码在程序运行过程中只被执行1次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
});
- GCD创建定时器执行代码块
// 创建Timer
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
// 设置定时器的触发时间(1秒后)和时间间隔(每隔2秒)
dispatch_source_set_timer(self.timer, dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC), 2 * NSEC_PER_SEC, 0);
// 设置Block回调
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"Timer %@", [NSThread currentThread]);
});
// 开启定时器
dispatch_resume(self.timer);
// 取消置空定时器
dispatch_cancel(self.timer);
self.timer = nil;
- GCD实现任务依赖
// 队列组管理任务一和任务二
dispatch_group_t group = dispatch_group_create();
// 异步执行任务一
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
});
// 异步执行任务二
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
});
// 任务一和任务二都结束Block回调
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
});
- GCD分线程遍历
/**
* GCD快速异步遍历
*/
- (void)apply
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSString *from = @"初始路径";
NSString *to = @目标路径";
NSFileManager *mgr = [NSFileManager defaultManager];
NSArray *subpaths = [mgr subpathsAtPath:from];
dispatch_apply(subpaths.count, queue, ^(size_t index) {
NSString *subpath = subpaths[index];
NSString *fromFullpath = [from stringByAppendingPathComponent:subpath];
NSString *toFullpath = [to stringByAppendingPathComponent:subpath];
[mgr moveItemAtPath:fromFullpath toPath:toFullpath error:nil]; // 剪切
NSLog(@"%@---%@", [NSThread currentThread], subpath);
});
}
/**
* 对比传统异步遍历
*/
- (void)moveFile {
NSString *from = @"初始路径";
NSString *to = @目标路径";
NSFileManager *mgr = [NSFileManager defaultManager];
NSArray *subpaths = [mgr subpathsAtPath:from];
for (NSString *subpath in subpaths) {
NSString *fromFullpath = [from stringByAppendingPathComponent:subpath];
NSString *toFullpath = [to stringByAppendingPathComponent:subpath];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[mgr moveItemAtPath:fromFullpath toPath:toFullpath error:nil]; // 剪切
});
}
}
NSOperation&&NSOperationQueue
NSOperation是个并不具备封装操作能力的抽象类,必须使用它的子类来封装操作,NSOperation子类包括NSInvocationOperation、NSBlockOperation、自定义子类继承NSOperation。如果仅仅是实例化了一个操作对象,然后简简单单的执行这个操作的start方法,这样没有丝毫意义,还不如在.m文件里面写一个私有方法来得直接,可是一旦把实例化的NSOperation对象放到NSOperationQueue队列之中,添加到队列里面的操作对象会自动开始调用操作对象的start方法,开始异步在一条新的线程里面执行封装在操作对象里面的一系列代码,这就实现了分线程操作了,好神奇有木有!
- 创建NSInvocationOperation操作
// 创建NSInvocationOperation对象
- (id)initWithTarget:(id)target selector:(SEL)sel object:(id)arg;
// 默认同步,除非添加操作到队列
- (void)addOperation:(NSOperation *)op;
- 创建NSBlockOperation操作
// 创建NSBlockOperation
+ (id)blockOperationWithBlock:(void (^)(void))block;
// 添加更多操作(只要任务数>1,就会异步执行操作)
- (void)addExecutionBlock:(void (^)(void))block;
- 创建自定义NSOperation操作
重写父类NSOperation的main方法,在main方法里面写入我想要封装的执行的任务,但是有一点需要特别注意,就是自己写的这段代码很可能会是在子线程里面异步执行,既然是异步操作,自然无法访问主线程的自动释放池,对象的内存释放便是需要重点考虑的内容。经常的做法就是,在封装想要执行的代码之前,必须先判断一下这个操作对象是否已经被取消,因为一旦开始执行进入了子线程就无法在判断isCancelled这个属性了,所以必须在开始执行封装的代码之前,必须先判断这个操作对象是否已经被取消。如果判断已经取消,我需要做的就是一些内存的管理。
- 创建NSOperation操作依赖
// 一定先让操作A执行完毕,然后才能执行操作B
[operationB addDependency:operationA];
!!!一定注意不能相互依赖
- 监听NSOperation操作执行完毕
- (void (^)(void))completionBlock;
- (void)setCompletionBlock:(void (^)(void))block;
- 添加操作到NSOperationQueue对列
- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;
- 设置操作对列NSOperationQueue最大并发数
- (NSInteger)maxConcurrentOperationCount;
- (void)setMaxConcurrentOperationCount:(NSInteger)cnt;
- 对列NSOperationQueue的取消、暂停、恢复
// 取消队列操作
- (void)cancelAllOperations;
- (void)cancel;
// 暂停Or恢复队列
- (void)setSuspended:(BOOL)bool;
- (BOOL)isSuspended;
GCD和NSOperation的区别?
GCD无法控制线程的最大并发数,而NSOperation可以控制同时进行的线程个数!
开辟多线程?
- performSelector
作为NSObject
的类别,因此只要是控制器的对象就可以直接调用这个方法。而且不仅仅可以通过performSelectorInBackground
将耗时操作放进分线程里,更可以通过performSelectorOnMainThread
将分线程里的参数传递到主线程以便进一步操作,例如刷新UI
。
- NSThread线程类
thread
本身就是线的意思。通过NSThread
类实例化一个对象。然后直接通过初始化方法就可以像为UIButton
增添点击事件那样增添一个在分线程里进行的方法。而且可以传递给这个分线程方法一个无论什么对象类型的参数。但是特别注意:通过NSThread
类实例化的对象所初始化增添的分线程方法必须通过[对象 start]方法表示开始执行分线程方法。而且无论第三方工具如何丰富,本质上都是对NSThread
进行一系列操作。所以说在任何情况下都可以调用[NSThread currentThread]
这个类方法获取到当前的线程信息,有点像代码版的活动监视器哈!这是苹果提供的三种方法里面相对轻量级的,但需要管理线程的生命周期、同步、加锁的问题,这会导致一定得性能开销。
- GCD队列Grand Central Dispatch
伟大的中枢调度器,充分发挥CPU内核的性能,自动管理线程的生命周期,包括创建线程,调度任务和销毁线程。而且使用过程中只需添加任务即可,无需管理线程。
- NSOperation
一块资源可能会被多个线程共享,通常添加互斥锁@synchronized(self){}
来避免引发数据错乱和数据安全问题。但是添加互斥锁会影响手机的性能,所以尽量将加锁和资源抢夺业务的逻辑交给服务器端处理,线程不会孤立存在,子线程下载图片,在主线程刷新UI显示图片。使用@synchronized(self的锁对象){}
包起来的代码同一时刻只能被一个线程执行,而且加锁的时候尽量缩小范围,范围越大就越消耗性能。
串行队列Vs并行队列?
相同点:
-
创建方式都是
dispatch_queue_t queue
,只不过创建队列时的标志符后面的参数有了一些变化,主要有(NULL, DISPATCH_QUEUE_SERIAL,DISPATCH_QUEUE_CONCURRENT)
三种类型。前两种等价都可以表示串行队列。而且有一个细节就是标识符的格式是否有要求一时确定不了! -
往队列里添加任务的方式一模一样,都是
dispatch_async(queue, ^{})
这种格式。凡是创建的队列都是以分线程的形式存在。所以对于一个分线程队列来说就必须考虑如何返回到主线程。而且这也不是说纯粹意义上的将这个分线程注销,更多的是想将无法在分线程中执行的代码及时的传递到主线程以便进行下一步操作。 -
将分线程中下载完成的数据如何让以参数的形式传递到主线程呢?两种方法,方法1就是使用控制器的对象调用
performSelectorOnMainThread
方法。方法2就是通过dispatch_queue_t mainQueue = dispatch_get_main_queue()
获取到主队列,这也就代表了主线程。通过在主队列中添加任务也就等于是通过主线程来执行分线程无法执行的代码。而且方法2还有一个优点就是,无需像方法一那样过于纠结于到底应该将分线程的什么参数传递出去,因为方法二是直接建立在分线程的基础上,本质上只是起一个临时调用主线程的功能,因此方法二可以直接调用分线程里的任何参数变量。 -
如果不理解什么是串行队列,其实主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue()
就是一个典型的串行队列,无论你添加多少任务总是按照任务的添加顺序依次执行,没有例外。而且自始至终都是一个线程,而且这个线程就叫做主线程。
不同点:
-
串行队列无论添加多少个任务都只是创建了一个分线程,而且执行任务的顺序严格按照添加任务的先后顺序进行。而对于并行队列而言,往并行对列中每增添一个任务就创建了一个分线程。也就是说并行队列中有多少个任务就有多少个分线程,我们也都知道,线程其实本质上也是一个对象,是对象就必须开辟内存,因此虽然说创建很多的分线程有利于获得良好的用户体验,但这对于用户终端的内存是一个极大的考验。
-
并行队列的任务因为是一个任务对应一个分线程。所以可以认为并行队列里的任务是同时进行的。所以最终并行队列里的哪一个任务最先执行结束的关键还在于任务的具体内容。
-
对于并行队列而言有一种很特殊的需求就是:我想准确地知道并行队列里的最后一个任务是什么时候执行完毕,然后我再进行下一部操作,因为通常情况下是无法准确地获取到现在并行队列的任务到底执行到了一个什么阶段。根本无法像串行队列那样通过添加任务的先后顺序来推断出到底哪一个任务结束后标志着队列任务全部结束!解决方案就是给并行队列添加一个
group
。同样需要通过dispatch_queue_t queue = dispatch_queue_create("123", DISPATCH_QUEUE_CONCURRENT)
先创建一个并行队列,然后通过dispatch_group_t group = dispatch_group_create()
创建一个管理并行队列里所有任务的组。接着跟常规添加任务到并行队列不同的是:dispatch_group_async(group, queue, ^{});
当然从分线程返回到主线程的方法dispatch_async(dispatch_get_main_queue(), ^{});
并没有发生变化。把所有的任务添加到有管理组的并行队列中目的就是时刻监听并行队列里的任务的进行阶段,监听到并行队列里所有的任务全部结束时就会触发方法dispatch_group_notify(group,dispatch_get_main_queue(),^{});
需要注意的是,这个方法里面的代码全部是由主线程执行。因为并行队列里的所有任务所对应的分线程也已经结束。监听到任务结束自动将监听方法里面的代码交由主线程执行。这也就是说,其实管理并行队列里所有任务的组group
的监听方法其实本质上就是就是并行队列里所有任务都结束后返回到主线程以便进行下一步操作。 -
如果我们想要创建多个并行对列,那么又如何确定哪一个并行队列率先执行呢,答案是,根据并行队列的优先级来进行判断。创建有优先级的并行队列的方法为dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)。当然在这里将并行队列的优先级设置为了默认,也就相当于又增加了一种创建普通并行队列的方法。
-
如果我想队列里的任务在多少秒之后才开始执行,那么最简单的方法就是在具体的任务里添加睡眠延时
sleep(1)
,但是的但是这也仅仅适用于串行队列中,而且就算这样可以也会带来一个整体任务进行的延时。对于并行队列来说,只要添加完了任务就会全面开始,根本不会停下来,所以最好的方法就是通过dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 4ull*NSEC_PER_MSEC)
来设置过4秒后再通过dispatch_after(time, queue, ^{})
将任务添加到队列queue
中。这样就可以完美的实现至少在多少秒后再进行某一个任务。6、无论在串行队列还是在并行队列中,有时候我们想要某个任务只进行一次,就算这是一个死循环的队列,也要保证某个任务只被执行一次,方法就是先用static dispatch_once_t onceTaken
确定谓词onceTaken
,用来保证dispatch_once(&onceTaken,^{ })
方法里的代码也就是任务将从始至终只会被执行一次。
互斥锁
@synchronized(锁对象) { // 需要锁定的代码 }
static id _instance;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
@synchronized(self) {
if (_instance == nil) {
_instance = [super allocWithZone:zone];
}
}
return _instance;
}
+ (instancetype)sharedInstance
{
@synchronized(self) {
if (_instance == nil) {
_instance = [[self alloc] init];
}
}
return _instance;
}
- (id)copyWithZone:(NSZone *)zone
{
return _instance;
}
优点:能有效防止因多线程抢夺资源造成的数据安全问题
缺点:需要消耗大量的CPU资源
前提:多条线程抢夺同一块资源
线程同步:多条线程在同一条线上执行(按顺序地执行任务)
atomic:原子属性,为setter方法加锁(默认就是atomic),线程安全,需要消耗大量的资源
nonatomic:非原子属性,不会为setter方法加锁,非线程安全,适合内存小的移动设备,尽量避免多线程抢夺同一块资源,尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力
网友评论