多线程使用要注意哪些事项

使用多线程是每个程序员必须要掌握的。然而使用多线程的时候,如果不加注意就会产生很多比较难排查的bug。所以要对多线程有深入的理解才行。比如可变数组和可变字典是线程安全的吗?本身就是异步执行的任务,如何等待真正的结果返回之后才继续后面的事情?在线程中睡眠,睡醒之后当前类已经释放,self会为nil吗?等等,在多线程的实际使用过程中会有很多出现,因此需要把基础打牢,也要有更深的理解。

首先从最基本的定义说起

什么是进程?

进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。

上面是百科的部分定义,简单来说,每个应用启动之后,都对应着一个进程。打开活动监视器即可看到,第一列就是进程名称,每一个程序对应一个进程,每个进程都有一个唯一标示PID,即进程ID。

进程概念主要有两点:

  1. 进程是一个实体。每个进程都有自己的地址空间,一般情况下包括 文本区域text region、数据区域data region 和堆栈stack region。
  2. 进程是一个执行中的程序。程序是一个没有生命的实体,只有程序执行的时候,它才能成为一个活动的实体。我们称它为进程。

除去进程的新建和终止,运行中的进程具有三种基本状态:

运行中的进程状态
  • 就绪:进程已经获得除处理器外的所需资源,等待分配处理器资源。只要分配了处理器进程就可以执行。
  • 运行:进行占用处理器资源,出于此状态的运行数小于等于处理器数
  • 阻塞:由于进程等待某种条件,比如I/O操作或者进程同步,在条件满足之前无法继续执行

什么是线程?

线程是程序执行的最小单元。一个标准的线程由线程ID,当前指令指针,寄存器集合和堆栈组成。线程是进程中的一个实体。线程也具有就绪,阻塞和运行三种基本状态。

通常一个进行中可以包含若干个线程,它们可以利用进程所拥有的全部资源。进程是分配资源的基本单位。而线程则是独立运行和调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。

线程是进程的基本执行单元,进程的所有任务都是在线程中执行的。

多线程的实现原理

先说下任务执行有两种方式,串行和并行。

串行:任务一个一个执行,所需时间是所有任务执行完成之和。

并行:任务并发执行,所需时间是耗时最久的任务完成时间。

任务的串行于行与线程并没有必然的联系。串行并非就只有一个线程,也可以有多个线程。并行必然有多个线程。

对于单核操作系统,同一时间只有一个线程在执行。每个线程都分配有时间片进行执行,然后切换到其他线程。从宏观来看是并行的,从微观上来看,是串行的。

对于多核操作系统,就真正实现了并行执行任务。在同一时间可以有多个线程在执行任务。

多线程的场景使用场景:

在iOS系统中,UIKit中的所有操作都是在主线程中执行的,包括用户的触摸事件等。如果在主线程执行耗时操作就会造成卡顿等现象,影响用户体验。常见的耗时操作有:

  • 网络请求

  • 图片加载

  • 文件处理

  • 数据存储

  • 多任务

常见实现方式

  • pThread --c 语言
  • NSThread -- oc对象
  • GCD -- c语言
  • NSOpreation -- oc对象

pThread 用的是一套c语言库,在iOS开发中使用的不多。

使用时需要先导入头文件#import <pthread.h>

使用时创建线程,并指定执行的方法即可

- (IBAction)pThreadClicked:(UIButton *)sender {
    NSLog(@"主线程事件");
    pthread_t pthread;
    pthread_create(&pthread, NULL, run, NULL);
}

void *run(){
    NSLog(@"run方法执行");
    sleep(1);
    NSLog(@"执行结束");
    return NULL;
}
// 打印结果4540是进程id,后面的是线程id,打印结果可以看出run在子线程中执行。
2019-01-03 17:22:26.260893+0800 ThreadTest[4540:1088694] 主线程事件
2019-01-03 17:22:26.262011+0800 ThreadTest[4540:1089164] run方法执行
2019-01-03 17:22:27.263254+0800 ThreadTest[4540:1089164] 执行结束
    

