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 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)//抛出异常
            }
        }
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于Java+SpringBoot+vue+elementUI私人健身教练预约管理系统设计实现

基于JavaSpringBootvueelementUI私人健身教练预约管理系统设计实现 欢迎点赞 收藏 ⭐留言 文末获取源码联系方式 文章目录 基于JavaSpringBootvueelementUI私人健身教练预约管理系统设计实现一、前言介绍&#xff1a;二、系统设计&#xff1a;2.1 性能需求分析2.2 B/S架构&…

Baumer工业相机堡盟工业相机如何通过NEOAPI SDK实现相机掉线自动重连(C#)

Baumer工业相机堡盟工业相机如何通过NEOAPI SDK实现相机掉线自动重连&#xff08;C#&#xff09; Baumer工业相机Baumer工业相机的掉线自动重连的技术背景通过PnP事件函数检查Baumer工业相机是否掉线在NEOAPI SDK里实现相机掉线重连方法&#xff1a;工业相机掉线重连测试演示图…

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

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

优雅地展示20w单细胞热图|非Doheatmap 超大数据集 细胞数太多

单细胞超大数据集的热图怎么画&#xff1f;昨天刚做完展示20万单细胞的热图要这么画吗&#xff1f; 今天就有人发消息问我为啥他画出来的热图有问题。 问题起源 昨天分享完 20万单细胞的热图要这么画吗&#xff1f;&#xff0c;就有人问为啥他的数据会出错。我们先来看下他的…

CMU15-445-Spring-2023-Project #0 - C++ Primer

前置任务。 Task #1 - Copy-On-Write Trie Copy-on-write (COW) Trie 在进行修改时&#xff0c;不会立即复制整个数据结构。相反&#xff0c;它会在需要修改的节点被多个引用的时候才进行复制。当要对某个节点进行写操作&#xff08;添加子节点或者继续向下insert&#xff09…

FLASH 闪存-stm32入门

本节我们学习的内容是 STM32 的 FLASH&#xff0c;闪存。 当然闪存是一个通用的名词&#xff0c;表示的是一种非易失性&#xff0c;也就是掉电不丢失的存储器。比如&#xff0c;我们之前学习 SPI 的时候&#xff0c;用的 W25Q64 芯片&#xff0c;就是一种闪存存储器芯片。 而…

【QML】与 C++ 混合编程:互相调用函数

文章目录 qml 调用 C 函数案例 a&#xff1a;Q_INVOKABLE 标记 C 函数 视图设置进 qml 属性案例 b&#xff1a;qml 通过发送信号的方式&#xff0c;调用 Qt 槽函数 C调用qml函数 qml 调用 C 函数 qml 要使用 C 的函数有两个方法&#xff1a; 一种是&#xff0c;用 Q_INVOKABLE…

守护进程“独辟蹊径”

守护进程“独辟蹊径” 一、前言二、实际运用2.1 知识介绍2.2 单机库场景应用2.2.1 配置dmwatcher.ini2.2.2 注册后台守护服务2.2.3 配置dmmal.ini2.2.4 配置归档和守护OGUID2.2.5 开启mal2.2.6 启动守护2.2.7 测试dmserver异常退出 三、总结 DM技术交流QQ群&#xff1a;9401242…

数据结构—环形缓冲区

写在前面&#xff0c;2023年11月开始进入岗位&#xff0c;工作岗位是嵌入式软件工程师。2024年是上班的第一年的&#xff0c;希望今年收获满满&#xff0c;增长见闻。 数据结构—环形缓冲区 为什么要使用环形数组&#xff0c;环形数组比起原来的常规数组的优势是什么&#xf…

Windows 10系统用Xlight FTP搭建SFTP服务器

步骤&#xff1a; 1.安装SFTP服务器 刚开始我使用的是freeSSHd&#xff0c;后面发现由于公司网络原因&#xff0c;打不开这个软件&#xff0c;改成了使用Xlight FTP&#xff0c; 官网下载链接&#xff1a;Xlight FTP 服务器 - 下载免费的windows FTP 服务器 Xlight FTP有30…

LocalSend 开源跨平台的局域网文件互传工具

如果您需要在多平台设备之间进行文件传输&#xff0c;例如从Windows电脑到安卓手机&#xff0c;或者从安卓手机到macOS&#xff0c;通常会使用聊天工具或者U盘进行传输。为了简化这一过程&#xff0c;推荐使用一款全平台支持的文件共享传输工具&#xff1a;LocalSend。 LocalS…

Qt通过pos()获取坐标信息

背景&#xff1a;这是一个QWidget窗体&#xff0c;里面是各种布局的组合&#xff0c;一层套一层。 我希望得到绿色部分的坐标信息(x,y) QPoint get_pos(QWidget* w, QWidget* parent) {if ((QWidget*)w->parent() parent) {return w->pos();}else {QPoint pos(w->po…

以 Serverfull 方式运行无服务器服务

当前 IT 架构中最流行的用例是从 Serverfull 转向 Serverless 设计。在某些情况下&#xff0c;我们可能需要以 Serverfull 方式设计服务或迁移到 Serverfull 作为运营成本的一部分。 在本文中&#xff0c;我们将展示如何将 Kumologica flow 作为 Docker 容器运行。通常&#x…

alibabaCloud学习笔记01(小滴课堂)

微服务架构常见的核心组件 讲解业务微服务架构常见解决方案 讲解AlibabaCloud核心组件介绍 创建数据库。 建表&#xff1a; 添加数据&#xff1a; 再建个用户库&#xff1a; 建表&#xff1a; 插入数据&#xff1a; 创建订单库&#xff1a; 建表&#xff1a; 创建项目&#x…

基于SpringBoot的旅游网站

目录 前言 开发环境以及工具 项目功能介绍 用户端&#xff1a; 管理端&#xff1a; 详细设计 用户端首页 登录页面 管理端页面 源码获取 前言 本项目是一个基于IDEA和Java语言开发基于SpringBoot的旅游网站。应用包含管理端和用户端等多个功能模块。 改革开放以来&am…

redis 三主六从高可用dockerswarm高级版(不固定ip)

redis集群(cluster)笔记 redis 三主三从高可用集群docker swarm redis 三主六从高可用docker(不固定ip) redis 三主六从高可用dockerswarm高级版(不固定ip) 此博客解决&#xff0c;redis加入集群后&#xff0c;是用于停掉后重启&#xff0c;将nodes.conf中的旧的Ip替换为新的…

【机器学习】卷积神经网络(五)-计算机视觉应用

七、应用-计算机视觉 7.1 人脸检测 DenseBox\Femaleness-Net\MT-CNN\Cascade CNN 介绍 VJ框架的分类器级联用于卷积网络 用于人脸检测的紧凑卷积神经网络级联 问题&#xff1a;作者希望实时检测高分辨率视频流中的正面&#xff0c;由于人脸图像和背景的多样性和复杂性&#xff…

Godot4.2——爬虫小游戏简单制作

目录 一、项目 二、项目功能 怪物 人物 快捷键 分数 游戏说明 提示信息 三、学习视频 UI制作 游戏教程 四、总结 一、项目 视频演示&#xff1a;Godot4爬虫小游戏简单制作_哔哩哔哩bilibili 游戏教程&#xff1a;【小猫godot4入门教程 C#版 已完结】官方入门案例 第…

【人工智能】百度智能云千帆AppBuilder,快速构建您的专属AI原生应用

大家好&#xff0c;我是全栈小5&#xff0c;欢迎来到《小5讲堂》&#xff0c;此序列是《人工智能》专栏文章。 这是2024年第5篇文章&#xff0c;此篇文章是进行人工智能相关的实践序列文章&#xff0c;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&…

ResNet论文阅读和简单实现

论文&#xff1a;https://arxiv.org/pdf/1512.03385.pdf Deep Residual Learning for Image Recognition 本模块主要是阅读论文&#xff0c;会做简单的翻译&#xff08;至少满足我自己能看明白&#xff09;。 Introduction 由上图可见&#xff0c;在20层和56层的网络上训练的…