Swift并发的结构化编程

并发(concurrency)

早期的计算机 CPU 都是单核的,操作系统为了达到同时完成多个任务的效果,会将 CPU 的执行时间分片,多个任务在同一个 CPU 核上按时间先后交替执行。由于 CPU 执行速度足够地快,给人的错觉就像在同时执行多个任务。这种通过不同任务的指令切换来实现多任务的技术,称为「concurrency」,中文术语为「并发」。

后来,CPU 发展到两核、多核,同一个时刻,在不同的核上可以执行不同的任务。理论上,有 N 个 CPU 核即可同时不受干扰地在 N 个核上都完全独立运行一个任务。这种通过在不同 CPU核 上运行多任务的技术称为「parallel」,中文术语为「并行」。

现代操作系统的进程和线程调度已经完全屏蔽了这两种多任务技术的差异。大部分情况下,开发者不需要关心两个任务到底是在不同的 CPU 核上执行还是在一个 CPU 核的不同时间分片上执行。因此,在很多技术文档中,也常常使用「concurrency」一词表示多个任务同时进行这种特性,用于充分利用 计算机 系统的多核处理器,提高程序的性能和效率。

结构化编程

在我们开始学习 C 语言时,尽量不用或少用 goto 语句。尽管 C 语言规范已经限制了 goto 必须在本函数内部跳转,但使用 goto 语句仍然有着很大的不确定性:它可以跳转到函数中的任意位置。想象一下,如果函数中大量使用 goto 语句会是什么样的景象。如果你曾经了解过汇编语言,那么一定对汇编语言的看似排列整齐实则包含各种跳转的逻辑深恶痛觉,开发者必须一个语句一个语句地分析,小心翼翼地探索才能理清其中关系。过度使用 goto 会和使用汇编面临一样的问题。

好在现代编程语言早就经过了早期的洪荒时代,几乎每一种现代编程语言都会包含函数、条件语句、循环等基本要素。这些我们早已习以为常的代码逻辑,恰恰正是体现「结构化编程」的良好范例:使用函数、条件语句、循环等的代码的控制流总是单一的,不会出现类似 goto 这样的无法预知跳转到哪里的「分叉」。

「结构化编程」的核心思想就是代码的抽象和封装,确保程序运行路径总是从单一入口进入,执行结束后在单一出口退出,不会有第二种情况。以函数为例,不管函数中实现了多么复杂的逻辑,调用方根本不需要关心函数内部是如何实现的,当调用发生时,执行控制权交给该函数,无论是否发生错误、是否存在未能准备好的资源,该函数一定会在未来的某一个时刻返回结果并将执行控制权交还给调用方。

我们一直在享受现代编程语言「结构化编程」提供的便利,在我们日常开发的同步代码中,随处可见「结构化编程」的影子。

非结构化并发

在单线程编程中,借助函数、条件判断等控制流使得「结构化编程」早已司空见惯;但在并发编程中,涉及到线程和并发任务的切换,就没有那么容易实现「结构化」了。事实上,在过去的很长一段时间,「非结构化并发」仍然是主流。

非结构化并发最明显的问题就是很多时候会浪费 CPU 算力。当线程进入到耗时 I/O 操作时就处于阻塞状态,必须等待 I/O 操作完成,当很多线程都出现这种状态时,CPU 实际上就处于低负载或空闲状态而造成算力的浪费 - 空闲的算力本可以用来执行其他计算任务。

另外,非结构化并发将会异步调用多出一个一个的执行分支,这些分支并没有像函数调用那样有一个统一的出口,也没有办法将并发任务的执行结果或错误信息在调用者的线程上下文中回传。来看一个使用「非结构化并发」的示例:

func task0() {print("in task0")
}func task1() {DispatchQueue.global().async {self.task0()}
}func main() {DispatchQueue.global().async {self.task1()}
}

上例中,调用 main、task1、task0 方法时,控制流会返回给调用着,但 main、task1 内部异步执行了并发任务,相当于执行了类似 goto 的跳转行为,这些并发任务将在其它线程完成处理并获取结果,其生命周期也和 main、task1 的作用域完全无关。

