Effective Objective-C 2.0 读书笔记——大中枢派发

devtools/2025/2/20 18:59:23/

Effective Objective-C 2.0 读书笔记——大中枢派发

多用派发队列,少用同步锁

说到同步锁,我们不难想起我们前面在学习线程之中的内容时学习到的关键字@synchronized,使用这个同步块可以让我们这段程序实现加锁的操作,即在不同线程之中这个关键字的内容只能有一条线程运行。自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码结尾处,锁就释放了。以下是例子

 -(void) synchronizedMethod {@synchronized (self) {///Safe}} 

但是滥用@synchronized (self)会很危险,因为所有同步块都会彼此抢夺同一个锁。要是有很多个属性都这么写的话,那么每个属性的同步块都要等其他所有同步块执行完 毕才能执行,这也许并不是开发者想要的效果。我们只是想令每个属性各自独立地同步。

替代方案就是使用 GCD,它能以更简单、更高效的形式为代码加锁。比方说,属性就是开发者经常需要同步的地方,这种属性需要做成“原子的”。

初步代码

有种简单而高效的办法可以代替同步块或锁对象,那就是使用“ 串行同步队列” (serial synchronization queue)。将读取操作及写入操作都安排在同一个队列里,即可保证数据同步。 其用法如下:

// 创建同步队列(通常在初始化方法中创建)
_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);- (NSString *)someString {__block NSString *localSomeString;dispatch_sync(_syncQueue, ^{localSomeString = _someString;});return localSomeString;
}- (void)setSomeString:(NSString *)someString {dispatch_sync(_syncQueue, ^{_someString = someString;});
}

由于写操作(setter)不需要返回值,所以可以考虑使用 异步派发(dispatch_async) 替代同步派发,这样调用者就不必等待写操作完成,从而可能提升设置方法的执行速度。

异步写入

- (void)setSomeString:(NSString *)someString {// 异步派发写操作,调用者不会被阻塞dispatch_async(_syncQueue, ^{_someString = someString;});
}

优点:

  • 调用者性能提升:因为写操作是异步执行的,调用者立即返回,不需要等待 block 执行完毕。

缺点:

  • Block 拷贝开销:dispatch_async 会拷贝 block。如果拷贝 block 的成本超过 block 实际执行的时间,在简单的例子中可能会导致整体性能反而变慢。
  • 适用场景:对于比较轻量的操作,异步写可能没有明显优势,但如果 block 内执行的任务较重或耗时,那么异步方式能使得调用者更快返回,提升整体效率。

用栅栏函数再优化

我们先前使用的就是串行队列,我们知道如果我们使用并行队列,那么性能将会更好,但是如果使用并行队列似乎就没办法完成安全读写这个点,于是我们就想起我们之前学习过的栅栏函数,将读写两个操作分开

// 创建一个并发队列,使用 barrier 确保写操作的独占性
_syncQueue = dispatch_queue_create("com.example.syncQueue", DISPATCH_QUEUE_CONCURRENT);- (NSString *)someString {__block NSString *localSomeString;// 使用同步派发保证读取时数据一致dispatch_sync(_syncQueue, ^{localSomeString = _someString;});return localSomeString;
}- (void)setSomeString:(NSString *)someString {// 使用 barrier 异步派发确保写操作独占执行dispatch_barrier_async(_syncQueue, ^{_someString = someString;});
}

image-20250214180228383

我们多个读取操作变为异步执行,而写入操作则变成了单独执行,既避免了读取发生错误,又提高了程序的效率。

多用 GCD,少用performSelector系列方法

我们在讲述OC之中的动态性介绍过,performSelector与选择子结合用于动态绑定,可简化复杂的代码

该方法与直接调用选择子等效。所以下面两行代码的执行效果相同:

[object performSelector: @selector (selectorName) ];
[object selectorName] ;

看似多余之举,实则如果选择子在运行期才能决定,就能体现其强大之处了

SEL selector;
if (/* some condition */) {selector = @selector(newObject);
} else if (/* some other condition */) {selector = @selector(copy);
} else {selector = @selector(foo);
}[object performSelector:selector];

但是这样写的代码也会出现一定的问题,我们前面也知道了在 Objective-C 中,根据方法命名规范(比如以 allocnewcopymutableCopy 开头的方法返回的对象属于调用者,需要手动释放),调用方法时返回的对象的内存管理责任是有约定的。那么不难理解这段代码之中的ret对象,在前两种情况需要被手动释放,最后一种则无需释放。那么在运行期才确定的内容,那么ARC就无法用简单的内存管理规则来管理相应内存,那么ARC在这种情况下会使用比较谨慎的做法,即不添加释放操作,那就会造成内存泄漏。

另外一点,这些方法的返回值只能是void或者是对象类型。如果调用的方法实际上返回了一个基本数据类型(如整数或浮点数),则必须通过一些复杂且容易出错的转换才能正确处理,因为这些数据类型与指针大小可能不一致。

由于 id 只是一个指向 Objective-C 对象的指针:

  • 在 32 位系统上,返回值的大小只能是 32 位以内的数据。
  • 在 64 位系统上,则只能是 64 位以内的数据。
  • 如果返回的是一个 C 语言的结构体,而该结构体的大小超过了指针的大小,就不能使用 performSelector方法来调用,因为它无法正确返回超出指针大小的数据。

由于参数类型是id,所以传入的参数必须是对象才行。如果选择子所接受的参数是整数或浮点数,那就不能采用这些方法了。此外,选择子 最多只能接受两个参数,也就是调用performSelector: withObject: withObject:这个版本。 而在参数不止两个的情况下,则没有对应的 performSelector方法能够执行此种选择子。

书中给了两组示例展示了如何用 GCD 的块来实现与 performSelector 系列方法相同的功能,同时也解决了它们在内存管理和返回值类型上的一些限制。

延后执行任务

方案一:使用 performSelector:withObject:afterDelay:

// 延后 5 秒执行 doSomething 方法
[self performSelector:@selector(doSomething)withObject:nilafterDelay:5.0];

方案二(推荐):使用 dispatch_after:

// 计算延迟时间(5秒)
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));// 在主队列中延后 5 秒后执行 doSomething 方法
dispatch_after(delayTime, dispatch_get_main_queue(), ^{[self doSomething];
});

