Static Thread Safety

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.

This would be OK though:

func g() {
    DispatchQueue.main.async {
        f {
            DispatchQueue.main.async {
                NSView().layout()
            }
        }
    }
}

You got it ;)

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.

1 Like

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.

Interesting, could you point to a real world example on GitHub ? Perhaps this can be solved with inference.

1 Like

What about pointers?

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

The reference &b would be flagged as an error, the compiler will not permit the on(.main) func test() access a variable restricted to another Q.

Yes, this is very tricky to deal with. Essentially the calls filter down to

public func response<Serializer: DownloadResponseSerializerProtocol>(
        queue: DispatchQueue = .main,
        responseSerializer: Serializer,
        completionHandler: @escaping (AFDownloadResponse<Serializer.SerializedObject>) -> Void)

    ...
    queue.async { completionHandler }

Ideally, the class would be updated so the completionHandlers would be defined as:

    on(queue) (AFDownloadResponse<Serializer.SerializedObject>) -> Void

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.

OK, I don't seem to be able to edit the original post anymore. Here is the change I propose.

I have posted the proposal as a gist here.

1 Like

What about this:

on(qa) var a: Int = 0
on(qb) var b: Int = 0
on(qc) var c: [UnsafePointer<Int>] = []

on(qa)
func grabA() {
    let pa = &a
    qc.async { self.c.append(pa) }
}

on(qb)
func grabB() {
    let pb = &b
    qc.async { self.c.append(pb) }
}

on(qc)
func useThem() {
    for p in self.c {
        p.pointee += 1
    }
}

From Unsafe Pointer in the Apple docs

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.

1 Like

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.

14 Likes

Interesting stuff! If it helps, some friends and I implemented a similar feature here: Tagged queues by peter-tomaselli ¡ Pull Request #25 ¡ wayfair-archive/prelude ¡ GitHub

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 :slightly_smiling_face:

I’m a bit curious when the @ prefix should be used and when shouldn’t. Why can’t we follow such syntax:

class Model {
    @on(.main) var count: Int = 0

    @on(timer) func badCode() {
        self.count = self.count + 1  // Error
    }

    @on(.main) func goodCode() {
        self.count = self.count + 1  // Perfect
    }
}

to mark the thread specifications as additional context?

Anyone else interested in or working on this proposal?

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 can't wait thread guard in the Swift. It is absolutely terrible mine field while working on big apps now

Does it mean that Swift will be closely related to the concept of queues in Dispatch?

No. The concept is general. The idea is that GCD would adopt the annotations so that work placed on a dispatch queue could be audited by the compiler.

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.