iOS-多线程

一 进程和线程

进程

  • 进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元.
  • 进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,可以理解为手机上的一个app.
  • 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内,拥有独立运行的全部资源.

线程

  • 程序执行流的最小单元,线程是进程中的一个实体.
  • 一个进程要想执行任务,必须有一条线程.应用程序启动时,系统会默认开启一条线程,也就是主线程.

线程和进程的关系

  • 线程是进程的执行单元,进程所有的任务都在线程中执行的.
  • 线程是CPU分配资源和调度的最小单位.
  • 一个程序可以对应多个进程,一个进程中可以有多个线程,但至少要有一个主线程.
  • 同一个进程内的线程共享资源.

二 多进程和多线程

多进程

打开MAC的活动监视器,可以看到很多歌进程同事运行.

  • 进程是程序在计算机上的一次执行活动.当你运行一个程序,你就启动了一个进程.显然,程序 是死的(静态的),进程是活的(动态的).
  • 进程可以分为系统进程和用户进程.凡是用于完成操作系统的各种功能的进程就是系统进程,他们就是处于运行状态下的操作系统本身;所有由用户启动的进程都是用户进程.进程是应用系统进行资源分配的单位.
  • 在同一个时间内,同一个计算系统中如果允许两个或两个以上的进程处于运行状态,这便是多进程

多线程

同一时间,CPU只能处理一条线程.多线程并发执行,其实是CPU快速的在多个线程之间调度.如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象.如果线程非常多,CPU会在N个多线程之间调度,消耗大量的CPU资源,每条线程被执行的频次会被降低,造成线程的执行率下降.

多线程的优点

  • 能适当的提高程序的执行效率;
  • 能适当的提高资源利用率(CPU,内存利用率);

多线程的缺点

  • 开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果大量开启线程,会占用大量的内存空间,降低程序性能;
  • 线程越多,CPU在调度线程上的开销越大;
  • 程序设计更加复杂:比如线程之间的通信,多线程的数据共享;

三 任务/队列

任务

所执行的操作,也就是在线程中执行的那段代码.在GCD中是放在block中的.执行任务有两种方式:同步执行(sync)和异步执行(async)

同步(sync):同步添加任务到指定的队列中,在添加的任务执行结束执行之前会一直等待,知道队列里的任务完成之后再继续执行,即会柱塞线程.只能在当前线程中执行任务(不一定是主线程),不具备开辟新线程的能力.

异步(async):线程会立即返回,无需等待就会继续执行下面的任务,不阻塞当前线程.可以在新的线程中执行任务,具备开启新线程的能力.

队列

队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列.队列是一种特殊的线性表,采用FIFO(先进先出)的原则,即新任务总是被插入到队列的末位,而读取任务总是从队列的头部读取.每读取一个任务,则从队列释放一个任务.

在GCD中有两种队列:串行队列和并发队列.两者都符合FIFO(先进先出)的原则.两者主要区别是:执行顺序不同,以及开启线程数不同.

串行队列(Serial Dispatch Queue):同一时间内,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务.(只 开启一个线程,一个任务执行完毕后,再执行下一个任务).主队列是主线程上的一个串行队列,是 系统自动为我们创建的

并发队列(Concurrent Dispatch Queue):同时允许多个任务并发执行.(可以开启多个线程,并且同时执行任务).并发队列的并发功能只有在异步(dispatch_async)函数下才有效.

四 iOS中的多线程

iOS中的多线程主要有三种:NSThread,NSOperationQueue,GCD

NSThread:轻量级别的多线程

是我们手动开辟的子线程,如果使用的是初始化方式需要我们自己启动,如果使用的构造器方式它会自动启动.只要我们手动开辟线程,都需要我们自己管理该线程,不只是启动,还有该线程使用完毕后的资源回收.

 NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(testThread:) object:@"我是参数"];
    //使用初始化方法时需要start来启动
    [thread start];
    //开辟子线程的名字
    thread.name = @"NSThread线程";
    //线程的权限,范围值0-1,权限越高,先执行的概率越高,由于是概率,所以并不能很准确的实现我们想要的执行
    thread.threadPriority = 1;
    //取消当前已启动的线程
    [thread cancel];

