What does share() do for several subscribers, and what happens to a subscriber who arrives after the shared work has finished?

11.8.3 · L5 · Interview

Predict first

Two subscribers at once and a third after the work finished. What is printed?

import Combine
import Foundation

var bag = Set<AnyCancellable>()
var calls = 0
let source = Deferred { Future<Int, Never> { p in
    calls += 1
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { p(.success(calls)) }
} }
let shared = source.share()
shared.sink { print("A", $0) }.store(in: &bag)
shared.sink { print("B", $0) }.store(in: &bag)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
    shared.sink { print("late C", $0) }.store(in: &bag)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { print("source ran", calls, "time(s)"); exit(0) }
RunLoop.main.run()
  • A 1 / B 1 / source ran 1 time(s)
  • A 1 / B 1 / late C 1 / source ran 1 time(s)
  • A 1 / B 2 / late C 3 / source ran 3 time(s)

Mental model. share is a group bus that leaves once for the people at the stop. It does not photograph the trip for those who show up after it has left.

share() makes the upstream publisher run once for all current subscribers: it subscribes upstream when the first subscriber arrives and fans the values out. In the check, A and B both receive “1” from one run of the source (calls stays 1) rather than two requests. This is the standard way to deduplicate a network request that two parts of the UI observe at the same moment.

What share() does not do is remember. It does not replay values, so a subscriber who arrives after the upstream has finished gets none of them: in the check late C, at 300 ms, receives nothing (no “late C” line) and the source is not run again (“source ran 1 time(s)”). (If the shared pipeline had been cancelled by all subscribers and then re-subscribed, the upstream would start again.) So share() is about concurrent sharing, not caching. If you need “everyone gets the result, even late”, keep the result in a CurrentValueSubject or a stored property, or use multicast { CurrentValueSubject(...) } with connect, or hold the in-flight Task or result in an actor (Q295). Also note that share() uses reference semantics and a subscription count, so cancelling every subscriber stops the upstream, and an upstream error is delivered to all current subscribers.

The two concurrent subscribers share one run; the late one gets nothing and does not restart it.

Common traps

  • Assuming share() caches the result. Late subscribers get nothing, and a re-subscribe after cancel may re-run the work.
  • Subscribing the second observer after the response arrives. It looks like “the request ran once” but the second UI never updates.

They might follow up with

  • How do I build “share with replay of the last value”? Back the pipeline with a CurrentValueSubject via multicast, or store the result yourself.
  • Where does the async/await version of this live? A dictionary of in-flight Tasks keyed by request, so a second caller awaits the same task (Q295).

Related questions

  • 11.8.1 When does a Future run its work, what do two subscribers get, and what does Deferred change?
  • 11.8.2 What happens when you send to a subject after it has completed, and what does a late subscriber to a finished…
  • 11.2.3 What do debounce, throttle and removeDuplicates do, and what do share and multicast change?
  • 10.4.1 What does an actor guarantee, and what is actor reentrancy?

← What happens when you send to a subject after it has completed, and…   How do you move between Combine and async/await: the values property… →