多线程第二弹 - 常用的GCD函数

前言

前面第一章知识简单介绍了多线程的理论知识,并没有太多的实际示例,那么本章就主要介绍关于GCD相关的函数,通过每个示例,更加深刻的了解多线程.如果文中观点有错,请大家提出来,相互进步

  • 队列的创建
- (void)viewDidLoad {
    [super viewDidLoad];

    /*
     const char *label : 队列的标签
     dispatch_queue_attr_t attr : 队列的类型(串行还是并发)
     #define DISPATCH_QUEUE_SERIAL 或 NULL :都表示串行
     #define DISPATCH_QUEUE_CONCURRENT
     */
    dispatch_queue_t queue = dispatch_queue_create("com.William.Alex", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{

        // 执行的任务
    });
}
  • 常用方法
- (void)viewDidLoad {
    [super viewDidLoad];

    // 获取当前线程
    [NSThread currentThread];

    // 获取主队列
    NSLog(@"%@",dispatch_get_main_queue());

    // 获取全局并发队列
    /*
     long identifier : 优先级(0:表示默认优先级)
     unsigned long flags : 预留字段,填0即可
     */
    NSLog(@"%@",dispatch_get_global_queue(0, 0));
}

延迟操作
上一章我们大概提了一下延迟操作的三种方法,但是没有细说,在实际开发中,很多时候都需要使用到延迟操作,比如说使用GCD的延迟操作来模拟网络延迟,从而找出隐藏较深的Bug.本章主要讲GCD的延迟操作.

  • 方法


    GCD的延迟操作.png
  • GCD中的延迟操作函数
// 参数 1 :时间  参数 2 : 队列  参数3 : 任务block代码块
dispatch_after(dispatch_time_t when, dispatch_queue_t queue, ^(void)block);

//  参数 : delayInSeconds : 延迟的时间,表示多少时间后执行block代码块中的代码
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 需要执行的任务
    });

dispatch_after示例


// 示例虽然简单,但是通俗易懂
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{

    NSLog(@"点击屏幕就会立即打印出来");

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        NSLog(@"点击屏幕后,2秒之后才会答应出来,不信自己去试试");
    });

}

补充 : performSelector方法虽然不是很常用,但是最好也要知道有这个方法可以实现延迟操作

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"仍然需要有一个参照,不然谁知道是不是延迟了3秒");

    [self performSelector:@selector(run) withObject:nil afterDelay:3.0];
}

- (void)run {

    NSLog(@"看来真的延迟了3秒后才打印");
}
  • 控制线程的状态
- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建一条线程
    NSThread *thread = [[NSThread alloc] initWithTarget:self
                                              selector:@selector(run)
                                                object:nil];

    // 开启线程(当线程执行完毕之后,自动进入死亡状态)
    [thread start];

    // 手动阻塞线程
    [NSThread sleepForTimeInterval:0.2];

    // 强制停止线程(一旦线程停止(死亡)状态,就无法再次开启该线程)
    [NSThread exit];
    }

- (void)run {

    NSLog(@"跑你妹啊");
}

线程的安全隐患问题

  • 多线程的安全隐患 : 为什么多线程存在安全隐患等问题呢,具体是什么原因造成的呢?带着这些问题我们一步一步的学习为什么多线存在安全隐患,解决方法是怎样的.

经典示例 : 资源共享

  • 分析 : 当多个线程访问同一块资源时,就很容易引发数据混乱和数据安全等问题.总之,多个线程访问同一个对象,同一个变量,或者同一个文件时,就可能发生数据紊乱等现象.
  • 解决方案 : 针对上面所述的问题,这里引入了一个新的概念:互斥锁.

