onevcat / OneV-s-Den-Comments

0 stars 0 forks source link

2021/07/swift-concurrency/ #18

Open utterances-bot opened 3 years ago

utterances-bot commented 3 years ago

Swift 并发初步 | OneV's Den

本文是我的新书《Swift 异步和并发》中第一章的初稿,主要从概览的方向上介绍了 Swift 5.5 中引入的 Swift 并发特性的使用方法。这本书籍正在预购阶段,我也正随着 Xcode 13 和 Swift 5.5 的 beta 发布对剩余章节“锐意撰写”中,预计在今年晚些时候 Swift 5.5 正式发布后发

https://onevcat.com/2021/07/swift-concurrency/

hjw6160602 commented 3 years ago

瞄大yyds 顺便问下 Xcode 13 beta 2 环境 安装最新的 Swift 5.5 toolchain 写出来的 并发代码可以跑在iOS14以下的系统上吗?

onevcat commented 3 years ago

瞄大yyds 顺便问下 Xcode 13 beta 2 环境 安装最新的 Swift 5.5 toolchain 写出来的 并发代码可以跑在iOS14以下的系统上吗?

并不可以 😂

HideOnBushTuT commented 3 years ago

那完全没有动力去学啊

onevcat commented 3 years ago

如果是 iOS 15 才能用上的话,确实没动力..不过社区一直在吵要不要back port到之前的版本 (比如 iOS 12)。Xcode 的 release note 里是把 “from iOS 15” 这件事情列在 Known issue 里了,Core Team 和 Darwin 的人也知道社区对这个的不满,但是他们表示工作量非常大,不能给什么承诺或者时间表。

所以说其实还有转机...看 Apple 要不要强迫员工加班加到死了 😂

hjw6160602 commented 3 years ago

那完全没有动力去学啊

这是你不学习的借口吗?[加油]

hjw6160602 commented 3 years ago

如果是 iOS 15 才能用上的话,确实没动力..不过社区一直在吵要不要back port到之前的版本 (比如 iOS 12)。Xcode 的 release note 里是把 “from iOS 15” 这件事情列在 Known issue 里了,Core Team 和 Darwin 的人也知道社区对这个的不满,但是他们表示工作量非常大,不能给什么承诺或者时间表。

翻喵大之前微博看到了 不过这玩意儿 等等 要真能从iOS12开始支持确实太棒了

fatbobman commented 3 years ago

期待尽早完稿!

anchao-lu commented 3 years ago

真要 15 才能用的话,会很难受的,光看着好,就是不能用

jacksonon commented 3 years ago

不能像以前的运行时库一起从Apple store下载么?

taosiyu commented 3 years ago

哈哈哈,不错,可以来个优惠码

is0bnd commented 3 years ago

假如某个异步任务需要在某种未完成情况下取消,async/await 是否提供了中途取消的机制,需要怎么做?

onevcat commented 3 years ago

假如某个异步任务需要在某种未完成情况下取消,async/await 是否提供了中途取消的机制,需要怎么做?

Swift 并发采用的是协作式的取消方式,从而满足结构化并发的要求。单纯的 Task.cancel 调用只是将 Task 的取消标识设为 true,并不会终止 (也无法终止) 任务的运行。实际的取消依赖异步函数的具体实现。在书中的话,会有关于这个话题一整章的详细展开。

venn0126 commented 3 years ago

那是不是可以理解为Task算是异步模型的一个高层抽象,对于取消的实际操作还是依靠异步功能函数来实现,而异步函数对于已经在执行中的无法取消,而是对于等待或者排队中的任务才可以取消?

cache0928 commented 3 years ago
class Person {
  var name: String
  let birthDate: Date

  init(name: String, birthDate: Date) {
    self.name = name
    self.birthDate = birthDate
  }
}

actor BankAccount {
  var owners: [Person] = [Person(name: "", birthDate: Date.now)]
  func primaryOwner() -> Person? { return owners.first }
}

let account = BankAccount()
Task.detached(priority: .background) {
  if let primary = await account.primaryOwner() {
    primary.name = "The Honorable " + primary.name  // problem: concurrent mutation of actor-isolated state      
    print(primary.name)
  }
}

上面这段代码,跨actor访问了非Sendable引用类型并修改了它的属性,按照SE-0306的描述的话编译器应该会报错,但是我在Xcode 13 Beta 5中却能正常运行,是我理解的问题吗?

onevcat commented 3 years ago

@venn0126

那是不是可以理解为Task算是异步模型的一个高层抽象

Task 主要是一套为了实践结构化并发搞的 API。在这个角度上,你可以说 Task 是对异步模型 (主要是结构化并发) 的一个高层抽象,但是它和异步函数的关系仅仅只是后者提供了工具。。

而异步函数对于已经在执行中的无法取消,而是对于等待或者排队中的任务才可以取消

这是不准确的。

事实上,在结构化并发的 Task API 中,是没有我们理解的所谓的传统的“取消”,或者说任务运行到一半就突然中止了这种事情的。异步操作的生命周期一定和任务的 { } 作用域绑定。

Task API 的取消只是设置一个 cancel 值。异步函数的能力 (或者更精确说,GCD 中新加的 cooperative thread pool 的调度执行的能力),有能力将任务细分为若干个等待和执行的段,在每一段开始前,实现这个异步函数的人有机会检查 cancel 值,并决定要继续执行还是抛出。

当然,这只是在高层级上可以做到的事情。如果能够深入到操作 continuation (比如这个或者这个),针对取消,就会有更多的选择。

onevcat commented 3 years ago

@cache0928

对,现在这个没有按照 proposal 的提案报错,可能是还没有来得及实装。

不过其实关于这个是有一点争论的。Person 作为 class,本来就不应该有并发的数据安全。在 actor BackAccount 中,它的成员 owners 确实是被保护的;而 Person.name 作为 Person 的一员,它的特性应该由 Person 来决定。如果要保护 name,那么应该由开发者明确地把 Person 也做成 actor 或者 Sendable

可能这部分暂时会被作为容忍范围内的问题,会在 Roadmap 提到的第二阶段里再进行实装。

The second phase will enforce full actor isolation

Class component memory can also be accessed from any code that hold a reference to the class. This means that while the reference to the class may be protected by an actor, passing that reference between actors exposes its properties to data races. This also includes references to classes held within value types, when these are passed between actors.

The goal of full actor isolation is to ensure that (this is) protected by default.

cache0928 commented 3 years ago

@cache0928

对,现在这个没有按照 proposal 的提案报错,可能是还没有来得及实装。

不过其实关于这个是有一点争论的。Person 作为 class,本来就不应该有并发的数据安全。在 actor BackAccount 中,它的成员 owners 确实是被保护的;而 Person.name 作为 Person 的一员,它的特性应该由 Person 来决定。如果要保护 name,那么应该由开发者明确地把 Person 也做成 actor 或者 Sendable

可能这部分暂时会被作为容忍范围内的问题,会在 Roadmap 提到的第二阶段里再进行实装。

The second phase will enforce full actor isolation

Class component memory can also be accessed from any code that hold a reference to the class. This means that while the reference to the class may be protected by an actor, passing that reference between actors exposes its properties to data races. This also includes references to classes held within value types, when these are passed between actors.

The goal of full actor isolation is to ensure that (this is) protected by default.

我在Task的构造器@Sendable闭包中直接捕获非Sendable的实例(比如Person的实例)也不会报错,@Sendable闭包不是不能捕获非Sendable成员的吗?

venn0126 commented 3 years ago

翻了一些资料和你的官方API的查看,其实这个“Task or Task Group”其实就是把aync和parallel对高级开发接口的扩充,这也是官方Beta文档中指出的结构化的主题,那么对于你说的“ GCD 中新加的 cooperative thread pool 的调度执行的能力”,是对结构化的底层的支持么?如果是有相关的这方面的说明资料么,如果不是,那对结构化底层的支持是什么?

Wei Wang @.***>于2021年8月19日 周四17:44写道:

@venn0126 https://github.com/venn0126

那是不是可以理解为Task算是异步模型的一个高层抽象

Task 主要是一套为了实践结构化并发 https://250bpm.com/blog:71/index.html搞的 API。在这个角度上,你可以说 Task 是对异步模型 (主要是结构化并发) 的一个高层抽象,但是它和异步函数的关系仅仅只是后者提供了工具。。

而异步函数对于已经在执行中的无法取消,而是对于等待或者排队中的任务才可以取消

这是不准确的。

事实上,在结构化并发的 Task API 中,是没有我们理解的所谓的传统的“取消”,或者说任务运行到一半就突然中止了这种事情的。异步操作的生命周期一定和任务的 { } 作用域绑定。

Task API 的取消只是设置一个 cancel 值。异步函数的能力 (或者更精确说,GCD 中新加的 cooperative thread pool 的调度执行的能力),有能力将任务细分为若干个等待和执行的段,在每一段开始前,实现这个异步函数的人有机会检查 cancel 值,并决定要继续执行还是抛出。