因为phread不常用,所以也不做多做介绍,使用的时候时候看一下官方的api就可以了。

NSThread 的三种创建方式

// 1. 对象方式,可以获取到线程对象,需要手动执行start方法,当然也方便设置其他属性,比如优先级,比如线程名字等。

- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
- (IBAction)NSTreadClick:(UIButton *)sender {
    NSLog(@"主线程");
    NSThread *thread = [[NSThread alloc]initWithBlock:^{
        sleep(1);
        NSLog(@"子线程");
    }];
    thread.name = @"thread1";
    thread.threadPriority = 1.0;
    [thread start];
}
// 打印结果
2019-01-03 17:44:14.421668+0800 ThreadTest[4668:1122856] 主线程
2019-01-03 17:44:15.423002+0800 ThreadTest[4668:1123018] 子线程

// 2. 类方法 不能直接获取线程对象 不过可以在线程执行过程中使用类方法currentThread获取当前线程

+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

// 3. NSObject的分类方法,不能直接获取线程对象

- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

GCD

以下面的代码为例

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       ... 
    });
// async 异步
// get_global_queue 并发
// ^{       ...     } 执行的任务

GCD的使用需要告诉gcd三个东西

  1. 同步 还是 异步:同步会阻塞当前线程,异步不会阻塞。
  2. 在哪个队列:队列分为串行和并行,即需要一个一个执行还是需要并发执行。
  3. 任务是什么: block中的东西即是要执行的任务。

主队列:dispatch_get_main_queue() 串行队列

全局队列: dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>) 并发队列

举个异步执行任务回到主线程刷新的例子:

- (IBAction)gcdClick:(UIButton *)sender {
    NSLog(@"用户点击事件在主线程");
    // 在自线程执行任务
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"start...");
        [NSThread sleepForTimeInterval:3];
        NSLog(@"end...");
        // 回到主线程 刷新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"回到主线程刷新UI");
        });
    });
}
// 打印 可以通过线程id区分是不是主线程,也可以打印当前线程
2019-01-03 19:14:14.841561+0800 ThreadTest[4789:1195593] 用户点击事件在主线程
2019-01-03 19:14:14.841752+0800 ThreadTest[4789:1195941] start...
2019-01-03 19:14:17.845246+0800 ThreadTest[4789:1195941] end...
2019-01-03 19:14:17.845676+0800 ThreadTest[4789:1195593] 回到主线程刷新UI

获取全局队列时,第一个参数是设置优先级的,第二个是预留参数,暂时没有用。优先级越高,先执行的概率越大。

进行串行执行,串行还是并发和同步异步没有关系,也就是和线程没有必然的联系,我们举个例子来说明这一点,同步因为没有开启线程必然是串行的,但异步如果指定了串行队列,也会一个一个执行。