performSelector...只要是 NSObject 的子类或者对象都可以通过调用方法进入子线程和主线程,其实这些 方法所开辟的子线程也是 NSThread 的另一种体现方式,是在NSObject的分类中对NSThread的封装.在编译阶段并不会去检查方法是否有效存在,如果不存在只会给出警告.

 //在当前线程延迟1s执行
    [self performSelector:@selector(performSelectorTest:) withObject:@"performSelectorTestafterDelay1s" afterDelay:1.f];
    //回到主线程执行.waitUntilDone是否将该回调方法执行完在执行后面的代码
    [self performSelectorOnMainThread:@selector(performSelectorTest:) withObject:@"performSelectorOnMainThread" waitUntilDone:YES];
    //开辟子线程
    [self performSelectorInBackground:@selector(performSelectorTest:) withObject:@"performSelectorInBackground"];
    //在指定线程上执行
    [self performSelector:@selector(performSelectorTest:) onThread:[NSThread currentThread] withObject:@"performSelectorOnThread" waitUntilDone:YES];

需要注意的是:如果是带 afterDelay 的延时函数,会在内部创建一个 NSTimer,然后添加到当前线程的 Runloop 中.也就是如果当前线程没有开启 runloop,该方法会失效.在子线程中,需要启动 runloop(注意调用顺序)

NSOperationQueue

operation 是苹果基于GCD封装的,是面向对象。一般 operation 和 operation queue配合使用,这样可以很方便的在异步执行任务,但是这样也不是必须的。operation 内部有一个 start 方法可调用,但是无法保证在异步执行。我们通常使用的NSInvocationOperation和NSBlockOperation都是NSOperation的子类。当然,我们也可以自己创建NSOperation的子类。

NSInvocationOperation
NSOperationQueue *queue = [NSOperationQueue new];
NSInvocationOperation *invOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationTest:) object:@"参数"];
[queue addOperation:invOp];

- (void)operationTest:(NSString *)parameter
{
    NSLog(@"%@ - %@", [NSThread currentThread], parameter);
}

打印结果可以看到我们在异步线程执行了这段方法。如果我们直接使用 operation 的 start 方法而不是加入 queue ,那打印结果就只会在当前线程打印。

NSBlockOperation
NSOperationQueue *queue = [NSOperationQueue new];
NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%@", [NSThread currentThread]);
}];
[queue addOperation:blockOp];

除了上述使用 NSOperationQueue 添加 NSBlockOperation 实例外, NSBlockOperation 实例还有 addExecutionBlock: 方法,可以方便的添加多个 block

NSOperationQueue *queue = [NSOperationQueue new];

NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"1- %@", [NSThread currentThread]);
}];
[blockOp addExecutionBlock:^{
    NSLog(@"2- %@", [NSThread currentThread]);
}];
[blockOp addExecutionBlock:^{
    NSLog(@"3 - %@", [NSThread currentThread]);
}];

//    [blockOp start];
[queue addOperation:blockOp];
自定义NSOperation
@synthesize finished = _finished;
@synthesize executing = _executing;

- (BOOL)isAsynchronous
{
    return YES;
}

- (void)start
{
    if (self.isCancelled) {
        [self willChangeValueForKey:@"isFinished"];
        _finished = YES;
        [self didChangeValueForKey:@"isFinished"];
        return;
    }
    
    [self willChangeValueForKey:@"isExecuting"];
    
    [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
    _executing = YES;
    
    [self didChangeValueForKey:@"isExecuting"];
}

- (void)main
{
    for (NSInteger i = 0; i < 9999; i++) {
        if (self.isCancelled) {
            break;
        }
        
        NSLog(@"thread - %@, i is %@", [NSThread currentThread], @(i));
    }
    
    [self willChangeValueForKey:@"isFinished"];
    [self willChangeValueForKey:@"isExecuting"];
    
    _executing = NO;
    _finished  = YES;
    
    [self didChangeValueForKey:@"isFinished"];
    [self didChangeValueForKey:@"isExecuting"];
}

首先,NSOperation 类中含有executing、finished属性,并且是只读的。如果想修改这两个属性的值,我们可以使用 @synthesize 关键字手动合成两个实例变量 _executing 和 _finished 。并且通过 willChangeValueForKey: 和 didChangeValueForKey: 通过 KVO 通知它们的值修改了。

那怎么实现并发了:
1.重写 - (BOOL)isAsynchronous 方法来告诉别人方法是否是并发。
2.一般我们会重写 start 方法来进行一些初始化操作。比如,如果判断线程被取消或者添加一些条件判断。
3.重写 main 方法做复杂逻辑操作。

GCD

GCD---队列

GCD共有三种队列类型:
main queue:通过dispatch_get_main_queue()获得,这是一个主线程相关的串行队列.
global queue:全局队列是并发队列,由整个进程共享.存在高中低三种优先级的全局队列.调用dispatch_get_global_queue()并传入优先级来访问.
自定义队列:通过dispatch_queue_create()创建队列.

GCD任务执行顺序
    //创建一个串行队列
    dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    
    NSLog(@"1");
    
    //异步任务插入串行队列
    dispatch_async(serialQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"2");
    });
    
    NSLog(@"3");
    
    //同步任务插入串行队列
    dispatch_sync(serialQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"4");
    });
    
    NSLog(@"5");
    
    //创建一个并发队列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

    //异步任务插入并发队列
    dispatch_async(concurrentQueue, ^{
       [NSThread sleepForTimeInterval:2];
        NSLog(@"6");
    });
    
    NSLog(@"7");
    
    //同步任务插入并发队列
    dispatch_sync(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"8");
    });
    
    NSLog(@"9");
    
    //获取主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    //异步任务插入主队列
    dispatch_async(mainQueue, ^{
       [NSThread sleepForTimeInterval:2];
        NSLog(@"10");
    });
    
    NSLog(@"11");
    
    //同步任务插入主队列
    dispatch_sync(mainQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"12");
    });
    
    NSLog(@"13");

