The actor function contains a receiver parameter of the ActorScope type. The source code of the actor function looks as follows:
public fun <E> CoroutineScope.actor(
context: CoroutineContext = EmptyCoroutineContext,
capacity: Int = 0,
start: CoroutineStart = CoroutineStart.DEFAULT,
onCompletion: CompletionHandler? = null,
block: suspend ActorScope<E>.() -> Unit
): SendChannel<E> {
val newContext = newCoroutineContext(context)
val channel = Channel<E>(capacity)
val coroutine = if (start.isLazy)
LazyActorCoroutine(newContext, channel, block) else
ActorCoroutine(newContext, channel, active = true)
if (onCompletion != null) coroutine.invokeOnCompletion(handler = onCompletion)
coroutine.start(start, coroutine, block)
return coroutine
}
The ActorScope interface looks similar to the ProducerScope interface, but implements the ReceiveChannel interface:
public interface ActorScope<E> : CoroutineScope, ReceiveChannel<E> {
val channel: Channel<E>
}
As you probably know, it is not a good idea to access mutable data from different coroutines. To deal with this, we can use channels and the actor function, in the following way:
suspend fun numberConsumer() = GlobalScope.actor<Int> {
var counter = 0
for (value in channel) {
counter += value
println(counter)
}
}
The preceding snippet contains a mutable variable named counter. We change the value of the counter variable when a channel receives a new value. Since a channel suspends the caller until a consumer finishes processing the current value, we can be sure that the counter variable will be modified in the right way.
The numbersCounter function can be used as follows:
@Test
fun actorExample() = runBlocking<Unit> {
val actor = numberConsumer()
(0..10).forEach {
launch {
actor.send(it)
}
}
}
The preceding snippet launches ten coroutines that send a value to an actor in parallel.
The output looks as follows:
0
1
3
6
10
15
21
28
36
45
55
The output shows that the counter variable is modified in the right way.