Swift中的异步编程方式

简介: 说到异步编程,我们很容易想到的编译回调。无论是需要并行的耗时任务,还是允许串行的简单任务,都通过回调的方式返回结果。回调也是在开发中使用最为广泛的一种异步编程方式。回想一下,通常的网络请求,文件操作等函数都会提供一个回调参数。回调使用起来虽然方便,但其并不利于进行程序流程的控制,仅仅从代码层面看,也很难组织清楚代码的执行顺序和逻辑。

Swift中的异步编程方式

说到异步编程,我们很容易想到的编译回调。无论是需要并行的耗时任务,还是允许串行的简单任务,都通过回调的方式返回结果。回调也是在开发中使用最为广泛的一种异步编程方式。回想一下,通常的网络请求,文件操作等函数都会提供一个回调参数。回调使用起来虽然方便,但其并不利于进行程序流程的控制,仅仅从代码层面看,也很难组织清楚代码的执行顺序和逻辑。

Swift从代码层面提供了结构化的方式来支持异步编程,在Swift5.5中引入了async和await相关的关键字。需要注意,异步和并行本身是两个概念,在Swift中,异步编程模型已经建立在线程调度之上,这也就是说,我们无需关心其中线程的调用,异步的函数本身就是在子线程中并行执行的,线程切换和调度全有语言本身控制。但是Swift不会保证函数会在哪个特定的线程上执行。

异步函数

在尝试Swift中提供的异步编程方式外,可以先回想下对于异步并行的场景,之前是如何处理的,例如下面的代码:

func test(callback: @escaping (_ success: Bool)->Void) {
   
    DispatchQueue.global().async {
   
        print("Test任务完成", Thread.current)
        callback(true)
    }
}
print("Begin", Thread.current)
test {
    success in
    print("EndTest")
}
print("End", Thread.current)

其中test函数所执行的任务是手动放在全局队列中执行的,DispatchQueue会自动的进行线程的调度,将其分配到空闲的子线程来执行。打印结果如下:

Begin <_NSMainThread: 0x600002310100>{number = 1, name = main}
End <_NSMainThread: 0x600002310100>{number = 1, name = main}
Test任务完成 <NSThread: 0x600002300300>{number = 5, name = (null)}
EndTest

上面的示例代码比较简单,如果有更多的异步任务是依赖test任务的,则闭包回调的写法就会变得非常丑陋,代码难以阅读和维护。

在Swift5.5之后,我们可以使用async关键字来定义异步函数,编程模型会自动分配线程执行,例如:

func test1() async -> Bool {
   
    print("ts1", Thread.current)
    return true
}

func test2() async -> Bool {
   
    print("ts2", Thread.current)
    return true
}

print("Begin", Thread.current)
async let a = test1()
async let b = test2()
print("End", Thread.current)

上面代码中,async关键字将函数标记为异步的,异步函数是一种特殊的函数,其支持在执行过程中被暂时的挂起,即暂停。对于普通的函数来说,会有3种状态:

1. 执行完成

2. 抛出异常

3. 永不返回

异步函数对应的也会有这3种状态,不同的是,当需要做某些等待操作时,其可以暂时的挂起。运行上面的代码,打印效果如下:

Begin <_NSMainThread: 0x60000329c3c0>{number = 1, name = main}
End <_NSMainThread: 0x60000329c3c0>{number = 1, name = main}
ts1 <NSThread: 0x60000328cc40>{number = 4, name = (null)}
ts2 <NSThread: 0x600003282d40>{number = 6, name = (null)}

可以看到,test1和test2两个任务是异步执行,且被分配在了不同的线程。需要注意,理论上在异步函数中是不允许使用Thread相关接口的,因为任务的挂起和恢复所在线程都是由系统调度的,逻辑上开发者无需关心线程问题,在Swift6版本中继续这样使用将会报错。

上面演示的代码中,test1和test2任务的执行并不依赖关系,如果test2和执行是需要test1执行结束的,则可以直接使用await关键字来将运行挂起,直到结果返回。例如:

func test1() async -> Bool {
   
    print("ts1", Thread.current)
    return true
}

func test2() async -> Bool {
   
    print("ts2", Thread.current)
    return true
}

print("Begin", Thread.current)
let a = await test1()
let b = await test2()
print("End", Thread.current)

打印效果如下:

Begin <_NSMainThread: 0x600002180140>{number = 1, name = main}
ts1 <NSThread: 0x600002198100>{number = 6, name = (null)}
ts2 <NSThread: 0x6000021accc0>{number = 8, name = (null)}
End <_NSMainThread: 0x600002180140>{number = 1, name = main}

