Thread Safety for Combine Publishers

In a legacy Objective-C application, I routinely use a one-to-many pattern for notifying delegates of a change. There is a single "Service" class and multiple "Client" classes. The client classes register for delegate notifications from different threads. The service makes no guarantees what thread the delegate notifications will be called on. The service has an internal array of references to the clients. Mutation of this array is protected by a lock in the service as clients can register and unregister from any thread.

In Combine, what thread safety rules do I have to follow when it comes to subscribing to a Publisher? If the service above exports a property of type AnyPublisher (perhaps an internal PassthroughSubject) does Combine allow clients to subscribe (and cancel) to that Publisher from any thread?

If not, then what is an effective pattern to use such that clients can chain operators to the exposed Publisher?

Update

I noticed that this post was marked as off-topic. That's fine, but can someone please elaborate as to why? There was an existing Combine tag and the Using Swift category is described as:

This area is intended for users to get help with or ask questions about Swift or its related tools. Beginner questions welcome!

Hello,

I'm not sure there is a general answer to your question yet. The documentation is still very sparse.

I'd recommend the in-depth posts by Matt Gallagher on "Cocoa with Love" blog:

Those articles explore sharing and concurrency behaviors with a great deal of detail :+1:

Marked off-topic possibly because Combine is a private Apple-only framework that is written in Swift, but, is not part of the Swift eco-system

1 Like

Yeah, that was my guess as well, though not all questions tagged with Combine have been marked off-topic. As well, there are questions related to SwiftUI which is equally Apple-only.

If questions related to Apple specific frameworks are not appropriate on Swift.org, then I would recommend that the forum guidelines be updated and tags like Combine and SwiftUI be disabled or otherwise unavailable when posting new questions.

I think the forums guidelines are pretty clear. Your point about removing SwiftUI and Combine tags is a good one.

There is a fine line between questions about Combine and SwiftUI, and the underlying technologies like property wrappers, function pointers, and opaque return types that are used to implement them. Many questions mention Combine and/or SwiftUI, but are really about how they are implemented as hints to using the new Swift language and standard library enhancement. So, fine line to thread, but, your points are valid.

1 Like

Given those forums give us access to first-hand Combine engineers (I think of @millenomi and @Tony_Parker who chime in from time to time), I think it would be a net loss if those forums would discourage all Combine-related discussions by closing them as off-topic.

This one, especially, asks a very high level general question about Combine scheduling, a topic that is of general interest and can profit from the design-oriented people we find here.

1 Like

I'd like to share some observation regarding topics related to SwiftUI and Combine:

  • The Apple Developer Forums do not have a section that specifically addresses Combine.
  • The response from Apple to posts in the Apple Developer Forums has been somewhat lacking.
  • The documentation for these new frameworks is lacking.
  • Apple has given the community very little by way of examples.

While I realize that Apple has been very busy pushing these new frameworks out the door, the feeling in the community isn't a good one, leaving them to resort to other measures in an effort to understand these frameworks. With this said, I think this community should allow these as off-topic issues, at least until Apple starts addresses the issues listed above.

2 Likes

Some of the topics may contain SwiftUI or Combine code samples but the general issue is related to some Swift feature such as property wrappers, therefore not all of them are marked with the #off-topic tag.

#off-topic tag is nothing bad, nor does it mean "won't be discussed". I add it on topics that are either not meant for these forums or to signal to admins that a particular topic does not necessarily fit into these forums. There is a small but significant difference here, because some admins might just close the whole thread. I've seen that happening to some Combine/SwiftUI threads. Therefore #off-topic is a weak default to hopefully avoid that and still allow people that share any interest in the thread to participate in it. :nerd_face:

That said, if I personally add #off-topic tag to threads, I don't explicitly communicate why I did that, as this happens a lot and I'm too lazy repeating myself. :upside_down_face:

Moderating a forum is tough. I appreciate the high quality discussions currently on Swift.org and don't want to see those diminished.

If a question is deemed to be off-topic, then I would suggest that it just be closed. Tagging a question as off-topic but leaving it active and open for discussion just dilutes the meaning of off-topic, IMHO. It's a nice gesture to consider the interest level of others, but I fear you'll just end up with a lot more off-topic questions.

If questions related to the usability of Apple-specific frameworks like Combine and SwiftUI are not appropriate for the Using Swift category, but there is sufficient interest to keep such questions open, then perhaps two new categories can be opened under Related Projects.