打印顺序是1,3,2,4,5,7,8,9,11 (6随机出现在7之后或者没有)
原因是:首先打印 1,接下来将任务2添加至串行队列上,由于任务2是异步,不会阻塞线程,继续向下执行,打印3,然后是任务4,将任务4添加至串行队列上,因为任务4和任务2在同一个串行队列上,任务4必须等任务2执行后才能执行,又因为任务4是同步任务,会阻塞线程,只有执行完4后才能继续向下执行打印5.之后创建一个并发队列并将异步任务6添加到并发队列,因为6是异步任务不会阻塞线程,继续向下执行,打印7.接下来将同步任务8添加到并发队列,因为是并发队列8不需要等6完成,又因为8是同步任务,会阻塞线程,所以任务8执行完后才能执行任务9.向下继续执行,获取主队列,前面提到过主队列是一个主线程相关的串行队列,所以把异步任务10添加到主队列不会阻塞主线程,接下来会打印11.当把同步任务12添加到主线程时,会发生死锁(关于死锁,下面篇幅有讲解),所以12,13不会被执行,异步任务10也因为死锁的原因斌没有执行完.任务6会因为计算时间不稳定,随机出现在7之后,或者没有被打印.

总结 串行队列 并发队列 主队列
同步任务(sync) 不开辟新线程,阻塞当前线程 不开辟新线程,阻塞当前线程 死锁
异步任务(async) 开辟新线程,不阻塞当前线程,异步任务串行执行 开辟新线程,不阻塞当前线程,异步任务并发执行 主线程运行,不阻塞当前线程,异步任务串行执行

CGD中的死锁

-(void)mainQueueLock{
//获取主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    //异步任务插入主队列
    dispatch_async(mainQueue, ^{
       [NSThread sleepForTimeInterval:2];
        NSLog(@"10");
    });
    
    NSLog(@"11");
    
    //同步任务插入主队列
    dispatch_sync(mainQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"12");
    });
    
    NSLog(@"13");
}

上面例子中,会在同步任务12添加到主队列时发生死锁.我们知道主队列是一个主线程相关的串行队列,当程序执行到mainQueueLock方法时,已经在主线程中有一个同步任务mainQueueLock,而在执行mainQueueLock时又向主队列中添加了同步任务12.mainQueueLock是同步任务,会阻塞线程,只有mainQueueLock执行完成后才能执行任务12,而mainQueueLock要执行完又必须得执行完任务12,这样两个任务就会因为相互等待而导致死锁.
同样,下面的代码也会造成死锁.

    dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

    dispatch_async(serialQueue, ^{
        dispatch_sync(serialQueue, ^{
                   NSLog(@"0");
        });
    });

外面的函数无论是同步还是异步都会造成死锁
这是因为里面的任务和外面的任务都太同一个串行队列里,又是同步,这和上面的主队列同步是一样的.
解决方案,试讲里面的同步改为异步或者将serialQueue换成其他串行队列或者并行队列.

GCD中的栅栏函数

