Yes, that was one simple way to do it with a class
isolated to a global actor.
As you suggest, the more generalized approach with an actor
is to expose an function for mutating its state:
actor Example {
var string = "hello"
func setString(to value: String) {
string = value
}
}
And, if updating this from a synchronous context (where you cannot await
directly), you could again use a Task
:
Task {
await myExample.setString(to: "world")
}
With actor
types, you cannot directly update the individual properties directly.
This is an important language feature. The elimination of the data race on a given property is not our only concern: There might be interdependencies with other properties within the actor
, too. Callers really should not be reaching and mutating individual properties, and the language enforces this principle for us. The actor
bears the responsible for coordinating the mutation of its internal state.
This example is so trivial that having to go through the function seem like an unnecessary step. But it is, more generally, a very important design principle, namely that the actor
bears responsible for maintaining its own internal integrity and for providing external interfaces.
On a separate topic, elsewhere you have contemplated the use of custom executors. I know this was just a thought-exercise, but I might advise some care: One of the principles of Swift concurrency’s cooperative thread pool is to ensure that the system is never over-committed (by limiting this pool to the number of the CPUs on the device). If you start employing custom executors all over the place, you lose this aspect of Swift concurrency.
Don’t get me wrong: Custom executors are a very welcome addition to the language. But, I might suggest limiting their use to their intended purpose (e.g., where some external system necessitates it due to some unusual threading limitations, etc.). Or, at the very least, if you adopt custom executors, understand the tradeoffs they entail. But, this does not strike me as a prudent use-case.