As a new poster who tried to do some research before posting to determine what was an appropriate question, the forum guidelines and Using Swift description where not clear enough to me. As mentioned above, I would suggest editing the Using Swift description to more clearly state that questions should be focused on Swift the language and that questions related to AppKit, UIKit, Combine, SwiftUI and any other Apple specific frameworks are best posted on the Apple Developer Forums or StackOverflow.

2 Likes

I appreciate your feedback and I see your points. Discussions similar to what you just mentioned are still floating around but so far they went nowhere.

To be fair with you, even though I tag some of the SwiftUI and Combine related topics with an #off-topic tag, I personally would welcome discussions about these two frameworks in our forums.

As a user with the regular trust level I personally can‘t close or slice topics. The only moderation that I can help with is through fixing titles, moving topics into appropriate categories or signaling to users and admins through tags such as #off-topic.

I'm not a moderator on the forums, so just my personal 2 cents: I think this forum is a valuable place to have conversations about Combine too. It is clearly the case that the language itself and higher level Swift frameworks have a virtuous circle with each other, so being inclusive in discussions of these topics seems beneficial for everyone in the end.

Now, on to the actual topic at hand.

Here are the thread safety rules for Publisher and Subscriber:

  1. A call to receive(subscriber:) (the implementation-required method) can come from any thread
  2. "Downstream" calls to Subscriber's receive(subscription:), receive(_:), and receive(completion:) must be serialized (but may be on different threads)
  3. "Upstream" calls to Subscription's request(_:) and cancel() must be serialized (but may be on different threads)

This usually results in a lock of some kind in the implementation of Publisher if it maintains a list of Subscribers. For example, PassthroughSubject and CurrentValueSubject do this.

The easiest way to make sure you're following the rules is to simply have your interface privately hold one of these subjects and pass through calls to it.

5 Likes

Thanks Tony,

Based on your comment, it sounds like Combine is about as thread-safe as I expected. Subscribing to a Publisher can be done from any thread, so I assume Subscriptions can also be cancelled from any thread.

The code snippet below shows a rough outline of a Client-Service pattern I'm using in an app. I interpret your reply as confirming that the Clients can safely subscribe to a Publisher from different threads. Likewise, when a client is goes out of scope and is deleted, it's Subscription will be safely cancelled regardless of which thread the Client's deinit is called on.

I realize the usual data-sharing rules apply with multiple threads; this question is only concerned with Combine's thread-safety rules as related to publishing and subscribing.

final class Service {
  
  let publisher:AnyPublisher<Int, Never>
  private let subject = PassthroughSubject<Int, Never>()
  
  init() {
    publisher = subject.eraseToAnyPublisher()
  }
  
  private func notifySubscribers() {
    // This is thread-safe?
    subject.send(1)
  }
}

final class Client {
  
  var cancellables = Set<AnyCancellable>()
  
  init(_ service:Service) {
    
    // This is all thread-safe?
    service.publisher
      .filter { $0 > 1 }
      .sink { [weak self] in self?.handleValue($0) }
      .store(in:&cancellables)
  }
  
  private func handleValue(_ value:Int) {
    print("Value \(value)")
  }
}

// Likely created as a singleton.
let service = Service()

// In thread 1
let c1 = Client(service)

// In thread 2
let c2 = Client(service)

// In thread 3
let c3 = Client(service)

PassthroughSubject (and CurrentValueSubject) takes a lock around sending values downstream specifically to enforce the serialization rule (there is one lock per downstream). So subject.send(1) from any thread should be ok.

For the subscription part, PassthroughSubject will take a lock around its own internal data structure of a list of downstream subscriptions. So the second part should be ok too, when the Client.init is invoked on several threads.

2 Likes

You mention that PassthroughSubjecct acquires a lock around its own internal data structure for subscriptions. What about CurrentValueSubject?

Same thing!

Tony, thanks for your response.

What about Subscriber? Specifically, are calls to sink(receiveCompletion:receiveValue:) and assign(to:on:) thread-safe? I ask, because I have code that gives me the impression that it is not.

Not to be pedantic, but Subscriber is a protocol, so talking about its thread safety doesn't really make sense. Implementations of it are responsible for doing the right things.

I think I'd need to see some example of the calls to sink and assign. I'm not sure if I fully understand what the exact scenario you're seeing is.

Sorry, I meant to say Subscribers.Sink.

The example I refer to isn't mine. I would encourage you to look at the following:

I have tested this using Xcode 11.1 running on macOS 10.15 (none of this is beta) and the logic race that Matt Gallagher still exists.

Terms of Service

Privacy Policy

Cookie Policy