同步的串行:

    // 同步 -- 串行
    NSLog(@"当前主线程");
    dispatch_queue_t serial = dispatch_queue_create("com.test.gcd", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(serial, ^{
        NSLog(@"sync-task1");
    });
    dispatch_sync(serial, ^{
        NSLog(@"sync-task2");
    });
    dispatch_sync(serial, ^{
        NSLog(@"sync-task3");
    });

异步的串行:

    //异步 -- 串行
    dispatch_async(serial, ^{
        NSLog(@"async-task1");
    });
    dispatch_async(serial, ^{
        NSLog(@"async-task2");
    });
    dispatch_async(serial, ^{
        NSLog(@"async-task3");
    });

上述两者的打印结果如下:

2019-01-03 19:28:09.275753+0800 ThreadTest[4836:1220108] 当前主线程
2019-01-03 19:28:09.276001+0800 ThreadTest[4836:1220108] sync-task1
2019-01-03 19:28:09.276151+0800 ThreadTest[4836:1220108] sync-task2
2019-01-03 19:28:09.276287+0800 ThreadTest[4836:1220108] sync-task3
2019-01-03 19:28:09.276452+0800 ThreadTest[4836:1220151] async-task1
2019-01-03 19:28:09.276656+0800 ThreadTest[4836:1220151] async-task2
2019-01-03 19:28:09.277657+0800 ThreadTest[4836:1220151] async-task3

上述结果说明:

  1. 同步时,没有开启线程。
  2. 异步时,开启了线程,因为是串行,所以只开了一个线程。
  3. 串行是为了保证任务的顺序执行,并行是为了保证任务的并发执行,串行没有创建新线程,并行会根据需要至少创建一个线程。

关于同步&异步。串行&并行 可以列个象限图

有四种组合:

  • 同步-串行:同步会阻塞当前线程,因此不会创建线程,也可以保证任务同步执行

  • 同步-并发:同步会阻塞当前线程,所以不会创建线程,所以无法实现并发,实际上还是串行。

  • 异步-串行:异步不会阻塞当前线程,会创建新线程,为保证串行,一般创建一个线程就够了。

  • 异步-并发:异步不会阻塞当前线程,会创建新线程,为保证并发,一般会创建多个线程。

这里重点说明一下同步-并发 其实因为同步会阻塞线程所以不能并发

    // 同步-并发,实际无法实现并发,依然是串行执行
    NSLog(@"当前主线程");
    dispatch_queue_t async = dispatch_queue_create("com.test.async", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 8; i++) {
        dispatch_sync(async, ^{
            NSLog(@"sync_concurrent_task%d",i);
        });
    }
// 打印结果 并没有创建新的线程,所以并不是并发执行的。
2019-01-03 19:44:25.405840+0800 ThreadTest[4888:1241863] 当前主线程
2019-01-03 19:44:25.406140+0800 ThreadTest[4888:1241863] sync_concurrent_task0
2019-01-03 19:44:25.406288+0800 ThreadTest[4888:1241863] sync_concurrent_task1
2019-01-03 19:44:25.406429+0800 ThreadTest[4888:1241863] sync_concurrent_task2
2019-01-03 19:44:25.406534+0800 ThreadTest[4888:1241863] sync_concurrent_task3
2019-01-03 19:44:25.406634+0800 ThreadTest[4888:1241863] sync_concurrent_task4
2019-01-03 19:44:25.406756+0800 ThreadTest[4888:1241863] sync_concurrent_task5
2019-01-03 19:44:25.407134+0800 ThreadTest[4888:1241863] sync_concurrent_task6
2019-01-03 19:44:25.407561+0800 ThreadTest[4888:1241863] sync_concurrent_task7

// 任务组 — 用来一组任务结束之后,再执行其他操作,组任务结束之后会调用notify方法

    NSLog(@"当前主线程");
dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t group_queue = dispatch_queue_create("com.test.group", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_async(group, group_queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"group task 1");
    });
    dispatch_group_async(group, group_queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"group task 2");
    });
    dispatch_group_async(group, group_queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"group task 3");
    });
    
    dispatch_group_notify(group, group_queue, ^{
        NSLog(@"all task done");
    });
// 打印 启用了三个线程 任务等待完成之后执行。
2019-01-03 19:55:20.669903+0800 ThreadTest[4924:1260901] 当前主线程
2019-01-03 19:55:22.674533+0800 ThreadTest[4924:1260946] group task 2
2019-01-03 19:55:22.674533+0800 ThreadTest[4924:1260947] group task 1
2019-01-03 19:55:22.674533+0800 ThreadTest[4924:1260945] group task 3
2019-01-03 19:55:22.674753+0800 ThreadTest[4924:1260946] all task done

需要注意的是:以上每一个任务本身就是同步的,如果任务本身就是异步的,每个任务很快就会执行,可能获取不到我们想要的结果。比如我们模拟一个异步请求。

    // 任务组
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t group_queue = dispatch_queue_create("com.test.group", DISPATCH_QUEUE_CONCURRENT);

    dispatch_group_async(group, group_queue, ^{
        [self requestOneInfo:^{
            NSLog(@"one info done");
        }];
    });
    
    dispatch_group_async(group, group_queue, ^{
        [self requestOtherInfo:^{
            NSLog(@"other info done");
        }];
    });
    
    dispatch_group_notify(group, group_queue, ^{
        NSLog(@"all task done");
    });
