如何防止PassthroughSubject在并发的上游期货交易完成之前杀死.sink?

时间:2019-07-11 01:42:07

标签: swift ios13 combine

我有一个PassthroughSubject,它发送30个整数,后跟完成消息。

从受试者那里得到这些数字后,我产生了一个会睡一秒钟的未来,并以输入数字* 2结束。

我使用.receiveOn来确保期货同时运行,但这意味着完成消息也同时通过链传播 并在所有期货交易完成之前结束汇。

那里的任何RxSwift / Combine向导都知道如何做到这一点,因此期货完成会延迟收到完成消息?

这是一个实现上述行为的游乐场:

import Foundation
import Combine
import PlaygroundSupport

/// Setting up the playground
PlaygroundPage.current.needsIndefiniteExecution = true

/// Injects numbers 0-30 into combine message stream, and then sends a finish.
func publishNumbers(to subject: PassthroughSubject<Int, Error>) {
    (0..<30).forEach {
        subject.send($0)
    }
    subject.send(completion: .finished)
}
/// Delays for one secont, and completes the future by doubling the input.
func delayAndDoubleNumber(_ int: Int) -> Future<Int, Error> {
    return Future<Int, Error> { complete in
        sleep(1)
        complete(.success(int * 2))
    }
}

// Properties involved in Combine processing chain.
let numbersSubject = PassthroughSubject<Int, Error>()
let processingQueue = DispatchQueue.global(qos: .userInitiated)


// Combine processing chain
numbersSubject
    .receive(on: processingQueue) //Comment this line to observe that all futures finish, and are collected before the finish message kills the sink.
    .flatMap { number in
        return delayAndDoubleNumber(number)
    }
    .collect(4)
    .sink(receiveCompletion: { completion in
        print("Complete: \(completion)")
    }, receiveValue: { value in
        print("Received Value: \(value)")
    })

publishNumbers(to: numbersSubject)

2 个答案:

答案 0 :(得分:3)

从Xcode 11 beta 3开始,您不能将并发队列与Combine一起使用。您应该能够使用Xcode 11 GM。

Philippe Hausler是一位Apple工程师,从事Combine的工作。他在the official Swift forum上说了以下话:

  

另外,值得注意的是,用作调度程序的DispatchQueue必须始终是串行的,以遵守Combine运营商的合同。

Then later他说:

  

因此,在此进行跟进时,在传播下游事件的方式方面存在一些变化。现在,即使DispatchQueue是并发的,或者OperationQueue不是maxConcurrentOperations的限制为1,或者任何有效的调度程序都在并发,我们现在也能够满足1.03的约束。我们将始终在.receive(on:)的请求的调度程序上发送序列化事件。我们稍微偏离规范的一个剩余警告是,我们世界中的上游事件(例如cancel()request(_:)可以同时发生。话虽如此,我们确实以线程安全的方式处理它们。

您可以通过在Future闭包内调度到并发队列,然后返回主队列,来使并发在Xcode 11 beta 3中起作用:

import Foundation
import Combine
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

func delayAndDoubleNumber(_ int: Int) -> Future<Int, Never> {
    return Future<Int, Never> { complete in
        DispatchQueue.global(qos: .userInitiated).async {
            sleep(1)
            DispatchQueue.main.async {
                complete(.success(int * 2))
            }
        }
    }
}

let subject = PassthroughSubject<Int, Never>()

subject
    .flatMap { delayAndDoubleNumber($0) }
    .collect(4)
    .sink(
        receiveCompletion: { print("Complete: \($0)") },
        receiveValue: { print("Received Value: \($0)") })

let canceller = (0 ..< 30).publisher().subscribe(subject)

答案 1 :(得分:1)

免责声明,这可能是对文档的错误解释,但我认为您应该使用receive(on:)运算符而不是numbersSubject

Apple Docs

  

与receive(on:options :)会影响下游消息相反,subscribe(on :)会更改上游消息的执行上下文。

我对此的解释是,如果您希望将subscribe(on:)中的事件发送到队列中,则可以使用numbersSubject .flatMap { number in return delayAndDoubleNumber(number) } .collect(4) .subscribe(on: processingQueue) .receive(on: RunLoop.main) .sink(receiveCompletion: { completion in print("Complete: \(completion)") }, receiveValue: { value in print("Received Value: \(value)") }) ,例如:

HTTP connector configuration: 
...
Connector connector = new MyTomcatConnector(MyHttp11NioProtocol.class.getName());
            connector.setProperty("address", "xxx.xxx.xxx.xxx");
            connector.setPort(80);
            connector.setRedirectPort(443);
            ... set addition connector properties(connector);
...

public class MyConnector extends org.apache.catalina.connector.Connector {
...
     public MyTomcatConnector(String protocol) throws Exception {
            super(protocol);
            + my own logger;
     }
     ...
     public MyRequest createRequest() {
             my logger
             MyRequest request = new MyRequest(this);
             return (request); 
     }
}

public class MyRequest extends org.apache.catalina.connector.Request {
     ...
     public MyRequest(Connector connector) {
      super(connector);
      + my own logger;
   }
   @Override
   public Locale getLocale() {
      + my own logger;
      Locale locale = super.getLocale();
      ....
      return locale;
   }
}

public class MyHttp11NioProtocol extends AbstractHttp11JsseProtocol<NioChannel> {
   ...
   public MyHttp11NioProtocol(){
        super(new MyNioEndpoint(HttpInterface.SERVICE));
        + my own logger;
    }
   ...
   Copy/paste method from Http11NioProtocol
}

public class MyNioEndpoint extends NioEndpoint implements TLSContextObserver {
    protected MySSLContext context;
    protected SSLEngine engine;
    ...
      public MyNioEndpoint(String sslContextName) {
      super();
      ...
      try {
         context = //get instance of my own SSLContext by name (sslContextName);
         + logger

      }catch(...)...      
    }

   @Override
   protected SSLEngine createSSLEngine(String sniHostName, List<Cipher> clientRequestedCiphers, List<String> clientRequestedApplicationProtocols) {
      engine = context.getSSLEngine();
      context.addObserver(this);
      return engine;
   }

}

public class MySSLContext extends AbstractObservable<TLSContextObserver, TLSInterfaceConfig>  implements org.apache.tomcat.util.net.SSLContext {
    //configuing standart SSLContext (keyManager[], TrusteManager[] ... etc...
}