在主线程上执行任务

方案一:使用 performSelectorOnMainThread:withObject:waitUntilDone:

// 在主线程上异步执行 doSomething 方法
[self performSelectorOnMainThread:@selector(doSomething)withObject:nilwaitUntilDone:NO];

方案二(推荐):使用 dispatch_async:(若需要等待则用 dispatch_sync:)

// 在主队列上异步执行 doSomething 方法
dispatch_async(dispatch_get_main_queue(), ^{[self doSomething];
});

通过DispatchGroup 机制,根据系统资源状况来执行任务

dispatch_group_async

void dispatch_group_async (dispatch_group_t group, dispatch_queue_t queue,dispatch_block_t block) ;

下面是一个使用 dispatch_group_async 的例子,展示如何同时执行多个异步任务,并在所有任务完成后进行统一处理:

// 创建一个调度组
dispatch_group_t group = dispatch_group_create();// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);// 异步提交第一个任务到组中
dispatch_group_async(group, queue, ^{NSLog(@"任务1开始");// 模拟耗时操作sleep(2);NSLog(@"任务1完成");
});// 异步提交第二个任务到组中
dispatch_group_async(group, queue, ^{NSLog(@"任务2开始");sleep(3);NSLog(@"任务2完成");
});// 异步提交第三个任务到组中
dispatch_group_async(group, queue, ^{NSLog(@"任务3开始");sleep(1);NSLog(@"任务3完成");
});// 当组中所有任务执行完毕后,在主线程上执行回调
dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"所有任务完成,更新UI");
});

GCD 动态管理线程:
GCD 会根据当前系统资源状况(例如 CPU 核心数量、当前负载、队列中待执行任务数量等)自动创建新线程或复用旧线程。

  • 如果你使用并发队列,并且有大量任务等待执行,GCD 可能会在多个线程上同时执行这些任务,以充分利用多核处理器的能力。
  • 开发者不需要手动管理线程调度,GCD 会自动调整并发执行的程度,使得任务的并行度适应当前系统的资源状况。

调度组的作用:
通过 dispatch_group,你可以把一组任务“打包”,让它们并发执行,并在所有任务完成后收到通知。这样既利用了并发执行的优势,又能在全部任务结束后统一处理后续操作。