// 打印结果 很明显不是我们想要的
2019-01-03 20:07:51.231103+0800 ThreadTest[4950:1281306] 当前主线程
2019-01-03 20:07:51.231406+0800 ThreadTest[4950:1281362] get OneInfo start
2019-01-03 20:07:51.231426+0800 ThreadTest[4950:1281360] all task done
2019-01-03 20:07:51.231414+0800 ThreadTest[4950:1281359] get OtherInfo start
2019-01-03 20:07:53.234123+0800 ThreadTest[4950:1281359] get OtherInfo end
2019-01-03 20:07:53.234130+0800 ThreadTest[4950:1281362] get OneInfo end
2019-01-03 20:07:53.234489+0800 ThreadTest[4950:1281359] other info done
2019-01-03 20:07:53.234491+0800 ThreadTest[4950:1281362] one info done

以上结果很明显不是我们想要的,就是因为任务本身是异步执行的,任务组的任务很快就结束了,真正的任务并没有结束。这个时候,我们需要使用 enter 和 leave , enter 和 leave要成对出现。

我们修改一下代码,让任务组可以执行预期的异步操作

    // 任务组
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t group_queue = dispatch_queue_create("com.test.group", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_enter(group);
    dispatch_group_async(group, group_queue, ^{
        [self requestOneInfo:^{
            NSLog(@"one info done");
            dispatch_group_leave(group);
        }];
    });
    
    dispatch_group_enter(group);
    dispatch_group_async(group, group_queue, ^{
        [self requestOtherInfo:^{
            NSLog(@"other info done");
            dispatch_group_leave(group);
        }];
    });
    
    dispatch_group_notify(group, group_queue, ^{
        NSLog(@"all task done");
    });
// 打印,符合预期
2019-01-03 20:13:05.609618+0800 ThreadTest[4963:1290616] 当前主线程
2019-01-03 20:13:05.609897+0800 ThreadTest[4963:1290656] get OneInfo start
2019-01-03 20:13:05.609923+0800 ThreadTest[4963:1290654] get OtherInfo start
2019-01-03 20:13:07.615273+0800 ThreadTest[4963:1290656] get OneInfo end
2019-01-03 20:13:07.615284+0800 ThreadTest[4963:1290654] get OtherInfo end
2019-01-03 20:13:07.615583+0800 ThreadTest[4963:1290654] other info done
2019-01-03 20:13:07.615583+0800 ThreadTest[4963:1290656] one info done
2019-01-03 20:13:07.615914+0800 ThreadTest[4963:1290654] all task done

NSOpreation

是GCD的一种封装,需要使用子类。

任务队列:NSOpreationQueue 相当于一个线程池的概念,可以添加任务,设置最大并发数。

任务有几种状态:ready,canceld,executing,finished,asynchronous

任务可以很方便的添加依赖

我们可以使用系统提供了两个子类创建NSOpreaion通过调用任务的start方法启动任务,会在当前的线程同步执行。

如果我们需要异步执行,通过创建队列,把任务添加到队列即可。

    NSBlockOperation *op =  [NSBlockOperation blockOperationWithBlock:^{
      NSLog(@"任务执行了");
    }];
    [op start];// 在当前线程同步执行
    
   NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperation:op];// 异步执行

如果任务需要设置依赖关系,调用任务的方法

-(void*)addDependency:(NSOperation *)op;

如果我们需要等待所有任务完成,调用队列的方法

-(void*)waitUntilAllOperationsAreFinished;

注意事项:

线程之间共用进程所有资源,当多线程操作同一个变量的时候,可能会使得结果不正确。

因此要特别注意线程安全的问题。

通常保证线程安全有很多种方式

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

推荐阅读更多精彩内容