@@ -9,107 +9,51 @@ import kotlinx.coroutines.*
9
9
import kotlinx.coroutines.channels.*
10
10
import kotlinx.coroutines.flow.*
11
11
import kotlinx.coroutines.internal.*
12
- import kotlinx.coroutines.selects.*
13
12
import kotlin.coroutines.*
14
13
import kotlin.coroutines.intrinsics.*
15
14
16
- internal fun getNull (): Symbol = NULL // Workaround for JS BE bug
17
-
18
- internal suspend fun <T1 , T2 , R > FlowCollector<R>.combineTransformInternal (
19
- first : Flow <T1 >, second : Flow <T2 >,
20
- transform : suspend FlowCollector <R >.(a: T1 , b: T2 ) -> Unit
21
- ) {
22
- coroutineScope {
23
- val firstChannel = asFairChannel(first)
24
- val secondChannel = asFairChannel(second)
25
- var firstValue: Any? = null
26
- var secondValue: Any? = null
27
- var firstIsClosed = false
28
- var secondIsClosed = false
29
- while (! firstIsClosed || ! secondIsClosed) {
30
- select<Unit > {
31
- onReceive(firstIsClosed, firstChannel, { firstIsClosed = true }) { value ->
32
- firstValue = value
33
- if (secondValue != = null ) {
34
- transform(getNull().unbox(firstValue), getNull().unbox(secondValue) as T2 )
35
- }
36
- }
37
-
38
- onReceive(secondIsClosed, secondChannel, { secondIsClosed = true }) { value ->
39
- secondValue = value
40
- if (firstValue != = null ) {
41
- transform(getNull().unbox(firstValue) as T1 , getNull().unbox(secondValue) as T2 )
42
- }
43
- }
44
- }
45
- }
46
- }
47
- }
48
-
49
15
@PublishedApi
50
16
internal suspend fun <R , T > FlowCollector<R>.combineInternal (
51
17
flows : Array <out Flow <T >>,
52
18
arrayFactory : () -> Array <T ?>,
53
19
transform : suspend FlowCollector <R >.(Array <T >) -> Unit
54
- ): Unit = coroutineScope {
20
+ ): Unit = flowScope { // flow scope so any cancellation within the source flow will cancel the whole scope
55
21
val size = flows.size
56
- val channels = Array (size) { asFairChannel(flows[it]) }
57
- val latestValues = arrayOfNulls<Any ?>(size)
22
+ val latestValues = Array <Any ?>(size) { NULL }
58
23
val isClosed = Array (size) { false }
59
- var nonClosed = size
60
- var remainingNulls = size
61
- // See flow.combine(other) for explanation of the logic
62
- // Reuse receive blocks to avoid allocations on each iteration
63
- val onReceiveBlocks = Array < suspend (Any? ) -> Unit > (size) { i ->
64
- { value ->
65
- if (value == = null ) {
66
- isClosed[i] = true ;
67
- -- nonClosed
68
- }
69
- else {
70
- if (latestValues[i] == null ) -- remainingNulls
71
- latestValues[i] = value
72
- if (remainingNulls == 0 ) {
73
- val arguments = arrayFactory()
74
- for (index in 0 until size) {
75
- arguments[index] = NULL .unbox(latestValues[index])
24
+ val resultChannel = Channel <Array <T >>(Channel .CONFLATED )
25
+ val nonClosed = LocalAtomicInt (size)
26
+ val remainingAbsentValues = LocalAtomicInt (size)
27
+ for (i in 0 until size) {
28
+ // Coroutine per flow that keeps track of its value and sends result to downstream
29
+ launch {
30
+ try {
31
+ flows[i].collect { value ->
32
+ val previous = latestValues[i]
33
+ latestValues[i] = value
34
+ if (previous == = NULL ) remainingAbsentValues.decrementAndGet()
35
+ if (remainingAbsentValues.value == 0 ) {
36
+ val results = arrayFactory()
37
+ for (index in 0 until size) {
38
+ results[index] = NULL .unbox(latestValues[index])
39
+ }
40
+ // NB: here actually "stale" array can overwrite a fresh one and break linearizability
41
+ resultChannel.send(results as Array <T >)
76
42
}
77
- transform(arguments as Array <T >)
43
+ yield () // Emulate fairness for backward compatibility
44
+ }
45
+ } finally {
46
+ isClosed[i] = true
47
+ // Close the channel when there is no more flows
48
+ if (nonClosed.decrementAndGet() == 0 ) {
49
+ resultChannel.close()
78
50
}
79
51
}
80
52
}
81
53
}
82
54
83
- while (nonClosed != 0 ) {
84
- select<Unit > {
85
- for (i in 0 until size) {
86
- if (isClosed[i]) continue
87
- channels[i].onReceiveOrNull(onReceiveBlocks[i])
88
- }
89
- }
90
- }
91
- }
92
-
93
- private inline fun SelectBuilder<Unit>.onReceive (
94
- isClosed : Boolean ,
95
- channel : ReceiveChannel <Any >,
96
- crossinline onClosed : () -> Unit ,
97
- noinline onReceive : suspend (value: Any ) -> Unit
98
- ) {
99
- if (isClosed) return
100
- @Suppress(" DEPRECATION" )
101
- channel.onReceiveOrNull {
102
- // TODO onReceiveOrClosed when boxing issues are fixed
103
- if (it == = null ) onClosed()
104
- else onReceive(it)
105
- }
106
- }
107
-
108
- // Channel has any type due to onReceiveOrNull. This will be fixed after receiveOrClosed
109
- private fun CoroutineScope.asFairChannel (flow : Flow <* >): ReceiveChannel <Any > = produce {
110
- val channel = channel as ChannelCoroutine <Any >
111
- flow.collect { value ->
112
- return @collect channel.sendFair(value ? : NULL )
55
+ resultChannel.consumeEach {
56
+ transform(it)
113
57
}
114
58
}
115
59
@@ -131,12 +75,25 @@ internal fun <T1, T2, R> zipImpl(flow: Flow<T1>, flow2: Flow<T2>, transform: sus
131
75
val collectJob = Job ()
132
76
val scopeJob = currentCoroutineContext()[Job ]!!
133
77
(second as SendChannel <* >).invokeOnClose {
78
+ // Optimization to avoid AFE allocation when the other flow is done
134
79
if (! collectJob.isActive) collectJob.cancel(AbortFlowException (this @unsafeFlow))
135
80
}
136
81
137
82
val newContext = coroutineContext + scopeJob
138
83
val cnt = threadContextElements(newContext)
139
84
try {
85
+ /*
86
+ * Non-trivial undispatched (because we are in the right context and there is no structured concurrency)
87
+ * hierarchy:
88
+ * -Outer coroutineScope that owns the whole zip process
89
+ * - First flow is collected by the child of coroutineScope, collectJob.
90
+ * So it can be safely cancelled as soon as the second flow is done
91
+ * - **But** the downstream MUST NOT be cancelled when the second flow is done,
92
+ * so we emit to downstream from coroutineScope job.
93
+ * Typically, such hierarchy requires coroutine for collector that communicates
94
+ * with coroutines scope via a channel, but it's way too expensive, so
95
+ * we are using this trick instead.
96
+ */
140
97
withContextUndispatched( coroutineContext + collectJob) {
141
98
flow.collect { value ->
142
99
val otherValue = second.receiveOrNull() ? : return @collect
0 commit comments