dispatch_apply 的使用

  • 功能简介:
    dispatch_apply 是另一种并发任务调度函数,它会对一个给定次数(iterations)重复执行一个 block,并把每次执行时的索引传给该 block。

    • 例如,如果你希望对数组中的每个元素执行相同的操作,可以用 dispatch_apply 替代常规的 for 循环。
  • 使用示例:

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply(10, queue, ^(size_t i) {// 这里 i 从 0 到 9,每次执行 block 时 i 的值会递增NSLog(@"Iteration %zu", i);// 执行一些任务...
    });
    

    或者针对数组:

    NSArray *array = @[@"A", @"B", @"C", @"D"];
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply(array.count, queue, ^(size_t i) {id object = array[i];[object performTask];
    });
    
  • 与常规 for 循环的比较:

    • 使用简单的 for 循环也可以达到相同效果,但 dispatch_apply 的优势在于它直接利用 GCD 的调度机制,可以自动将任务分发到多个线程上并发执行(前提是使用并发队列)。
    • 不过需要注意,dispatch_apply 是一个阻塞调用,会等待所有任务执行完成后才返回,因此在性能测试中,若每个 block 的执行时间非常短,block 拷贝的开销反而可能使它比简单 for 循环更慢。

使用dispatch_once 来执行只需运行一次的线程安全代码

GCD引入了一项特性,能使单例实现起来更容易。所用的函数是:

void dispatch_once (dispatch_once_t *token,dispatch_block_t block);

此函数接受类型为dispatch_once_t的特殊参数,书中称之为标记,对于给定的标记来说,该函数保证相关的块必定会执行,且仅执行 一次。首次调 用该函数时,必然会执行块中的代码,最重要的 一点在于,此操作完全是线程安全的。请注 意,对于只需执行一次的块来说,每次调用函数时传人的标记都必须完全相同。

我们的单例模式相关方法可以写成以下形式:


+ (id) sharedInstance (
static EOCClass *sharedInstance = nil;
stat icdispatch_once_t onceToken; 
dispatch_once (&onceToken, ^{sharedInstance = [[self alloc] init];
};
return sharedInstance;
}

使用dispatch _once可以简化代码并且彻底保证线程安全,开发者根本无须担心加锁 或同步。所有问题都由GCD 在底层处理。由于每次调用时都必须使用完全相同的标记, 所以标记要声明成static。把该变量定义在static作用域中,可以保证编译器在每次执行 sharedInstance 方法时都会复用这个变量,而不会创建新变量。

不要使用dispatch_get_current_ queue

概念不明确
“当前队列”这个概念并不总是明确的。当你调用 dispatch_get_current_queue() 时,它返回的队列可能是系统内部的私有队列,而不一定是你期望的那个队列。这会导致程序行为难以预测。

容易引发死锁
如果你在当前队列中同步调用某个任务,而这个任务又试图获取当前队列,就可能会导致死锁。由于对当前队列的依赖很容易形成循环依赖,使用 dispatch_get_current_queue() 增加了死锁风险。

dispatch_get_current_queue() 返回当前执行任务的队列。如果你使用它来获取队列,并用来进行同步调用,就有可能无意中对同一个队列调用 dispatch_sync。例如:

dispatch_queue_t currentQueue = dispatch_get_current_queue();
// 当前任务在 currentQueue 上执行
dispatch_sync(currentQueue, ^{// 这个 block 也要在 currentQueue 执行// 但 currentQueue 正在执行外部的任务,无法中断执行
});

即使我们在编程的过程之中可以通过仔细观察从而避免以上的问题,但是书中提到的另一种情况则不是那么容易查出来的,这是书中提出的场景

image-20250216222932249

排在队列B或队列C中的块,稍后会在队列A里依序执行。于是,排在队列A、B、C中 的块总是要彼此错开执行。然而,安排在队列D中的块,则有可能与队列A里的块(也包括队列B与C里的块)并行,因为A 与D的目标队列是个并发队列。若有必要,并发队列可以用多 个线程并行执行多个块,而是否会这样做,则需根据CPU的核心数量等系统资源状况来定。

排在队列C里的块,会认为当前队列就是队列C,而开发者可 能会据此认定:在队列A上能够安全地执行同步派发操作。但实际上,这么做依然会像前面 那样导致死锁。怎么解决这些问题呢?

GCD之中提供了一个队列特定数据(queue-specific data)的功能:

  • 定义:

    队列特定数据允许你将任意数据(以键值对形式)关联到一个特定的 GCD 队列上。这个数据就像“标签”一样,可以让你在执行任务时知道当前任务属于哪个队列。

  • 查找机制:

    当你调用 dispatch_get_specific(key) 时,系统不仅在当前队列中查找与该键关联的数据,还会沿着队列的目标队列(target queue)链进行查找,直到找到该数据或到达根队列为止。

dispatch_queue_t queueA =dispatch_queue_create("com.effectiveobjectivec.queueA", NULL);
dispatch_queue_t queueB =dispatch_queue_create("com.effectiveobjectivec.queueB", NULL);// 将 queueB 的目标队列设置为 queueA
dispatch_set_target_queue(queueB, queueA);static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueA");// 在 queueA 上设置队列特定数据:键是 &kQueueSpecific,值是 queueSpecificValue
dispatch_queue_set_specific(queueA,&kQueueSpecific,(void*)queueSpecificValue,(dispatch_function_t)CFRelease);dispatch_sync(queueB, ^{dispatch_block_t block = ^{NSLog(@"No deadlock!");};// 从当前队列(或沿着目标队列链)获取与键 &kQueueSpecific 关联的数据CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);if (retrievedValue) {// 如果取到了数据,说明当前执行环境就是在我们想要的queueA之中block();} else {// 否则,说明当前队列不满足条件,就同步将 block 提交到 queueA 执行dispatch_sync(queueA, block);}
});

这种方式避免了直接使用 dispatch_get_current_queue() 带来的风险,同时保证了任务在正确的队列中执行,从而防止因线程上下文不对而引发的问题。


http://www.ppmy.cn/devtools/160106.html

相关文章

MySQL数据库(3)—— 表操作

目录 一,创建表 1.1 创建表的SQL 1.2 演示 二,查看表 三,修改表 四,删除表 常用的表操作会涉及到两种SWL语句 DDL(Data Definition Language)数据定义语言:建表、改表、删表等&#xff0…

机器学习_14 随机森林知识点总结

随机森林(Random Forest)是一种强大的集成学习算法,广泛应用于分类和回归任务。它通过构建多棵决策树并综合它们的预测结果,显著提高了模型的稳定性和准确性。今天,我们就来深入探讨随机森林的原理、实现和应用。 一、…

数据分析--数据清洗

一、数据清洗的重要性:数据质量决定分析成败 1.1 真实案例警示 电商平台事故:2019年某电商大促期间,因价格数据未清洗导致错误标价,产生3000万元损失医疗数据分析:未清洗的异常血压值(如300mmHg&#xff…

一些网络编程的补充知识

1.INADDR_ANY 由于云服务器的IP地址是由对应的云厂商提供的,这个IP地址并不一定是真正的公网IP,这个IP地址是不能直接被绑定的,如果需要让外网访问,此时我们需要bind 0。系统当当中提供的一个INADDR_ANY,这是一个宏值…

工控网络安全介绍 工控网络安全知识题目

31.PDR模型与访问控制的主要区别(A) A、PDR把对象看作一个整体 B、PDR作为系统保护的第一道防线 C、PDR采用定性评估与定量评估相结合 D、PDR的关键因素是人 32.信息安全中PDR模型的关键因素是(A) A、人 B、技术 C、模型 D、客体 33.计算机网络最早出现在哪个年代(B) A、20世…

【RK3588嵌入式图形编程】-SDL2-构建模块化UI

构建模块化UI 文章目录 构建模块化UI1、概述2、创建UI管理器3、嵌套组件4、继承5、多态子组件6、总结在本文中,将介绍如何使用C++和SDL创建一个灵活且可扩展的UI系统,重点关注组件层次结构和多态性。 1、概述 在前面的文章中,我们介绍了应用程序循环和事件循环,这为我们的…

JavaScript(JS)

介绍 JavaScript(简称:JS)是一门跨平台、面向对象的脚本语言。是用来控制网页行为的,它能使网页可交互 JavaScript 和Java 是完全不同的语言,不论是概念还是设计。但是基础语法类似 JS引入方式 内部脚本:将JS代码定义在HTML页面中 JavaScript代码…

【基础架构篇十五】《DeepSeek权限控制:RBAC+ABAC混合鉴权模型》

某天深夜,电商平台运营总监误触按钮,把价值千万的优惠券设置成全员可领。当你想追究责任时,却发现系统日志写着"操作人:admin"。这血淋淋的事故告诉我们:权限控制不是选择题,而是生死攸关的必答题。本文将深挖DeepSeek的RBAC+ABAC混合鉴权体系,看看他们如何做…