簡體   English   中英

Swift Combine:使用 zip 運算符的意外背壓行為

[英]Swift Combine: Unexpected backpressure behaviour with zip operator

我有一個關於結合背壓組合中的zip運算符的問題。

取以下代碼片段:

let sequencePublisher = Publishers.Sequence<Range<Int>, Never>(sequence: 0..<Int.max)
let subject = PassthroughSubject<String, Never>()

let handle = subject
    .zip(sequencePublisher.print())
    .print()
    .sink { letters, digits in
        print(letters, digits)
    }

subject.send("a")

在操場上執行此操作時,輸出如下:

receive subscription: (0..<9223372036854775807)
receive subscription: (Zip)
request unlimited
request unlimited
receive value: (0)
receive value: (1)
receive value: (2)
receive value: (3)
receive value: (4)
receive value: (5)
receive value: (6)
receive value: (7)
...

在 iOS 設備上執行時,由於內存問題,代碼在幾秒鍾后崩潰。

根本原因可以在上面的第四行中看到,其中zipsequencePublisher請求無限數量的值。 由於sequencePublisher提供了整個范圍的Int值,這會導致內存溢出。

我想知道的:

  • zip等待每個發布者的一個值,然后再組合它們並推動它們
  • 背壓用於控制從訂閱者到發布者的需求

我的期望是zip只向每個發布者請求一個值,等待它們到達,並且只在從每個發布者收到一個值時才請求下一個值。

在這種特殊情況下,我嘗試構建一種行為,其中將序列號分配給subject產生的每個值。 但是,我可以想象,當zip結合來自發布頻率非常不同的發布者的值時,這總是一個問題。

zip運算符中使用背壓似乎是解決該問題的完美工具。 你知道為什么不是這樣嗎? 這是一個錯誤還是故意的? 如果是故意的,為什么?

謝謝你們

看來Sequence發布者只是不現實。 它似乎對背壓沒有反應; 它只是一次噴出整個序列,這在一個發布應該是異步的世界中是沒有意義的。 如果您將Int.max更改為 3,則沒有問題。 :) 我不知道這是 Sequence 發布者的整個概念中的錯誤還是缺陷。

但是,對於您的實際用例來說真的沒有問題,因為有一種更好的方法可以為 Subject 的每個發射分配一個連續的數字,即scan

這是一個更現實的方法:

func delay(_ delay:Double, closure:@escaping ()->()) {
    let when = DispatchTime.now() + delay
    DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
class ViewController : UIViewController {
    var storage = Set<AnyCancellable>()
    override func viewDidLoad() {
        super.viewDidLoad()
        let subject = PassthroughSubject<String, Never>()
        subject.scan(("",0)) {t,s in (s,t.1+1)}
            .sink { print($0.0, $0.1)
            }.store(in:&storage)
        delay(1) {
            subject.send("a") // a 1
            delay(1) {
                subject.send("b") // b 2
            }
        }
    }
}

這假設您有其他一些原因需要每個連續的枚舉通過管道傳遞。 但是,如果您的唯一目標是在每個信號到達接收sink本身時對其進行枚舉,那么您可以讓接收sink本身維護一個計數器(這很容易做到,因為它是一個閉包):

    var storage = Set<AnyCancellable>()
    let subject = PassthroughSubject<String, Never>()
    override func viewDidLoad() {
        super.viewDidLoad()
        var counter = 1
        subject
            .sink {print($0, counter); counter += 1}
            .store(in:&storage)
        delay(1) {
            self.subject.send("a") // a 1
            self.subject.send("b") // b 2
        }
    }

組合的 zip 運算符:

  1. 將請求的背壓需求從其下游訂閱者轉發到上游,這對接收器來說是無限的
  2. 從第一個上游緩沖整個序列

除了基於掃描的解決方案之外,您還可以通過控制下游的背壓需求或使用自定義 zip 運算符來避免該問題。

我設法開發了一個自定義 zip 運算符,它在兩個方面與原始 zip 運算符不同:

  1. 無論它從下游收到什么背壓需求,它總是向上游發送一個只有一個值的需求,然后等待每個人的響應,然后向下游發出結果,結束回合。 如此重復,直到需求耗盡。
  2. 它通過使用所描述的基於“回合”的方法避免緩沖整個上游序列。

此處包含的代碼有點廣泛,但請隨時在此 repo https://github.com/SergeBouts/XCombine 中查看

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM