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.rhkb.cn/news/230249.html

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

相关文章

【单片机 TB作品】节拍器,电子音乐节拍器,51单片机,Proteus仿真

节拍器的使用可以使练琴者正确掌握乐曲的速度,从而使音 乐练习达到事半功倍的效果。本课题基于单片机设计具有声光晋 示的电子乐器节拍器,充分利用单片机的定时和中断系统,通过 C语言程序设计,控制外部相关硬件电路,实现对音乐速,度 40~120次/分钟范围内连续可调,节拍114、 2/4…

【Electron】快速建立Vue3+Vite+Electron Ts项目

git https://github.com/electron-vite/electron-vite-vue 创建项目 npm create electron-vite or pnpm create electron-vite 初始化 pnpm install or pnpm i 启动项目 pnpm dev 打包项目 pnpm build 项目创建成功后默认情况下 窗口是H800 W600 在createWindow 函数…

【privateGPT】使用privateGPT训练您自己的LLM

了解如何在不向提供商公开您的私人数据的情况下训练您自己的语言模型 使用OpenAI的ChatGPT等公共人工智能服务的主要担忧之一是将您的私人数据暴露给提供商的风险。对于商业用途&#xff0c;这仍然是考虑采用人工智能技术的公司最大的担忧。 很多时候&#xff0c;你想创建自己…

Golang拼接字符串性能对比

g o l a n g golang golang的 s t r i n g string string类型是不可修改的&#xff0c;对于拼接字符串来说&#xff0c;本质上还是创建一个新的对象将数据放进去。主要有以下几种拼接方式 拼接方式介绍 1.使用 s t r i n g string string自带的运算符 ans ans s2. 使用…

flex布局中滚动条展示内容时部分内容无法显示

这段时间看了一下之前的demo&#xff0c;发现了当时记录了一句 justify-content: center; 会影响滚动条内容展示&#xff0c;觉得还是记录一下 情况复现 这里我简单的写一下demo复现一下这个问题&#xff0c;如下&#xff1a; <!DOCTYPE html> <html lang"en&quo…

python django 生鲜商城管理系统

python django 生鲜商城管理系统,包含用户端和管理端 功能&#xff1a; 用户端&#xff1a;商城主页展示&#xff0c;登录&#xff0c;注册&#xff0c;用户中心&#xff0c;购物车&#xff0c;我的订单&#xff0c;购物车结算 管理端&#xff1a;登录&#xff0c;商品&…

QT上位机开发(绘图软件)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 稍微复杂一点的软件&#xff0c;一般都是带有绘图功能。绘图的意义&#xff0c;不仅仅是像CAD一样&#xff0c;可以进行模型的设计、比对和调试。它…

学习调整echarts中toolbox位置toolBox工具栏属性

学习调整echarts中toolbox位置toolBox工具栏属性 toolbox工具栏属性介绍示例代码代码参数说明 toolbox工具栏属性介绍 参考网址&#xff1a;https://echarts.apache.org/zh/option.html#tooltip 属性类型说明toolbox.showbooleanboolean 默认值为true&#xff0c;是否显示工具…

自由DIY预约小程序源码系统:适用于任何行业+自由DIY你的界面布局+全新升级的UI+多用户系统 带安装部署教程

随着移动互联网的普及&#xff0c;预约服务逐渐成为人们日常生活的一部分。从家政服务、医疗挂号到汽车保养&#xff0c;预约已经渗透到各个行业。然而&#xff0c;市面上的预约小程序大多功能单一&#xff0c;界面老旧&#xff0c;无法满足商家和用户的个性化需求。今天来给大…

Oracle regexp_replace 手机号脱敏

select 18012345678,regexp_replace(18012345678,(.){4},****,4,1) from dual;

python实现图像的二维傅里叶变换——冈萨雷斯数字图像处理

原理 二维傅里叶变换是一种在图像处理中常用的数学工具&#xff0c;它将图像从空间域&#xff08;我们通常看到的像素排列&#xff09;转换到频率域。这种变换揭示了图像的频率成分&#xff0c;有助于进行各种图像分析和处理&#xff0c;如滤波、图像增强、边缘检测等。 在数学…

第3课 使用FFmpeg获取并播放音频流

本课对应源文件下载链接&#xff1a; https://download.csdn.net/download/XiBuQiuChong/88680079 FFmpeg作为一套庞大的音视频处理开源工具&#xff0c;其源码有太多值得研究的地方。但对于大多数初学者而言&#xff0c;如何快速利用相关的API写出自己想要的东西才是迫切需要…

【PowerMockito:编写单元测试过程中采用when打桩失效的问题】

问题描述 正如上图所示&#xff0c;采用when打桩了&#xff0c;但是&#xff0c;实际执行的时候还是返回null。 解决方案 打桩时直接用any() 但是这样可能出现一个mybatisplus的异常&#xff0c;所以在测试类中需要加入以下代码片段&#xff1a; Beforepublic void setUp() …

AI智能分析网关V4区域人数超员算法模型的应用原理及使用场景

视频AI智能分析技术已经深入到人类生活的各个角落&#xff0c;与社会发展的方方面面紧密相连。从日常生活中的各种场景&#xff0c;如人脸识别、车牌识别&#xff0c;到工业生产中的安全监控&#xff0c;如工厂园区的翻越围栏识别、入侵识别、工地的安全帽识别、车间流水线产品…

Laravel的知识点

1 、 {{ }}和{!! !!} 的区别 1&#xff09;{{ }} : 未解析直接输出&#xff08; 是在 HTML 中内嵌 PHP 的 Blade 语法标识符&#xff0c;表示包含在该区块内的代码都将使用 PHP 来编译运行&#xff09; 2&#xff09;{!! !!} : 若变量值含有HTML标签将解析成前端代码 2.两种写…

大模型实战营第二期——1. 书生·浦语大模型全链路开源开放体系

文章目录 1. 实战营介绍2. 书生浦语大模型介绍2.1 数据2.2 预训练2.3 微调2.4 评测2.5 部署2.6 智能体(应用) 1. 实战营介绍 github链接&#xff1a;https://github.com/internLM/tutorialInternLM&#xff1a;https://github.com/InternLM书生浦语官网&#xff1a;https://in…

西电期末1017.有序序列插值

一.题目 二.分析与思路 简单题。主要考察简单的排序&#xff0c;最后的插入数据同样不用具体实现&#xff0c;只需在输出时多输出一下即可&#xff0c;注意顺序&#xff01;&#xff01; 三.代码实现 #include<bits/stdc.h>//万能头 int main() {int n;scanf("%d…

【数据结构】二叉树(一)——树和二叉树的概念及结构

前言: 本篇博客主要了解什么是树&#xff0c;什么是二叉树&#xff0c;以及他们的概念和结构。 文章目录 一、树的概念及结构1.1 树的基本概念1.2 树的相关特征1.3 树的实现 二、二叉树的概念及性质2.1 二叉树的概念2.2 二叉树的性质 一、树的概念及结构 1.1 树的基本概念 树&…

Java技术栈 —— Redis的雪崩、穿透与击穿

Java技术栈 —— Redis的雪崩、穿透与击穿 〇、实验的先导条件&#xff08;NginxJmeter&#xff09;一、Redis缓存雪崩、缓存穿透、缓存击穿1.1 雪崩1.2 穿透1.3 击穿 二、Redis应用场景——高并发2.1 单机部署的高并发问题与解决&#xff08;JVM级别锁&#xff09;2.2 集群部署…

快速搭建知识付费小程序,3分钟即可开启知识变现之旅

产品服务 线上线下课程传播 线上线下活动管理 项目撮合交易 找商机找合作 一对一线下交流 企业文化宣传 企业产品销售 更多服务 实时行业资讯 动态学习交流 分销代理推广 独立知识店铺 覆盖全行业 个人IP打造 独立小程序 私域运营解决方案 公域引流 营销转化 …