当任务需要异步进行,但这些任务需要分成两组,第一组完成才能进行第二组操作.这时候就要用到GCD的栅栏方法dispatch_barrier_async和dispatch_barrier_sync.

    //创建串行队列
   dispatch_queue_t quene = dispatch_queue_create("yc", DISPATCH_QUEUE_SERIAL);
    NSLog(@"当前线程%@",[NSThread currentThread]);
    //串行队列添加异步操作
    dispatch_async(quene, ^{
        NSLog(@"1号我在哪里啊-----%@",[NSThread currentThread]);
    });
    //设置同步栅栏
    dispatch_barrier_async(quene, ^{
        NSLog(@"异步栅栏操作线程------%@",[NSThread currentThread]);
        sleep(5);
        NSLog(@"异步栅栏休息5秒");
    });
    //串行队列添加同步操作
    dispatch_sync(quene, ^{
        
        NSLog(@"2号我在哪里啊-----%@",[NSThread currentThread]);
    });
    //设置异步栅栏
    dispatch_barrier_sync(quene, ^{
           NSLog(@"同步栅栏操作线程------%@",[NSThread currentThread]);
           sleep(5);
           NSLog(@"同步栅栏休息五秒");
       });
    
    NSLog(@"结束");

打印结果是

14:16:25 当前线程<NSThread: 0x600002ec4b40>{number = 1, name = main}
14:16:25 1号我在哪里啊-----<NSThread: 0x600002e94bc0>{number = 4, name = (null)}
14:16:25 异步栅栏操作线程------<NSThread: 0x600002e8ffc0>{number = 3, name = (null)}
14:16:30 异步栅栏休息5秒
14:16:30 2号我在哪里啊-----<NSThread: 0x600002ec4b40>{number = 1, name = main}
14:16:30 同步栅栏操作线程------<NSThread: 0x600002ec4b40>{number = 1, name = main}
14:16:35 同步栅栏休息五秒
14:16:35 结束

我们再试下并发队列

    //创建并发队列
   dispatch_queue_t quene = dispatch_queue_create("yc", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"当前线程%@",[NSThread currentThread]);
    //串行队列添加异步操作
    dispatch_async(quene, ^{
        NSLog(@"1号我在哪里啊-----%@",[NSThread currentThread]);
    });
    //设置同步栅栏
    dispatch_barrier_async(quene, ^{
        NSLog(@"异步栅栏操作线程------%@",[NSThread currentThread]);
        sleep(5);
        NSLog(@"异步栅栏休息5秒");
    });
    //串行队列添加同步操作
    dispatch_sync(quene, ^{
        
        NSLog(@"2号我在哪里啊-----%@",[NSThread currentThread]);
    });
    //设置异步栅栏
    dispatch_barrier_sync(quene, ^{
           NSLog(@"同步栅栏操作线程------%@",[NSThread currentThread]);
           sleep(5);
           NSLog(@"同步栅栏休息五秒");
       });
    
    NSLog(@"结束");

结果

14:28:33 当前线程<NSThread: 0x600002fed440>{number = 1, name = main}
14:28:33 1号我在哪里啊-----<NSThread: 0x600002fa26c0>{number = 6, name = (null)}
14:28:33 异步栅栏操作线程------<NSThread: 0x600002fa26c0>{number = 6, name = (null)}
14:28:38 异步栅栏休息5秒
14:28:38 2号我在哪里啊-----<NSThread: 0x600002fed440>{number = 1, name = main}
14:28:38 同步栅栏操作线程------<NSThread: 0x600002fed440>{number = 1, name = main}
14:28:43 同步栅栏休息五秒
14:28:43 结束

我们对比两次的数据可以看到.
1.异步栅栏会开辟新的线程;
2.同步栅栏在当前线程上;
3.无论同步栅栏还是异步栅栏,都是需要前面一组完成后,在执行栅栏方法,之后再执行下一组操作.
从上面看,两种栅栏除了线程不一样其他基本相同.事实上,在基本需求上两者确实差不多.但栅栏方法内部执行异步方法时就有所区别.

    dispatch_queue_t quene = dispatch_queue_create("yc", DISPATCH_QUEUE_SERIAL);
    NSLog(@"当前线程%@",[NSThread currentThread]);
    
    dispatch_async(quene, ^{
        NSLog(@"1号我在哪里啊-----%@",[NSThread currentThread]);
    });
    
    dispatch_barrier_sync(quene, ^{
        NSLog(@"同步栅栏操作线程1------%@",[NSThread currentThread]);
        dispatch_async(quene, ^{
            NSLog(@"同步栅栏操作线程2------%@",[NSThread currentThread]);
            sleep(5);
            NSLog(@"同步栅栏休息5秒");
        });
    });
    
    NSLog(@"同步栅栏-异步操作--结束");
  
    dispatch_barrier_async(quene, ^{
        NSLog(@"异步栅栏操作线程1------%@",[NSThread currentThread]);
        dispatch_async(quene, ^{
            NSLog(@"异步栅栏操作线程2------%@",[NSThread currentThread]);
            sleep(5);
            NSLog(@"异步栅栏休息5秒");
        });
    });

    NSLog(@"异步栅栏-异步操作--结束");
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345