Actually, the proposal is that most bug free code in apps today would not need to be changed. All AppKit and UIKit as other system frameworks will need to be correctly labeled with assumptions. Nevertheless, all functions and variables of entire class heirarchies can be marked as requiring access from the main queue just by labelling the root class. e.g public on(.main) class UIView ought to cover a great deal of code. Special attention will be needed however for callback arguments.
Yes, the author of f() is providing no information to the compiler about what execution context it will call x(), so the compiler would flag an error if x() contains a call to a function that must be called on the main queue. This is one of the key expressive weaknesses of Swift that the proposal is aiming to address.
Yes, func c() would be flagged as an error, func b() would not.
func a() would depend on its context. If it is defined in a class which is a subclass of a UIView for instance, it would inherit the class access on(.main) and would be treated like b(). Similarly if it's enclosing class/struct had a access of on(.main) it would pass.
If it was a function on a type with no enclosing /execution-context-modifier/ or a global, then the function itself would always need to be called from a main queue. In absence of any explicit modifier, the on(.main) requirement within the block would need to be passed up to apply to the entire function.
If that's the case, I would change the "source compatibility" section, because "developers bug free code should be source compatible" is not true. For example almost all programs using Alamofire would stop compiling, even if they are bug free.
on(.main) var a: Int = 0
on(other) var b: Int = 0
on(.main) func test() {
var pMain = &a // What is the type of pMain? Is it UnsafeRawPointer<on(.main) Int>?
pMain.pointee += 1 // OK
pMain = &b // This probably should give an error, to make next line not possible
pMain.pointee += 1 // Bug: incrementing on(other) variable from main queue
}
I hope example app is enough of a real world example :) I don't have any of my own open source apps to show.
Notice how self.tableView.reloadData() and self.refreshControl?.endRefreshing() is being called inside requestComplete, because it knows that responseString completionHandler will always be called on the main queue by default.
I am afraid it would be hard to determine statically, because the queue is being passed as a parameter
It is probably too complex for the compiler to determine this requirement itself.
I get your point that this design pattern is not unusual, so the default behaviour of the compiler in the event of no /execution-context-modifier/ being provided for an escaping function parameter may need to be re-thought in order to improve wide scale source compatibility.
The philosophy behind the ideas presented here is to improve the ability for the developer to explain the assumptions about concurrency to the compiler. If no information is provided, then it would probably better for the compiler to flag any access to an on(.main) variable with a warning that it is unable to confirm that the access is safe.
In other words, the absence of a modifier for a callback should probably not be on(.any).
Unless anyone has other thoughts, I'll modify the proposal.
There may be nefarious ways to trick this scheme, but really what matters is catching as many common concurrency errors as possible, not catching every possible edge case.
I just finished writing a lot of code running in various queues hoping fervently that I did not call something on the wrong queue. A lot of doc comments like /// only call this on networkingQueue
This proposal would be a fantastic improvement, strong +1.
Adding something like this would definitely not be source-compatible, but the core team has specifically identified concurrency as an area where we assume that the benefits would justify a source-compatibility break. The breakage would need to be gated by a future language version.
@GuyB, this is an excellent proposal, and my current thinking for the concurrency design includes something similar. Rather than a new modifier, I was thinking of using a new kind of custom attribute, so that a system library could define something like this:
@globalActor
struct UI {
// Shift work to the actor.
static func run<T>(function: () async throws -> T) { ... }
}
and then programs could decorate methods, types, variables, and so on with that attribute:
@UI class MyController: NSObject {
// All the declarations here are implicitly @UI
}
By using a custom attribute, it becomes straightforward to adopt this for someone's own global queues.
One limitation I've found to this approach is it only really works for global queues, rather than instantiated ones. With an instantiated queue, you can of course limit callers to be running on some instance, but not necessarily the right one. So to me the right way of thinking about this is as a very important example of a broader actor concept, hence the @globalActor name I've suggested for the attribute above.
In general, actors have a certain amount of actor-private state (only usable from the actor), a synchronous internal interface (only usable from the actor), and an asynchronous external interface (usable from anywhere, but shifts execution temporarily to the actor). Instantiated actors would be like instances of an actor class, with the actor-private state being essentially like a class's instance properties and the interfaces being essentially like a class's instance methods. For global actors, both the state and the interface encompass basically all of the declarations that are decorated with the actor's attribute.
Obviously the above hangs off our âownâ async type, and we donât want that here, but I think the concepts are the same: dispatching in a particular way acquires a (in our case, type) witness for the fact that one has done so, and functions further down the line require that witness to be passed in/present in the type.
If we view this proposal as basically being implicit parameter passing under the hood, then I think an interesting parallel appears between the representation of queues in this proposal and things like PreferenceKey or EnvironmentKey in SwiftUI â we want a âtype-levelâ representation of a particular queue (or scheduler), or a type-level representation of something that can only be satisfied by a particular queue.
I think if the representation question can be settled, then itâs possible that an âexplicit parameter/witness-passingâ version of this proposal could be implemented as a library. Itâd then be just a matter of adding a feature that makes the value passing implicit
Iâd love to work on it but I am wholly unfamiliar with the compilerâs code base and it would take me ages to get enough information on it to be able to implement something useful like this proposal.
I believe barrier operations on the global concurrent queues are ignored. However for private concurrent queues, we can use barriers to implement something like a read/write lock. Would that somehow be taken into account with this part of the proposal?
Maybe that's not important in the grand scheme of this proposal, however, since this proposal is not strictly about GCD, but just related to it.