使用await关键字标记的地方为程序的挂起点,此时会停止当前线程上代码的执行,并等待异步函数的返回,在程序中,支持await进行挂起的场景包括:

1.异步的方法,属性或函数中。

2.main代码块中。

3.非结构化的子任务代码块中。

通常,我们直接使用await调用异步函数时,当前执行会被挂起,更多时候可以使用如下方式来同时执行多个异步函数,使用await来最终获得结果:

func test1() async -> Bool {
   
    print("ts1", Thread.current)
    return true
}

func test2() async -> Bool {
   
    print("ts2", Thread.current)
    return true
}

print("Begin", Thread.current)
async let a = test1()
async let b = test2()
let res = await [a ,b]
print(res)
print("End", Thread.current)

异步序列

Swift中的迭代也支持异步返回,通过AsyncIteratorProtocol协议来定义异步的迭代器,示例如下:

struct Group: AsyncSequence {
   
    typealias Element = Int

    let limit: Int

    struct AsyncIterator : AsyncIteratorProtocol {
   
        let limit: Int
        var current = 1
        mutating func next() async -> Int? {
   
            guard !Task.isCancelled else {
   
                return nil
            }

            guard current <= limit else {
   
                return nil
            }

            let result = current
            current += 1
            return result
        }
    }

    func makeAsyncIterator() -> AsyncIterator {
   
        return AsyncIterator(limit: limit)
    }
}
print("Begin")
let group = Group(limit: 10)

for await i in group {
   
    print(i)
}
print("End")

在对异步迭代器进行遍历时,需要使用for await in的语法方式。

任务组与任务

当有多个异步任务需要执行时,可以将其添加到一个任务组中,当任务组所有任务完成后再进行统一的返回。例如:

let res = await withTaskGroup(of: Bool.self, returning: [Bool].self, body: {
    taskGroup in
    taskGroup.addTask {
   
        let a = await test1()
        return a
    }
    taskGroup.addTask {
   
        let b = await test1()
        return b
    }
    var datas:[Bool] = []
    for await data in taskGroup {
   
        datas.append(data)
    }
    return datas
})

print(res)

其中,withTaskGroup方法将创建一个异步的父任务,其中可以添加多个子任务,任务组之间有非常明确的关系,这种编程方式也被称为结构化编程,当然,Swift也提供了非结构化的编程方式,即需要开发者处理任务之间的关系。这非常有用,有时我们需要在非并发的环境中调用异步函数,例如在iOS Application中ViewController的viewDidLoad方法中调用一个异步的函数,此时就需要为其包装一个并发环境,使用Task来创建任务即可:

class ViewController: UIViewController {
   

    override func viewDidLoad() {
   
        super.viewDidLoad()
        Task(priority: .background) {
   
            await test()
            self.view.backgroundColor = .red
            print("Finish")
        }
        // 上面task的执行不会影响当前线程
        print("Continue")
    }

    func test() async {
   
        try? await Task.sleep(for: .seconds(10))
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
   
        print("touch")
        print(Thread.current)
    }
}

这里再强调一下,所谓执行任务的挂起和线程的阻塞完全不同,当并发环境中当前任务被挂起时,线程资源会被释放去执行其他任务,直到异步任务有结果后,在恢复执行。上面代码中并没有记录Task实例,其实此实例可以控制任务的取消,获取任务返回值等操作,例如:

override func viewDidLoad() {
   
    super.viewDidLoad()
    let task = Task(priority: .background) {
   
        await test()
        self.view.backgroundColor = .red
        print("Finish")
        return "result"
    }
    // 上面task的执行不会影响当前线程
    print("Continue")

    // 取消任务
    task.cancel()
}
目录
相关文章
|
4月前
|
API Swift C语言
探索iOS开发:Swift中的异步编程与GCD应用
【8月更文挑战第4天】在iOS开发的海洋中,掌握Swift语言的航向是至关重要的。本文将引领你深入理解Swift中的异步编程概念,并借助Grand Central Dispatch(GCD)这一强大的工具,来简化并发编程的复杂性。我们将通过实际代码示例,展现如何在iOS应用中高效地管理后台任务和提升用户界面的响应性。
85 3
|
7月前
|
安全 Swift 开发者
【Swift开发专栏】Swift中的异步编程与性能提升
【4月更文挑战第30天】本文探讨了Swift中的异步编程,旨在提升应用性能。首先,介绍了异步编程的基本概念,强调其在处理并发任务和提高响应性中的重要性。接着,阐述了Swift中的异步模型,包括回调函数、Promises以及新引入的`async/await`和`Task`结构体。最后,讨论了通过避免主线程阻塞、合理使用线程和并发优化等策略来提升性能。Swift的异步模型为开发者提供了更简洁、安全的异步编程方式,有助于构建高效应用程序。
84 1
|
7月前
|
安全 编译器 Swift
IOS开发基础知识: 对比 Swift 和 Objective-C 的优缺点。
IOS开发基础知识: 对比 Swift 和 Objective-C 的优缺点。
411 2
|
5月前
|
Unix 调度 Swift
苹果iOS新手开发之Swift 中获取时间戳有哪些方式?
在Swift中获取时间戳有四种常见方式:1) 使用`Date`对象获取秒级或毫秒级时间戳;2) 通过`CFAbsoluteTimeGetCurrent`获取Core Foundation的秒数,需转换为Unix时间戳;3) 使用`DispatchTime.now()`获取纳秒级精度的调度时间点;4) `ProcessInfo`提供设备启动后的秒数,不表示绝对时间。不同方法适用于不同的精度和场景需求。
159 3
|
12天前
|
安全 Swift iOS开发
Swift 与 UIKit 在 iOS 应用界面开发中的关键技术和实践方法
本文深入探讨了 Swift 与 UIKit 在 iOS 应用界面开发中的关键技术和实践方法。Swift 以其简洁、高效和类型安全的特点,结合 UIKit 丰富的组件和功能,为开发者提供了强大的工具。文章从 Swift 的语法优势、类型安全、编程模型以及与 UIKit 的集成,到 UIKit 的主要组件和功能,再到构建界面的实践技巧和实际案例分析,全面介绍了如何利用这些技术创建高质量的用户界面。
21 2
|
28天前
|
Swift iOS开发 UED
如何使用Swift和UIKit在iOS应用中实现自定义按钮动画
本文通过一个具体案例,介绍如何使用Swift和UIKit在iOS应用中实现自定义按钮动画。当用户点击按钮时,按钮将从圆形变为椭圆形,颜色从蓝色渐变到绿色;释放按钮时,动画以相反方式恢复。通过UIView的动画方法和弹簧动画效果,实现平滑自然的过渡。
50 1
|
2月前
|
Swift iOS开发 UED
如何使用Swift和UIKit在iOS应用中实现自定义按钮动画
【10月更文挑战第18天】本文通过一个具体案例,介绍如何使用Swift和UIKit在iOS应用中实现自定义按钮动画。当用户按下按钮时,按钮将从圆形变为椭圆形并从蓝色渐变为绿色;释放按钮时,动画恢复原状。通过UIView的动画方法和弹簧动画效果,实现平滑自然的动画过渡。
52 5
|
4月前
|
存储 移动开发 Swift
使用Swift进行iOS应用开发:探索现代移动开发的魅力
【8月更文挑战第12天】使用Swift进行iOS应用开发,不仅能够享受到Swift语言带来的简洁、快速、安全的编程体验,还能够充分利用iOS平台提供的丰富资源和强大功能。然而,iOS应用开发并非易事,需要开发者具备扎实的编程基础、丰富的实践经验和不断学习的精神。希望本文能够为您的iOS应用开发之旅提供一些有益的参考和帮助。
|
5月前
|
Swift iOS开发 Kotlin
苹果iOS新手开发之Swift中实现类似Kotlin的作用域函数
Swift可通过扩展实现类似Kotlin作用域函数效果。如自定义`let`, `run`, `with`, `apply`, `also`,增强代码可读性和简洁性。虽无直接内置支持,但利用Swift特性可达成相似功能。
76 7
|
5月前
|
调度 Swift Android开发
苹果iOS新手开发之Swift中的并发任务和消息机制
Swift的消息机制类似Android的Handler,实现任务调度有三种方式: 1. **Grand Central Dispatch (GCD)**:使用`DispatchQueue`在主线程或后台线程执行任务。 2. **OperationQueue**:提供高级接口管理`Operation`对象。 3. **RunLoop**:处理事件如输入源、计时器,类似Android的`Looper`和`Handler`。 **示例**: - GCD:在不同线程执行代码块。 - OperationQueue:创建操作并执行。 - RunLoop:用Timer添加到RunLoop中。
102 2