当然,这只是在高层级上可以做到的事情。如果能够深入到操作 continuation (比如这个 https://developer.apple.com/documentation/swift/3814990-withtaskcancellationhandler 或者这个 https://github.com/apple/swift/blob/79d45c0a9689a85142311dbcb1c8d245cbd67334/stdlib/public/Concurrency/TaskSleep.swift#L192-L197 ),针对取消,就会有更多的选择。

— You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub https://github.com/onevcat/OneV-s-Den-Comments/issues/18#issuecomment-901770086, or unsubscribe https://github.com/notifications/unsubscribe-auth/ADSPQ2MPZ2LIV64VE2AH7FDT5THBVANCNFSM47UATMRQ . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&utm_campaign=notification-email .

onevcat commented 3 years ago

Task 只是对 LLVM Builtin 的任务指针和其他一些方法以及 flag (优先级,是否是子任务等) 的封装,GCD 使用这个封装来标记和确认如何在新的 cooperative thread pool 进行调度,而对于 Task 来说,执行这个调度的调度器是一个全局的并行 executor。

async 函数或者 await 等,都只是编译期间的辅助,实际上它们只是表示了这些东西必须跑在一个任务中,或者说,接受全局调度器的调度。因为 cooperative thread pool 调度的特点,调用栈可以在 continuation 之间得到保留,因此返回和抛出都成为可能,结构化并发也就自然实现了。

venn0126 commented 3 years ago

可否是这样理解(非科班出身,请忽略作图美观)

Wei Wang @.***>于2021年8月20日 周五10:37写道:

Task 只是对 LLVM Builtin 的任务指针和其他一些方法以及 flag (优先级,是否是子任务等) 的封装,GCD 使用这个封装来标记和确认如何在新的 cooperative thread pool 进行调度,而对于 Task 来说,执行这个调度的调度器是一个全局的并行 executor。

async 函数或者 await 等,都只是编译期间的辅助,实际上它们只是表示了这些东西必须跑在一个任务中,或者说,接受全局调度器的调度。因为 cooperative thread pool 调度的特点,调用栈可以在 continuation 之间得到保留,因此返回和抛出都成为可能,结构化并发也就自然实现了。

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/onevcat/OneV-s-Den-Comments/issues/18#issuecomment-902385699, or unsubscribe https://github.com/notifications/unsubscribe-auth/ADSPQ2NFD2HQHCKOAUDNDKDT5W5WRANCNFSM47UATMRQ . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&utm_campaign=notification-email .

venn0126 commented 3 years ago

如果看不到请点击这里

onevcat commented 3 years ago

@venn0126 嗯,我的理解差不多是这样..

venn0126 commented 3 years ago

@onevcat 感谢回复❤️

RbBtSn0w commented 3 years ago

官方发文了. 可以了. 书本要畅销了. 来个促销庆祝活动吧

onevcat commented 3 years ago

瞄大yyds 顺便问下 Xcode 13 beta 2 环境 安装最新的 Swift 5.5 toolchain 写出来的 并发代码可以跑在iOS14以下的系统上吗?

并不可以 😂

https://github.com/apple/swift/pull/39051

Core Team 居然真的在发布之前加班加出来了...

hjw6160602 commented 3 years ago

apple/swift#39051 Core Team 居然真的在发布之前加班加出来了...

@onevcat 哈哈 太好了 看到说是macOS的开发都跑去开发iOS了 因为macOS发布时间普遍晚一个月

nuomi1 commented 3 years ago
if (SWIFT_ALLOW_BACK_DEPLOY_CONCURRENCY)
  set(swift_concurrency_availability "macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0")
else()
  set(swift_concurrency_availability "macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0")
endif()

最低 iOS 13~

imWildCat commented 3 years ago

官方发文了. 可以了. 书本要畅销了. 来个促销庆祝活动吧

@RbBtSn0w 来源请求?

bestwnh commented 3 years ago
```cmake
if (SWIFT_ALLOW_BACK_DEPLOY_CONCURRENCY)
  set(swift_concurrency_availability "macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0")
else()
  set(swift_concurrency_availability "macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0")
endif()

最低 iOS 13~

iOS13已经好很多了,更新进程可以提早两年了。

yongyang007 commented 2 years ago
```cmake
if (SWIFT_ALLOW_BACK_DEPLOY_CONCURRENCY)
  set(swift_concurrency_availability "macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0")
else()
  set(swift_concurrency_availability "macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0")
endif()

最低 iOS 13~

iOS13已经好很多了,更新进程可以提早两年了。

看来短时间内还不能实现 https://forums.swift.org/t/swift-concurrency-back-deployment/51908/3

venn0126 commented 2 years ago
```cmake
if (SWIFT_ALLOW_BACK_DEPLOY_CONCURRENCY)
  set(swift_concurrency_availability "macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0")
else()
  set(swift_concurrency_availability "macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0")
endif()

最低 iOS 13~

iOS13已经好很多了,更新进程可以提早两年了。

看来短时间内还不能实现 https://forums.swift.org/t/swift-concurrency-back-deployment/51908/3

先提前学习吧

imWildCat commented 2 years ago

PR 在做了:https://github.com/apple/swift/pull/39342

onevcat commented 2 years ago

十分怀疑能不能赶得上正式 release...这个东西不经过一段时间验证的话还是很慌的。

之前有 rumor 是说要推后到 Swift 5.6。不过以最近 Apple 的 beta 当 alpha,正式版当 beta 的尿性..搞不好不测了直接强行在 Swift 5.5 就弄上也不无可能...

imWildCat commented 2 years ago

@onevcat back-port 合并到 5.5 branch 了,我们赶紧去买书 😂

qhd commented 2 years ago

发现 @MainActor 修饰的方法没有在主线程执行,这是 bug 吗

    override func viewDidLoad() {
        super.viewDidLoad()

        Task {
            await doSomething()
        }
    }

    func doSomething() async {
        await Task.sleep(2)
        print("doSomething thread \(Thread.current)")
        updateUI()
    }

    @MainActor
    func updateUI() {
        print("updateUI thread \(Thread.current)")
    }
onevcat commented 2 years ago

@qhd 如果你是在一个没有“完全迁移”到 Swift Concurrency Safe 的项目的话,可能需要在 class 申明上也加上 @MainActor 来让它生效:

@MainActor
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        Task {
            await doSomething()
        }
    }

    func doSomething() async {
        await Task.sleep(2)
        print("doSomething thread \(Thread.current)")
        updateUI()
    }

    @MainActor
    func updateUI() {
        print("updateUI thread \(Thread.current)")
    }
}
onevcat commented 2 years ago

另外,需要指出的是,@MainActor 需要 async 环境来完成 actor 的切换。

你最早的例子中只有 doSomething 会运行在自己的线程里,为全局的 ViewController 加上 @MainActor 意味着 doSomething 也许要 main actor。但是这并不影响 updateUI

所以另一种“修正”的方法(可能也是你更想要的方法),是把 updateUI 标记成 async:

    func doSomething() async {
        await Task.sleep(2)
        print("doSomething thread \(Thread.current)")
        await updateUI()
    }

    @MainActor
    func updateUI() async {
        print("updateUI thread \(Thread.current)")
    }
onevcat commented 2 years ago

这个问题,严格来说,应该算是一种编译器的考虑不周?因为 MainActor 中的函数(updateUI)在从外部使用时,应该是要强制加上 await 的。实际上 doSomething 是在 MainActor 之外的。但是在 Swift 5.5 这个尴尬的时间点上,编译器没有给提示。

所以 Swift 5.5 里想要认真写 Swift 并发的话,建议还是把 -Xfrontend -warn-concurrency -Xfrontend -enable-actor-data-race-checks 加到 OTHER_SWIFT_FLAGS,这样能帮你预先抓到一些类似的问题。

bestwnh commented 2 years ago

实际上 doSomething 是在 MainActor 之外的。

@onevcat 其实为什么doSomething会在MainActor之外?不是class声明为MainActor的话,下面所有的属性和方法都自动变成MainActor的么?或者说不是因为UIViewController已经是MainActor,所以继承它的ViewController已经是MainActor了么?

MainActor的行为相当的不清晰,另外想问一下目前有方法让一些方法可以根据调用者的线程保持在对应线程执行而不指定线程的么?像Then这样的框架如果加了上面的OTHER_SWIFT_FLAGS目前会出现不声明MainActor则没法执行MainActor的代码,加了的话就没法执行非MainActor的代码这样的必须取舍的情况。

BackWorld commented 2 years ago

BackWorld commented 2 years ago

@bestwnh 你那里的doSomething()方法是个async方法,这相当于我下面写的detached一个方法里执行的操作,所以也相当于nonisolated func,所以脱离了MainActor的隔离区域,因为一般情况下,你的MainActor里的操作都是sync同步的,不可能出现async,一旦你标识为async,系统可能(猜想)会隐式的给你kick out of the main actor isolated context,所以打印出来才不是主线程。

上述全是个人见解(猜想),不知道对不对。

@MainActor 
class MyViewController: UIViewController {
    func onPress(...) { ... } // implicitly @MainActor

// 这个方法可以脱离主线程运行
    nonisolated func fetchLatestAndDisplay() async { ... } 
}
fanthus commented 2 years ago

学习了,同步函数和同步操作并不是一个概念。

akring commented 1 year ago

今天重读文章,发现这部分代码中的 strings.forEach 是否因为笔误多写了两次?

if let signature = signature {
        strings.forEach { 
                strings.forEach { 
        strings.forEach { 
          addAppending(signature, to: $0)
        }
      }
onevcat commented 1 year ago

@akring 大概是 Cmd+V 被口香糖粘住了...我修一下,感谢指出!