What's the behaviour of this? ```var currentJob: J...
# coroutines
m
What's the behaviour of this?
Copy code
var currentJob: Job? = null

fun haveFun() {
  currentJob = someScope.launch(Dispatchers.IO) {
    // ...
  }.apply {
    invokeOnCompletion {
      currentJob = null
    }
  }
}
There are two assignments: could it happen (even if it's unlikely) that
invokeOnCompletion
runs before the assignment of
someScope.launch(...) { ... }
?
👌 1
d
Yes. This looks like a race condition waiting to happen.
.apply {} has to complete before it can return the result to currentJob. If anything, you'd be better off with:
Copy code
someScope.launch(...){...}.also { job ->
   currentJob = job
   job.invokeOnComplete { currentJob = null }
}
j
Yes a race condition is definitely possible. As for the fix, I don't think it's worth using a scoping function at all here. Why not simply:
Copy code
currentJob = someScope.launch { ... }
currentJob.invokeOnCompletion { currentJob = null }
m
Shouldn’t you anyway be checking
currentJob == this
before setting it to null, just in case another coroutine has set
currentJob
in the meantime?
d
Not even that will be safe, since there isn't any synchronization around the access. You would need a mutex etc...
m
@Mark Between that check and the new assignment something could happen. Simple checks can't solve race conditions. In any case, the code isn't real, I'm not using it. I have something similar, but it does not use
.apply
. My question was actually about what
.apply
compiles to: in typical synchronized code and immutable values, we don't really care how it does what it does. But this made me wonder the compiler transformed
.apply
in a way that 1) It created the job first 2) it ran
.apply
3) it assigned the job to
currentJob
or if 1) it created the job 2) it assigned the job to
currentJob
3) it ran the
.apply
block. The decompiler to Java couldn't quite answer this and I wasn't going to learn the in and outs of bytecode haha.
e
apply
is just a normal (albeit
inline
) function, so yes it gets called before
currentJob
is assigned
nod 1
you could conceivably do something like
Copy code
val currentJob = AtomicReference<Job>()

fun haveFun() {
    val newJob = someScope.launch(start = CoroutineStart.LAZY) { ... }
    if (currentJob.compareAndSet(null, newJob)) {
        newJob.invokeOnCancellation { currentJob.compareAndSet(newJob, null) }
        newJob.start()
    } else {
        newJob.cancel()
    }
}
or depending on your use case, maybe a single actor triggered by a flow or channel would be a better solution
d
invokeOnCompletion
is very rarely useful. If you explain what you intend to do, maybe we could suggest a more idiomatic approach. For example, if you need
null
to do
while (currentJob != null)
or something to that effect, it may be possible to replace it with
while (currentJob.isActive)
.
d
It’s almost funny how few people read through the thread and answer the question you didn’t mean to ask ;-)