互斥锁

  • 互斥锁的原理 : 它的原理就是线程同步技术.
  • 线程同步 : 即多条线程在同一条线生执行任务(并且执行任务是有序的,是按顺序执行任务的).
  • 互斥锁的使用格式 : @synchronized(锁对象){ // 需要锁定的代码 }

互斥锁的优缺点

  • 优点
    • 能够有效防止因多条线程因抢夺资源而引发的数据安全问题
  • 缺点
    • 需要消耗大量的CPU资源

互斥锁的注意点

  • 锁定一份代码只能使用1把锁,不管你嵌套多少把锁都是无效的.
  • 互斥锁的使用前提 : 多条线程抢夺同一块资源.

互斥锁的应用场景

  • 讲到互斥锁的应用场景,这里我们又要引入每天都能接触到两个概念:即原子和非原子属性,对,就是我们每天定义属性时,传入的修饰参数.

  • OC中定义属性时有两种选择:即nonatomic和atomic

  • nonatomic : 非原子属性,不会为setter方法加互斥锁

  • atomic : 原子属性,会为setter方法加锁(系统默认是atomic是需要枷锁的).

  • 知识补充 : 一般我们开发APP时,都是使用nonatomic,就性能而言非原子属性的性能高于原子属性,原因是atomic是线程安全的,加锁会消耗大量的CPU资源,nonatomic虽然是非线程安全,但是对于内存较小的移动设备,尽量使用nonatomic.

示例

(忽略多次点击程序会崩溃,这里主要想演示线程加锁)

#import "ViewController.h"

@interface ViewController ()

/** 火车票数量 */
@property (nonatomic, assign) NSInteger titketCount;

/** 售票员1 */
@property(nonatomic, strong) NSThread *conductor1;
/** 售票员2 */
@property(nonatomic, strong) NSThread *conductor2;
/** 售票员3 */
@property(nonatomic, strong) NSThread *conductor3;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    // 假设有50张火车票
    self.titketCount = 50;

    // 创建三个售票窗口
    self.conductor1 = [[NSThread alloc] initWithTarget:self
                                                   selector:@selector(saleTitkets)
                                                     object:nil];
    self.conductor1.name = @"售票员1";

    self.conductor2 = [[NSThread alloc] initWithTarget:self
                                                   selector:@selector(saleTitkets)
                                                     object:nil];
    self.conductor2.name = @"售票员2";

    self.conductor3 = [[NSThread alloc] initWithTarget:self
                                                   selector:@selector(saleTitkets)
                                                     object:nil];
    self.conductor3.name = @"售票员3";
}

#pragma mark - 开启线程

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{

    [self.conductor1 start];
    [self.conductor2 start];
    [self.conductor3 start];
}

#pragma mark - 开始售票

- (void)saleTitkets {

    while (1) {
    @synchronized(self) {
            NSInteger count = self.titketCount;
            if (count > 0) {
                self.titketCount = count - 1;

                NSLog(@"%@卖了一张火车票,还剩%ld张火车票",[NSThread currentThread].name, self.titketCount);
            } else
            {
                NSLog(@"没票了");
                break;
            }
        }
    }
}

打印结果(没有枷锁时的部分打印)

2016-03-22 14:57:28.770 01 - 多线程[1241:105971] 售票员3卖了一张火车票,还剩47张火车票
2016-03-22 14:57:28.770 01 - 多线程[1241:105969] 售票员1卖了一张火车票,还剩49张火车票
2016-03-22 14:57:28.770 01 - 多线程[1241:105970] 售票员2卖了一张火车票,还剩48张火车票
2016-03-22 14:57:28.771 01 - 多线程[1241:105971] 售票员3卖了一张火车票,还剩46张火车票
2016-03-22 14:57:28.771 01 - 多线程[1241:105969] 售票员1卖了一张火车票,还剩45张火车票
2016-03-22 14:57:28.771 01 - 多线程[1241:105970] 售票员2卖了一张火车票,还剩44张火
  • 根据上面的打印,我们没有添加互斥锁,造成的结果就是数据紊乱,第49对应的火车票都还没有被售出去,就直接将第47对应火车票售出.应该是先售出第49对应的火车票,才能去售卖第47对应的火车票.

打印结果(加锁后的部分打印)

2016-03-22 15:02:55.688 01 - 多线程[1268:108069] 售票员1卖了一张火车票,还剩49张火车票
2016-03-22 15:02:55.689 01 - 多线程[1268:108070] 售票员2卖了一张火车票,还剩48张火车票
2016-03-22 15:02:55.689 01 - 多线程[1268:108071] 售票员3卖了一张火车票,还剩47张火车票
2016-03-22 15:02:55.689 01 - 多线程[1268:108069] 售票员1卖了一张火车票,还剩46张火车票
2016-03-22 15:02:55.689 01 - 多线程[1268:108070] 售票员2卖了一张火车票,还剩45张火车票
2016-03-22 15:02:55.690 01 - 多线程[1268:108071] 售票员3卖了一张火车票,还剩44张火车票
2016-03-22 15:02:55.690 01 - 多线程[1268:108069] 售票员1卖了一张火车票,还剩43张火

  • 注意 : 使用@synchronized(锁对象) { 需要锁定的代码}时,注意它的位置,我又一次将循环也放在锁内,导致打印的结果:火车票全是"售票员1"卖出的.这明显不对.我们只需要将售票环节锁住即可.

一次性代码(单例:下一章专门讲单例模式)

  • 一次性代码 : 使用dispatch_once_t函数,能保证在整个应用程序在运行过程中只会被执行一次,其内部的代码系统默认是线程安全的.
- (void)once {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"内部的代码系统默认是线程安全的");
        NSLog(@"整个进程运行过程中,只会有一份内存");
    });

}
  • 一次性代码的应用场景 : 单例模式,具体下一章会专门介绍单例模式的原理以及使用,这里就不再过多介绍了.

快速迭代遍历

  • 快速迭代类似于for循环,但是它在并发队列中时,会并发执行block中的任务,所以它的效率肯定是比一般的for循环要更加高效一点
  • 注意最后的打印,dispatch_apply是会阻塞主线程的。这个NSLog打印会在dispatch_apply迭代结束后才开始执行,如果没有打印,那么说明阻塞了.
  • dispatch_apply快速迭代会避免线程爆炸,因为GCD会管理线程

快速剪切文件-dispatch_apply示例

- (void)apply {
/*
思路 :
1, 获取文件所在的原始文件夹路径
2, 获取文件要去的新文件夹的路径
3, 创建一个文件管理者
4, 拿到原始路径下的所有文件
5, 遍历原始路径下的的文件夹中的所有文件
6, 拼接原始路径和新路径为全路径
7, 使用异步 + 并发队列 将会话管理者中的所有文件从原始文件移动到新文件夹下.
*/
    // 创建一个全局队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    // 设置剪切文件的路径
    NSString *from = @"/Users/mac/Desktop/Frome";
    NSString *to = @"/Users/mac/Desktop/To";

    /*
     NSFileManager也是一个单例
     创建文件管理者
     */
    NSFileManager *fileManager = [NSFileManager defaultManager];

    // 拿到from文件路径中所有子路径
    NSArray *subPaths = [fileManager subpathsAtPath:from];

    /*
     参数 1: from路径下的所有子路径的总数. 参数 2 : 队列  参数 3: block代码块,双击后需要添加索引index
     */
    dispatch_apply(subPaths.count, queue, ^(size_t index) {

        // 根据索引拿到需要剪切的文件
        NSString *subpath = subPaths[index];

        // 拼接全路径
        NSString *fullFromPath = [from stringByAppendingPathComponent:subpath];
        NSString *fullToPath = [to stringByAppendingPathComponent:subpath];

        /*
         剪切文件:从from文件夹中剪切文件到to文件夹
         */
        [fileManager moveItemAtPath:fullFromPath toPath:fullToPath error:nil];
    });
}

队列组 : dispatch_group

  • dispatch_group的作用 : dispatch_group是专门用来监听多个异步任务,换句话说,dispatch_group实例是用于追踪不同队列中不同的任务.
  • 当dispatch_group(队列组)中所有的事件都执行完毕后,GCD提供了两种API方式发送通知.
    • 第一种方式 : dispatch_group_wait
      • 会阻塞线程,等所有任务都完成或者等待超时时才会执行下一个任务.
    • 第二种方式 : dispatch_group_notify(重点讲这个)
      -不会阻塞线程,是异步执行block中的任务.

示例

要求分别下载两张网络上的图片,然后将两张图片合成一张显示出来

  • 分析 : 上述示例简单总结为:
    • 1, 分别执行两个耗时的异步操作.
    • 2, 等2个耗时的异步操作执行完毕之后,再回到主线程刷新UI界面
#import "ViewController.h"

@interface ViewController ()

/**
 *  显示下载的图片
 */
@property (weak, nonatomic) IBOutlet UIImageView *AppleImage;

/**
 *  图片 1
 */
@property (nonatomic, weak) UIImage *image1;