再来看一个简单却典型的例子:

load_conf { url inload_image(url) { data inresize_image(data) { data inshow_image(data)//1}//2}//3
}

示例演示了一系列的任务:load_conf(读取配置) -> load_image(加载图片) -> resize_image(处理图片) -> show_image(显示),这种书写方式在之前的开发中普遍存在。很显然它有以下问题:

  • 任务界限不清晰

每个异步任务都在回调中反馈执行结果,一层回调嵌套一层回调,开发者需要通过代码缩进来判断任务边界,当代码量较多时分析起来会很痛苦。特别是当拷贝大段代码时,原始的缩进信息可能存在丢失或错乱的情况,处理起来更让人头疼,可维护性较差。

  • 任务终止时机不明确

每一个异步任务都具备一定的执行条件(上述示例中没有体现),比如 load_image 会要求参数 url 是一个合法的图片地址,否则会终止执行。当一个异步任务因为执行条件或中间过程失败时,就不应该继续执行其他任务,上述示例中,最终可能会在 1、2、3 处终止。而有些开发者仅仅会关注最常规的那个路径,也就是在 1 处终止,而没有考虑到 2、3 处终止时的异常处理。上述示例还比较简单,实际开发中比这复杂的比比皆是,开发者要考虑的异常路径则更多。

另外,请回忆一下在 Objective-C ,如果需要实现一个或多个代码块依赖另一个或多个代码块的逻辑,有哪些实现方式?

我们可以使用 NSOperationdispatch_groupdispatch_barrier 甚至是信号量等相关 API 完成需求,但这些方案需要开发者明确了解对应 API 的含义及使用陷阱。比如,在使用 GCD 时一个经典的死锁问题:

dispatch_queue_t queue = dispatch_queue_create("kanchuan.com", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{dispatch_sync(queue, ^{NSLog(@"thread => %@", [NSThread currentThread]);});
}); 

这种初学者一不小心就会犯错的例子不在少数。传统的非结构化并发方案在语法表达和使用上比较繁琐,使用不当会造成资源竞争、死锁等严重问题。

通过以上的分析,我们来总结下非结构化并发的缺点:

  1. 线程 I/O 阻塞时无法充分发挥 CPU 算力;
  2. 异步任务无法获知自己从那里来,调用者也不知道异步任务会在何时结束;
  3. 调用者无法找到一个合适的时机统一处理异步任务的返回结果;
  4. 调用者无法取消自己派发的异步任务;
  5. 异步任务可能也会触发自己内部的异步任务,这会让问题变得更加复杂;
  6. 开发者需要手动管理资源竞争问题。

以上问题在非结构化并发中广泛存在,那么,现在是时候考虑使用「结构化并发」了。

四、实现结构化并发的底层技术

结构化并发既然能解决非结构化并发的问题,那么为什么不一开始就采用结构化并发的设计呢?不要说古老的 Objective-C ,就是 Apple 全新设计的 Swift 语言也要到 5.5 才支持结构化并发且最开始还有一些问题。归根到底,是因为实现结构化并发所需要的底层技术栈更加复杂。这些技术包括:

1、作用域(Scope)

结构化是以 代码块(Code Block) 为执行范围,而结构化并发则是以 作用域(Scope) 为执行范围。在不同的编程语言中,对于结构化并发作用域的命名所有不同,如 Kotlin 称为 Scope,Swift 称为 Task。

在 Swift 中,结构化并发依赖异步函数,异步函数又必须运行在某个 Task 中,Swift 结构化并发是以 Task 为基本要素进行组织的。

2、协程(Coroutine)和 异步函数(Async function)

用户态和内核态线程的主要区别如下:

优点缺点
用户态线程轻量,避免了从用户态到内核态切换的开销。一个线程只能占用一个核;操作系统无法感知用户态线程,需要开发者管理用户态线程的调度。
内核态线程操作系统可充分利用多核优势,实现真正的并行。内核态线程调度时要进行寄存器切换、特权模式切换、内核检查等,开销较大;内核线程表支持的线程个数有限。

那么,协程又是什么呢?

协程(Coroutine)这个名词早在 1958 年被提出来了,但很长一段时间没有被广泛应用。在一些资料中,直接定义“协程就是用户态线程”。我一开始看到这个定义是一脸懵逼的,在不断的 Google 和 ChatGPT 之后,我的结论是:协程确实就是用户态线程,但它和传统意义上的用户态线程还是有区别的:

  • 协程是编程语言运行时支持和负责调度的

传统意义上的用户态线程是由如 POSIX threads 库实现并进行管理和调度的。虽然也有代码库实现的协程方案,但使用的更多的还是来自编程语言原生支持的协程方案。协程的调度在编程语言上最直观地表现就是简化了异步任务的书写方式,对应到 Swift 中,就是通过 async 声明异步函数,通过 await 挂起任务让出线程。

  • 协程的调度是非抢占式的

通过协程调度的代码在执行过程中可以主动让出执行权给其他协程,而不必像传统用户态线程那样必须阻塞。

  • 协程更加轻量

协程通常运行在单个线程上,而用户态线程需要由操作系统进行调度和管理,需要额外的线程控制块等数据结构来维护线程状态、切换线程上下文。

3、计算续体(Continuation)

协程需要解决的一个重要问题就是:await 异步函数之后的代码是怎么被调度在 异步函数 执行完成后在执行的?

Task.detached { [self] inlet result = await async_func()print("result = \(result)") // print 函数总是会等待异步函数 async_func 执行完成后再执行。
}

这就要借助 计算续体(Continuation)了。

计算续体(Continuation)是一种在并发编程中用于管理异步操作的机制。它帮助开发者更加清晰地表达异步操作的逻辑,避免嵌套闭包和回调地狱。当一个计算过程在中间被打断,其后续部分的信息可以使用一个对象表示,这个对象就是计算续体(Continuation)。

选择哪些部分作为计算续体,需要开发者通过 asyncawait 等关键字明确地告诉编译器。当使用 await 调用一个异步函数时,编译器会将后续部分的代码转换成 Continuation,当异步任务执行完毕之后,再将其结果值传递至 Continuation 中继续执行。可以想见,多个 Continuation 可以嵌套,就好像常规异步编程中的多层级回调一样。

我们知道,普通函数在调用过程中,支持其运行的一个重要内容就是栈帧。栈帧中保存着每一层级函数调用所需要的局部变量、方法参数、返回地址等重要内容,栈帧中内容的增加和减少对应着就是函数的进入和退出。栈帧是由操作系统管理的。

Continuation 和 栈帧 非常相似,与 栈帧 不同的是,Continuation 由编程语言的运行时管理。实践中 Continuation 也会保存函数栈帧的信息以确保在恢复 Continuation 时能够正确地找到执行所需的环境信息。

在 Swift 中,可以通过 withCheckedContinuation API 创建 Continuation:

@frozen public struct UnsafeContinuation<T, E> where E : Error {public func resume(returning value: T) where E == Neverpublic func resume(returning value: T)public func resume(throwing error: E)
}public func withCheckedContinuation<T>(function: String = #function, _ body: (CheckedContinuation<T, Never>) -> Void) async -> T// 异步函数抛出异常时使用 withCheckedThrowingContinuation
public func withCheckedThrowingContinuation<T>(function: String = #function, _ body: (CheckedContinuation<T, Error>) -> Void) async throws -> T

可以将传统异步调用包装为 Swift 的异步函数:

func chuanAsyncFunc() async throws -> Int {try await withCheckedThrowingContinuation { continuation inDispatchQueue.global().async {do {let result = try chuanFunc()continuation.resume(returning: result)//返回结果} catch {continuation.resume(throwing: error)//抛出异常}}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/594893.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【python入门】day17:模块化编程、math库常见函数

什么叫模块 模块的导入 导入所有&#xff1a;import 模块名称 导入指定&#xff1a;from 模块名称 import 函数/变量/类 python的math库 什么是math库 Python的math库是Python的内建库之一&#xff0c;它提供了许多数学函数&#xff0c;包括三角函数、对数函数、幂函数等&a…

Scikit-Learn线性回归(四)

Scikit-Learn线性回归四:梯度下降 1、梯度下降1.1、梯度下降概述1.2、梯度下降及原理1.3、梯度下降的实现2、梯度下降法求解线性回归的最优解2.1、梯度下降法求解的原理2.2、梯度下降法求解线性回归的最优解2.3、梯度下降法求解线性回归案例(波士顿房价预测)3、Scikit-Learn…

我的创作纪念日三年收获和感悟

机缘 我刚开始接触创作也是最近几年开始&#xff0c;当初就是希望自己的收获分享给大家&#xff0c;不仅使自己成长&#xff0c;也可以带着大家一起成长&#xff0c;独乐乐不如众乐乐&#xff0c;人都是自私的以前我都是看到好的知识文章都是自己藏起来&#xff0c;发现收获的…

NSSCTF 简单包含

开启环境: 使用POST传flag&#xff0c;flag目录/var/www/html/flag.php 先使用post来尝试读取该flag.php 没反应: 查看一下源码index.php&#xff0c;看有什么条件 base64解密: <?php$path $_POST["flag"];if (strlen(file_get_contents(php://input)) <…

Qt/C++编写视频监控系统82-自定义音柱显示

一、前言 通过音柱控件实时展示当前播放的声音产生的振幅的大小&#xff0c;得益于音频播放组件内置了音频振幅的计算&#xff0c;可以动态开启和关闭&#xff0c;开启后会对发送过来的要播放的声音数据&#xff0c;进行运算得到当前这个音频数据的振幅&#xff0c;类似于分贝…

SpringSecurity-2.7中跨域问题

SpringSecurity-2.7中跨域问题 访问测试 起因 写这篇的起因是会了解到 SSM(CrosOrigin)解决跨域,但是会在加入SpringSecurity配置后,这个跨域解决方案就失效了,而/login这个请求上是无法添加这个注解或者通过配置(WebMvcConfig)去解决跨域,所以只能使用SpringSecurity提供的.c…

AI原生应用开发“三板斧”亮相WAVE SUMMIT+2023

面对AI应用创新的风口跃跃欲试&#xff0c;满脑子idea&#xff0c;却苦于缺乏技术背景&#xff0c;不得不望而却步&#xff0c;这曾是许多开发者的苦恼&#xff0c;如今正在成为过去。 12月28日&#xff0c;WAVE SUMMIT深度学习开发者大会2023在北京举办。百度AI技术生态总经理…

CMake入门教程【核心篇】宏模板(macro)

&#x1f608;「CSDN主页」&#xff1a;传送门 &#x1f608;「Bilibil首页」&#xff1a;传送门 &#x1f608;「本文的内容」&#xff1a;CMake入门教程 &#x1f608;「动动你的小手」&#xff1a;点赞&#x1f44d;收藏⭐️评论&#x1f4dd; 文章目录 1. 定义宏1.1 基本语…

二分查找(一)

算法原理 原理&#xff1a;当一个序列有“二段性”的时候&#xff0c;就可以使用二分查找算法。 适用范围&#xff1a;根据规律找一个点&#xff0c;能将这个数组分成两部分&#xff0c;根据规律能有选择性的舍去一部分&#xff0c;进而在另一个部分继续查找。 除了最普通的…

Ps:创建基于颜色的蒙版

有时候画面上的某种颜色显得不是很和谐&#xff0c;如下图所示。 将画面上的某种颜色换掉&#xff0c;也是得到创意效果的一种重要手段。 演示视频 如果能创建好相关颜色的蒙版&#xff0c;这样在替换颜色的时候就会更加方便。 ◆ ◆ ◆ 创建基于颜色的蒙版 主要思路&#xf…

【动态规划】C++算法:44 通配符匹配

作者推荐 【动态规划】【字符串】扰乱字符串 本文涉及的基础知识点 动态规划 LeetCode44 通配符匹配 给你一个输入字符串 (s) 和一个字符模式 &#xff0c;请你实现一个支持 ‘?’ 和 ‘’ 匹配规则的通配符匹配&#xff1a; ‘?’ 可以匹配任何单个字符。 ’ 可以匹配…

122基于matlab的CSO-SVM,BA-SVM模式识别模型

基于matlab的CSO-SVM&#xff0c;BA-SVM模式识别模型。优化SVM的两个参数晚上最佳参数确定。输出分类识别结果和准确率。数据可更换自己的&#xff0c;程序已调通&#xff0c;可直接运行。 122鸡群优化算法蝙蝠优化算法 (xiaohongshu.com)

Unity | Shader基础知识番外(向量数学知识速成)

目录 一、向量定义 二、计算向量 三、向量的加法&#xff08;连续行走&#xff09; 四、向量的长度 五、单位向量 六、向量的点积 1 计算 2 作用 七、向量的叉乘 1 承上启下 2 叉乘结论 3 叉乘的计算&#xff08;这里看不懂就百度叉乘计算&#xff09; 八、欢迎收…

78 Python开发-多线程FuzzWaf异或免杀爆破

这里写目录标题 本课知识点:学习目的:演示案例:Python开发-简单多线程技术实现脚本Python开发-利用FTP模块实现协议爆破脚本Python开发-配合Fuzz实现免杀异或Shell脚本 涉及资源: 本课知识点: 协议模块使用&#xff0c;Request爬虫技术&#xff0c;简易多线程技术&#xff0c;…

探索模块化神经网络在现代人工智能中的功效和应用

一、介绍 在快速发展的人工智能领域&#xff0c;模块化神经网络 (MNN) 已成为一项关键创新。与遵循整体方法的传统神经网络架构不同&#xff0c;MNN 采用分散式结构。本文深入探讨了 MNN 的基础知识、它们的优势、应用以及它们带来的挑战。 evertongomede 在人工智能领域&#…

通灵术揭秘:空碗“竖筷子”不倒

通灵术揭秘&#xff1a;空碗“竖筷子”不倒 释名&#xff1a;竖筷子是流传很广的一种民间小术&#xff0c;因其法是在碗中竖起一支或三支筷子&#xff0c;故名。 用处&#xff1a;如果有人莫名其妙的生病了&#xff0c;医药无效&#xff0c;按民间的说法&#xff0c;就是遇鬼了…

苹果cmsV10暗黑大气MT主题模板源码-只有PC版本

苹果cms MT主题是一款多功能苹果cmsV10暗黑大气主题 初次使用说明&#xff1a; 网站模板选择mt 模板目录填写html 后台地址&#xff1a;MT主题,mt/mtset 先应用主题打开前台&#xff0c;再点击后台。 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725…

Python武器库开发-武器库篇之代理池配置(四十)

武器库篇之代理池配置(四十) 我们在渗透的过程中&#xff0c;是必须要挂代理的&#xff0c;相信为何要挂代理的原因&#xff0c;各位也是非常的明白的&#xff0c;这里就不多讲了。关于如何挂代理和购买代理大家可以去看内网隧道代理技术&#xff08;十&#xff09;之公网资产…

GitHub上的15000个Go模块存储库易受劫持攻击

内容概要&#xff1a; 目前研究发现&#xff0c;GitHub上超过15000个Go模块存储库容易受到一种名为“重新劫持”的攻击。 由于GitHub用户名的更改会造成9000多个存储库容易被重新劫持&#xff0c;同时因为帐户删除&#xff0c;会对6000多个存储库造成重新劫持的危机。目前统计…

华芯微特|MCU之TIMER输入捕获

引言 华芯微特公司SWM系列单片机提供的TIMER个数和功能有些微差别&#xff0c;为了让您更加简单的使用这一功能&#xff0c;下面小编将以SWM190为例&#xff0c;我们今天详细讲解一下TIMER的输入捕获功能。 TIMER输入捕获 一、TIMER定时器之输入捕获功能 我们今天详细讲解一下…