Swift并发的结构化编程

news2024/11/25 20:20:44

并发(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 in
    load_image(url) { data in
        resize_image(data) { data in
            show_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] in
	let 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 == Never
    public 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 in
        DispatchQueue.global().async {
            do {
                let result = try chuanFunc()
                continuation.resume(returning: result)//返回结果
            } catch {
                continuation.resume(throwing: error)//抛出异常
            }
        }
    }
}

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

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

相关文章

TDengine 荣获 2023 Frost Sullivan 客户价值领导力奖

近日&#xff0c;TDengine 被国际知名咨询公司沙利文&#xff08;Frost & Sullivan&#xff09;评为全球最佳工业数据管理解决方案&#xff0c;赢得了 2023 年客户价值领导力奖&#xff08;Frost & Sullivan duoxie&#xff09;&#xff0c;该奖项重点关注引领行业创新…

S32K312软件看门狗之Software Watchdog Timer (SWT)

S32DS的SDK中提供了Wdg&#xff0c;是属于MCAL层的&#xff0c;配置有点复杂&#xff0c;还需要以来Gpt、Mcu和Platform框架里的东西&#xff0c;配置到已经开发好的工程中还容易出现配置问题。本文主要讲解Software Watchdog Timer (SWT)的软件看门狗配置和使用示例&#xff0…

易点易动固定资产管理系统:集成飞书,助力企业全生命周期固定资产管理

易点易动固定资产管理系统&#xff1a;集成飞书&#xff0c;助力企业全生命周期固定资产管理 在现代商业环境中&#xff0c;固定资产管理对企业的运营和发展至关重要。为了提高管理效率和降低成本&#xff0c;我们引入了易点易动固定资产管理系统&#xff0c;该系统集成了飞书&…

技术学习|CDA level I 描述性统计分析(数据的描述性统计分析)

技术学习|CDA level I 描述性统计分析&#xff08;数据的描述性统计分析&#xff09; 数据的描述性统计分析常从数据的集中趋势、离散程度和分布形态3个方面进行。 一、集中趋势 集中趋势是指数据向其中心值靠拢的趋势。测量数据的集中趋势&#xff0c;主要是寻找其中心值。…

聚“工匠”建“双城”,《天府工匠》第二季如何为“双城经济圈”助力?

文&#xff5c;新熔财经 作者&#xff5c;和花 被火烧过破损不堪&#xff0c;还受了潮粘连在一起的古籍书页&#xff0c;如何快速被修复&#xff1f;直径只有0.05—0.07毫米的头发丝上&#xff0c;如何清晰不粘连雕刻出“心无旁骛”&#xff0c;还要确保头发丝不断裂&#xf…

Sectigo泛域名https证书有什么用

Sectigo旗下有泛域名https证书实现了同时为多个域名网站提供安全加密服务&#xff0c;虽然将域名网站的类型限制在了域名以及域名旗下的二级子域名中。Sectigo旗下的泛域名https证书分为DV基础型和OV企业型&#xff0c;提高了https证书对各个场景的适配。今天就随SSL盾小编了解…

主流桌面浏览器Chrome,FireFox和Edge等如何禁用弹出式窗口阻止程序,这里有详细步骤

为什么你想知道如何禁用浏览器中的弹出式窗口阻止程序?毕竟,弹出式窗口是网络的祸害:显示烦人的广告、虚假的安全消息和其他刺激,会分散你的浏览注意力,甚至可能包含恶意代码。 所有主要的桌面浏览器现在都默认阻止弹出式窗口,那么你到底为什么要取消阻止这些害虫呢?事…

自制数据库空洞率清理工具-C版-02-EasyClean-V1.1(支持南大通用数据库Gbase8a)

一、环境信息 名称值CPUIntel(R) Core(TM) i5-1035G1 CPU 1.00GHz操作系统CentOS Linux release 7.9.2009 (Core)内存3G逻辑核数2Gbase8a版本8.6.2-R43.34.27468a27EasyClean版本V1.1 二、简述 工作和兴趣相结合的产物&#xff0c;既能更好的完成工作&#xff0c;也能看看自…

springCould中的Hystrix【上】-从小白开始【7】

目录 1.简单介绍❤️❤️❤️ 2.主要功能 ❤️❤️❤️ 3.正确案例❤️❤️❤️ 4.使用jmeter压测 ❤️❤️❤️ 5.建模块 80❤️❤️❤️ 6.如何解决上面问题 ❤️❤️❤️ 7.对8001进行服务降级❤️❤️❤️ 8.对80进行服务降级 ❤️❤️❤️ 9.通用降级方法❤️❤️…