/**
 *  图片 2
 */
@property (nonatomic, weak) UIImage *image2;


@end

@implementation ViewController

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{

    [self group];
}

- (void)group {

    // 创建全局队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    // 创建一个队列组
    dispatch_group_t groupQueue = dispatch_group_create();

    // 下载第一张图片
    dispatch_group_async(groupQueue, queue, ^{

        // 根据url下载图片
        NSURL *url = [NSURL URLWithString:@"网络图片Ur"];

        // 将图片以二进制的形式保存到本地
        NSData *imageData = [NSData dataWithContentsOfURL:url];

        // 显示图片
        self.image1 = [UIImage imageWithData:imageData];
    });


    // 下载第二种图片
    dispatch_group_async(groupQueue, queue, ^{

        // 通过url找到图片
        NSURL *url = [NSURL URLWithString:@"网络图片Url"];

        // 将图片保存到本地
        NSData *imageData = [NSData dataWithContentsOfURL:url];

        // 显示图片
        self.image2 = [UIImage imageWithData:imageData];
    });


    // 执行完毕耗时操作,将图片合二为一,然后刷新界面显示图片
    dispatch_group_notify(groupQueue, queue, ^{

        // 开启上下文
        UIGraphicsBeginImageContext(CGSizeMake(100, 100));

        // 绘制图片
        [self.image1 drawInRect:CGRectMake(0, 0, 100, 100)];
        [self.image2 drawInRect:CGRectMake(50, 0, 100, 100)];

        // 获取合成的图片
       UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

       // 关闭上下文
        UIGraphicsEndImageContext();

        // 回到主线程刷新界面,显示图片
        dispatch_async(dispatch_get_main_queue(), ^{

            // 显示图片
            self.AppleImage.image = image;
        });
    });
}

@end

  • 总结 : 这里使用了图形上下文的知识,因为是需要将下载完毕的两张图片合成一张新图片展示到屏幕上.但是注意,当队列中耗时的操作执行完毕之后,必须要调用_notify这个方法,在这个方法中合成新图片,当该方法中的队列中的任务完成之后,才会来到主线程,进行刷新UI界面.处理UI事件.

线程间的通信

在应用程序运行过程中,线程之间并不是孤立存在的,它们之间是可以线程通讯的

  • 线程通信的具体体现

    • 一条线程想要传递数据给另一条线程
    • 当在某条线程上执行完毕任务之后,需要跳转到另一条线程上去执行任务
  • 比如上面队列组中的示例(下载图片),当程序开始下载图片是,原理是:会开启子线程执行下载图片的操作,当下载操作执行完毕之后,需要跳转到主线程上执行UI刷新操作.

下载图片的线程原理

线程间的通信.png

线程之间通信的三种方法

// 跳转到指定线程上执行任务(或者将数据传递到指定线程)
[self performSelector:(nonnull SEL) onThread:(nonnull NSThread *) withObject:(nullable id) waitUntilDone:(BOOL)];
[self performSelector:(nonnull SEL) onThread:(nonnull NSThread *) withObject:<#(nullable id)#> waitUntilDone:(BOOL) modes:(nullable NSArray<NSString *> *)];

// 跳转到主线程上执行任务(或者将数据传递到主线程)
[self performSelectorOnMainThread:(nonnull SEL) withObject:(nullable id) waitUntilDone:(BOOL)]
[self performSelectorOnMainThread:(nonnull SEL) withObject:(nullable id) waitUntilDone:(BOOL) modes:(nullable NSArray<NSString *> *)]

  • 注意 :他们虽然传入的参数不一样,但是功能是一样的.

下面介绍一种比较常用的


    // 创建全局的并发队列
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_async(globalQueue, ^{

        // 执行子线程上耗时的操作

        dispatch_async(dispatch_get_main_queue(), ^{

            // 回到主线程,执行UI刷新的操作
        });

    });
  • 这种方式的线程通信: 以将它的原理看出一个嵌套.在子线程中嵌套一个主线程,比如:下载图片

  • 首先在获取一个异步的并发队列,在并发队列中执行下载图片(耗时的操作),然后在并发队列中嵌套一个异步的队列,该队列是主线程,回到主线程处理UI更新操作.

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

推荐阅读更多精彩内容