Marking a function as @MainActor isolates it to MainActor.shared, which works very similar here to the actor case (isolated to self). You need to take extra care to make sure cache don’t get changed here.
Notice that the code you provide doesn’t show the definition of cache. I supposed it to be @MainActor private var cache: [URL: Image] = [:].
When you await, the Task you are on is suspended and waits (hence "await") until it is resumed. In this case, while the image is downloading, your Task is waiting.
While that Task is waiting, the actor is able to service operations from other Tasks. By the time the original Task is resumed, any number of other Tasks may have touched and changed the actor's state.
awaits should be thought of as a kind of discontinuity. The world around you can change a lot before/after the await, even though it looks a lot like a normal, synchronous function call. Local variables, however, are preserved across awaits (in this case, the let cached = ... variable you declared just before the await is a local and thus preserved).
Great explanation!
A little addition, and I hope I am right in what I am saying:
If you use the following analogy: A task is to asynchronous functions what a thread is to synchronous functions. then you can avoid this issue as long as you keep whatever you are doing in the context of one Task.
In the example provided note that they leave the Task context with "detach".