win10提示“KBDSF.DLL文件缺失”,游戏或软件无法启动运行,快速修复方法

很多用户在日常使用电脑的时候&#xff0c;或多或少都遇到过&#xff0c;在启动游戏或软件的时候&#xff0c;Windows桌面会弹出错误提示框“KBDSF.DLL文件缺失&#xff0c;造成软件无法启动或运行&#xff0c;请尝试重新安装解决”。 首先&#xff0c;先来了解DLL文件是什么&a…

【计算机毕业设计】SSM场地预订管理系统

项目介绍 本项目分为前后台&#xff0c;前台为普通用户登录&#xff0c;后台为管理员登录&#xff1b; 用户角色包含以下功能&#xff1a; 按分类查看场地,用户登录,查看网站公告,按分类查看器材,查看商品详情,加入购物车,提交订单,查看订单,修改个人信息等功能。 管理员角…

急急急!直接从压缩包打开文件,保存后再打开却找不到了怎么办???

这是我今天发生的蠢事&#xff0c;好险&#xff0c;改了一个上午的word文档&#xff0c;因为word突然未响应&#xff0c;强制关闭后再打开word文档找不到了才想起来自己没有解压缩就。。。。 &#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#…

绿色环保之选:探索智慧公厕公司厂家的可持续发展策略

随着城市化的不断推进&#xff0c;如何在城市中创造更为宜居的环境成为了当今社会亟待解决的问题。在这个背景下&#xff0c;智慧公厕公司厂家应运而生&#xff0c;不仅通过引入创新技术提升了公共卫生水平&#xff0c;更以其独特的可持续发展策略成为了绿色环保之选。 符合智慧…

未来十年不变的AI是什么?吴恩达等专家关于2024年AI发展趋势的预测

随着2024年的到来&#xff0c;人工智能领域正迎来前所未有的变革和发展。从深度学习到自然语言处理&#xff0c;AI技术的每一个分支都在经历着快速的进步。在这个关键的时刻&#xff0c;业界专家们提出了对未来趋势的深刻洞察&#xff0c;预测了将形成AI发展主流的关键方向。智…

DolphinScheduler实际应用

前言 最近公司新启动了一个项目&#xff0c;然后领导想用一下新技术&#xff0c;并且为公司提供多个大数据调度解决方案&#xff0c;我呢就根据领导要求调研了下当前的开源调度工具&#xff0c;最终决定采用DolphinScheduler&#xff0c; 因此研究了一下DolphinScheduler &…

JSON网络令牌JWT

1.什么是身份验证 日常生活中的身份验证的场景: 比如进入公司的大楼时&#xff0c;需要携带工牌&#xff1b;打卡上班时&#xff0c;需要指纹识别&#xff1b;打开工作电脑时&#xff0c;需要输入密码。 2. 什么是 JSON 网络令牌&#xff1f; JSON Web Token (JWT) 是一个开…

大数据技术在民生资金专项审计中的应用

一、应用背景 目前&#xff0c;针对审计行业&#xff0c;关于大数据技术的相关研究与应用一般包括大数据智能采集数据技术、大数据智能分析技术、大数据可视化分析技术以及大数据多数据源综合分析技术。其中&#xff0c;大数据智能采集数据技术是通过网络爬虫或者WebService接…

【数据结构和算法】小行星碰撞

其他系列文章导航 Java基础合集数据结构与算法合集 设计模式合集 多线程合集 分布式合集 ES合集 文章目录 其他系列文章导航 文章目录 前言 一、题目描述 二、题解 2.1 什么情况会用到栈 2.2 方法一&#xff1a;模拟 栈 三、代码 3.1 方法一&#xff1a;模拟 栈 四…

单轴PSO视觉飞拍与精准输出:EtherCAT超高速实时运动控制卡XPCIE1032H上位机C#开发(七)

XPCIE1032H功能简介 XPCIE1032H是一款基于PCI Express的EtherCAT总线运动控制卡&#xff0c;可选6-64轴运动控制&#xff0c;支持多路高速数字输入输出&#xff0c;可轻松实现多轴同步控制和高速数据传输。 XPCIE1032H集成了强大的运动控制功能&#xff0c;结合MotionRT7运动…

CRYPTO现代密码学学习

CRYPTO现代密码学学习 RC4 加密算法RSA加密解密DES加密解密详解密钥的生成密文的生成 RC4 加密算法 简单介绍&#xff1a;RC4加密算法是一种对称加密算法&#xff0c;加密和解密使用同一个函数 初始化分为以下几个步骤 初始化存储0-255字节的Sbox(其实就是一个数组)填充key到…