[swift-evolution] [Pitch] Synthesizing Concurrency: A Pitch For High-Level Abstractions & Low-Level Intelligence
Howard Lovatt
howard.lovatt at gmail.com
Thu Nov 16 19:01:28 CST 2017
Sorry I don't get the code. In particular:
// Compiler View - of a Constant Property on a Concurrent type
struct Person: Concurrent {
/* Variable Property */
// Constant Property
let birthday: Date {
get {
internalQueue.async { // Immediately returns on calling thread
return underlying_Birthday_Date_Value_Storage
}
}
}
/* Computed Property */
}
Doesn't make sense `async`'s closure doesn't return anything.
What am I missing?
-- Howard.
On 17 November 2017 at 10:50, Christopher Heath via swift-evolution <
swift-evolution at swift.org> wrote:
> Good evening all,
>
> I had a little idea and thought I’d pitch it. So here goes!
>
> Synthesizing Concurrency aims to provide a foundation by which, regardless
> of concurrency implementation (however current proposal uses GCD examples
> for design), concurrency can be synthesized by the compiler and remove many
> lines of boilerplate code. Offering benefits to Application, Server and OS
> Developers alike; with easy and intelligent concurrency conformance. Also,
> should the community decide to head in another direction with concurrency
> this should be a relatively mappable idea to any Synchronization Primitive
> with a little work.
>
> Thanks for looking, and I hope you like and want to contribute to the
> discussion of the Synthesizing Concurrency proposal. I’ve added a printout
> below. Please give it a read or check out the gist:
> https://gist.github.com/XNUPlay/a0d6f6c0afdb3286e324c480cb5c4290
>
> - Chris
>
>
> Printout :
> *Synthesizing Concurrency*
>
> - Proposal: SE-NNNN
> <https://gist.github.com/XNUPlay/a0d6f6c0afdb3286e324c480cb5c4290>
> - Author: Christopher Heath: XNUPlay <https://github.com/xnuplay>
> - Review Manager: TBD
> - Status: *Pending Discussion*
> - Implementation: *Awaiting Implementation*
> - Decision Notes: TBD
>
>
> *Introduction*
> Developers have to write large amounts of boilerplate code to support
> concurrency in complex types. This proposal offers a way for the compiler
> to automatically synthesize conformance to a high-level Concurrent protocol
> to reduce concurrent boilerplate code, in a set of scenarios where
> generating the correct implementation is known to be possible.
> Specifically:
>
> - It aims to provide a high-level Swift protocol that offers opt-in
> concurrency support for any conformable type
> - It aims to provide a well-defined set of thread-safe, concurrent
> implementations for types, their properties, and methods.
> - It aims to provide a language/library compatible implementation with
> deadlock prevention.
>
>
> *Motivation*
> Building robust types in Swift can involve writing significant boilerplate
> code to support concurrency. By eliminating the complexity for the users,
> we make Concurrent types much more appealing to users and allow them to use
> their own types in optimized concurrent and parallel environments that
> require thread safety with no added effort on their part (beyond declaring
> the conformance).
> Concurrency is typically not pervasive across many types, and for each one
> users must implement the concurrent code such that it performs some form of
> synchronization to prevent unexpected behavior.
>
> *Note: Due to it's current status in Swift and use in the **Runtime*
> <https://github.com/apple/swift/blob/master/stdlib/public/runtime/Once.cpp#L31>*,
> examples are written in **Grand Central Dispatch*
> <https://github.com/apple/swift-corelibs-libdispatch>
>
> // Concurrent Protocol - Dispatch Example
> protocol Concurrent {
> // Synthesized Property
> var internalQueue: DispatchQueue { get }
> }
>
> What's worse is that if any functions or properties are added, removed, or
> changed, they must each have their own concurrency code and since it must
> be manually written, it's possible to get it wrong, either by omission or
> typographical error (async vs. sync).
> Likewise, it becomes necessary when one wishes to modify an existing type
> that implements concurrency to do so without introducing bottlenecks or
> different forms of synchronization for some functions and not others, this
> leads to illegible and inefficient code that may defeat the purpose of
> implementing concurrency.
> Crafting high-performance, readable concurrency code can be difficult and
> inconvenient to write.
> Swift already derives conformance to a number of protocols, automatically
> synthesizing their inner-workings when possible. Since there is precedent
> for synthesized conformances in Swift, we propose extending it to
> concurrency in predictable circumstances.
>
>
> *Proposed solution*
> In general, we propose that a type synthesize conformance to Concurrent as
> long as the compiler has reasonable insight into the type. We describe the
> specific conditions under which these conformances are synthesized below,
> followed by the details of how the conformance requirements are implemented.
>
>
> *Requesting synthesis is opt-in*
> Users must *opt-in* to automatic synthesis by declaring their type as
> Concurrent without implementing any of its requirements. This conformance
> must be part of the *original type declaration* and not on an extension
> (see Synthesis in extensions below for more on this).
> Any type that declares such conformance and satisfies the conditions below
> will cause the compiler to synthesize an implementation of an internalQueue
> and async or sync for all properties and methods on that type.
> Making the synthesis opt-in—as opposed to automatic derivation without an
> explicit declaration—provides a number of benefits:
>
> - The syntax for opting in is natural; there is no clear analogue in
> Swift today for having a type opt out of a feature.
> - It requires users to make a conscious decision about the public API
> surfaced by their types. Types cannot accidentally "fall into" conformances
> that the user does not wish them to; a type that does not initially support
> Concurrent can be made to at a later date, but the reverse is a potentially
> breaking change.
> - The conformances supported by a type can be clearly seen by
> examining its source code; nothing is hidden from the user.
> - We reduce the work done by the compiler and the amount of code
> generated by not synthesizing conformances that are not desired and not
> used.
> - As will be discussed later, explicit conformance significantly
> simplifies the implementation for recursive types.
>
>
> *Overriding synthesized conformances*
> Any user-provided implementations of an internalQueue and use of async or
> sync will override the default implementations that would be provided by
> the compiler.
>
>
> *Defining conditions where synthesis is allowed*
> For example take the struct below, which contains all-kinds of properties;
> variable, constant, and computed.
> struct Person {
> var name: String // Variable Property
> let birthday: Date // Constant Property
> var age: Int { // Computed Property
> /* - */
> }
> }
>
>
> *Synthesized Requirements*
>
>
> *Constant Properties*
>
> - Constants are *always* accessed asynchronously.
>
> A Constant is guaranteed to be immutable and therefore able to be read
> from any thread without concern for unexpected mutation.
> The compiler sees this Constant as storage for a value.
>
> // Compiler View - of a Constant Property
> struct Person {
> /* Variable Property */
>
> // Constant Property
> let birthday: Date {
> get {
> return underlying_Birthday_Date_Value_Storage
> }
> }
>
> /* Computed Property */
> }
>
> After opting-in to the Concurrent protocol, the compiler synthesizes this
> implementation, adding an asynchronous access point to any Constant on a
> Concurrent type.
>
> // Compiler View - of a Constant Property on a Concurrent type
> struct Person: Concurrent {
> /* Variable Property */
>
> // Constant Property
> let birthday: Date {
> get {
> internalQueue.async { // Immediately returns on calling thread
> return underlying_Birthday_Date_Value_Storage
> }
> }
> }
>
> /* Computed Property */
> }
>
>
> *Synthesized requirements for Variable Properties*
>
> - Variables are *always* accessed synchronously.
>
> A Variable is mutable and therefore each thread must schedule writes and
> reads separately out of concern for possible mutation.
> Just like a Constant, the compiler sees this Variable as storage for a
> value.
>
> // Compiler View - of a Variable Property
> struct Person {
> // Variable Property
> var name: String {
> get {
> return underlying_Name_String_Value_Storage
> }
> set (newValue) {
> underlying_Name_String_Value_Storage = newValue
> }
> }
>
>
> /* Constant Property */
> /* Computed Property */
> }
>
> Here the compiler synthesizes synchronous access (read or write) to any
> Variable on a Concurrent type.
>
> // Compiler View - of a Variable Property on a Concurrent type
> struct Person: Concurrent {
> // Variable Property
> var name: String {
> get {
> internalQueue.sync { // Wait to ensure all mutation has
> finished
> return underlying_Name_String_Value_Storage
> }
> }
> set (newValue) {
> internalQueue.sync { // Schedule this mutation to happen, in
> order
> underlying_Name_String_Value_Storage = newValue
> }
> }
> }
>
> /* Constant Property */
> /* Computed Property */
> }
>
>
> *Synthesized requirements for Computed Properties*
>
> - Computed Properties are *always* accessed synchronously.
>
> A Computed Property is essentially a function that gets called to create a
> value from other values. These other values can be mutable and therefore
> each thread must schedule writes and reads separately out of concern for
> possible mutation. (Note: If a computed property only accesses Constants,
> it should probably be a one-time set Constant; Swift
> <https://github.com/apple/swift> could use a few proposals in this area.)
>
> // Compiler View - of a Variable Property
> struct Person {
> /* Variable Property */
> /* Constant Property */
>
> // Computed Property
> var age: Int {
> // Compute age from birthday; return
> }
> }
>
> Here the compiler synthesizes synchronous access (read or write) to any
> Computed Property on a Concurrent type.
>
> // Compiler View - of a Variable Property on a Concurrent type
> struct Person: Concurrent {
> /* Variable Property */
> /* Constant Property */
>
> // Computed Property
> var age: Int {
> internalQueue.sync { // Wait to ensure all mutation has finished
> // Compute age from birthday; return
> }
> }
> }
>
>
> *Considerations for recursive types and abstraction*
> By making the synthesized conformances opt-in, recursive types have their
> requirements fall into place with no extra effort. In any cycle belonging
> to a recursive type, every type in that cycle must declare its conformance
> explicitly. If a type does so but cannot have its conformance synthesized
> because it does not satisfy the conditions above, then it is simply an
> error for *that* type and not something that must be detected earlier by
> the compiler in order to reason about *all* the other types involved in
> the cycle. (On the other hand, if conformance were implicit, the compiler
> would have to fully traverse the entire cycle to determine eligibility,
> which would make implementation much more complex).
> With respect to abstraction, the idea that a synchronous function or
> property can access another synchronous function or property, introduces a
> problem: Deadlocking.
>
>
> *The Deadlock Problem*
> Or just Deadlocking, is a problem where a complex program cannot continue
> execution because one or more threads is waiting on a resource to become
> available or for another task to complete.
> Anytime a synchronous function or property accesses another synchronous
> function or property; this is defined as a Deadlock, because the first
> cannot finish without the second being run and the second cannot execute
> without the first being finished.
>
>
> *Solving the Deadlock Problem*
> In complex functions where any number of synchronous and asynchronous
> calls can happen inside a larger scope it is required that the compiler
> know how to handle compilation of such functions, that may access many
> concurrent objects through a multitude of calls. Much like Automatic
> Reference Counting
> <https://en.wikipedia.org/wiki/Automatic_Reference_Counting> increments
> and decrements a counter to determine whether an object should be marked
> for deallocation, we suggest that during compilation a call or set of calls
> is handled by evaluating their concurrent requirements.
> I.e. When a call nests as such:
>
> // Compiler View - of a Complex Function on a Concurrent type
>
> func heavyLift() {
> syncFunction() // 1 Sync
>
>
> async() // 1 Async
>
>
> syncSomeFunction() // 2 Sync
> syncSomeOtherFunction() // 3 Sync
>
>
> asyncSomeFunction() // 2 Async
> asyncSomeOtherFunction() // 3 Async
> }
>
> The compiler should implement a non-modified function, exactly as it would
> today, and wrap each usage in-scope with an asynchronous or synchronous
> requirement.
> Specifically:
>
> - If a higher-level function accesses only asynchronous functions or
> properties internally, that function can be executed in-order as a single
> asynchronous call and inlining access to all non-modified calls.
> - The same is true of synchronous functions or properties. They can be
> executed in-order as a single synchronous call and inlining access to
> existing non-modified calls.
> - If at any point a function or property, accesses an asynchronous and
> synchronous call then that function must be run as a single synchronous
> call.
>
>
> *Implementation details*
> Deadlock Prevention is then inherent by synthesis. The following example
> explains this through a chunk of modified, disassembled Swift code.
>
> // Disassembly View - of an Integer Assignment without Thread-Safety
> int __T07Project14ConcurrentTypeC21functionWithoutSafetyySi5value_tF(int
> arg0) { // Standard
> _swift_beginAccess(__T07Project6objectSiv, &var_30, 0x1, 0x0);
> *__T07Project6objectSiv = arg0;
> rax = _swift_endAccess(&var_30);
> return rax;
> }
>
> // Disassembly View - of an Integer Assignment with Asynchronous Access
> int __T07Project14ConcurrentTypeC17functionWithAsyncySi5value_tF(int
> arg0) { // AsyncCall
> _swift_beginAccess(r13 + 0x10, &var_30, 0x0, 0x0);
> _swift_endAccess(&var_30, &var_30, 0x0, 0x0);
> rax = _swift_rt_swift_allocObject();
>
>
> // Call the Standard Function
> __T07Project14ConcurrentTypeC21functionWithoutSafetyySi5value_tF(int
> arg0)
>
>
> var_60 = __NSConcreteStackBlock;
> var_98 = _Block_copy(&var_60);
> var_A0 = __T0So13DispatchQueueC0A0E5asyncySo0A5GroupCSg5group_
> AC0A3QoSV3qosAC0A13WorkItemFlagsV5flagsyyXB7executetFfA_(&var_60, 0x18,
> __NSConcreteStackBlock);
> var_A1 = __T0So13DispatchQueueC0A0E5asyncySo0A5GroupCSg5group_
> AC0A3QoSV3qosAC0A13WorkItemFlagsV5flagsyyXB7executetFfA0_();
> __T0So13DispatchQueueC0A0E5asyncySo0A5GroupCSg5group_
> AC0A3QoSV3qosAC0A13WorkItemFlagsV5flagsyyXB7executetF(var_A0, var_A1 &
> 0xff, __NSConcreteStackBlock, __T0So13DispatchQueueC0A0E5async
> ySo0A5GroupCSg5group_AC0A3QoSV3qosAC0A13WorkItemFla
> gsV5flagsyyXB7executetFfA1_(&var_60, 0x18), var_98);
> rax = _swift_rt_swift_release(rax, var_A1 & 0xff);
> return rax;
> }
>
> // Disassembly View - of an Integer Assignment with Synchronous Access
> int __T07Project14ConcurrentTypeC16functionWithSyncySi5value_tF(int arg0)
> { // SyncCall
> _swift_beginAccess(r13 + 0x10, &var_28, 0x0, 0x0);
> _swift_endAccess(&var_28, &var_28, 0x0, &var_28);
> rax = _swift_rt_swift_allocObject();
>
>
> // Call the Standard Function
> __T07Project14ConcurrentTypeC21functionWithoutSafetyySi5value_tF(int
> arg0)
>
>
> var_58 = __NSConcreteStackBlock;
> var_90 = _Block_copy(&var_58);
> _swift_rt_swift_release(rax, 0x18);
> dispatch_sync(*(r13 + 0x10), var_90);
> rax = _Block_release(var_90);
> return rax;
> }
>
> Here are 3 functions, one which assigns a value to an integer without any
> safety, like a non-concurrent type. As well as two more which call that
> function using asynchronous and synchronous access respectively.
> The compiler determines which access should be used in a given scope, and
> places that scope inside a synchronous or asynchronous call.
> Take a function which assigns this integer twice asynchronously:
>
> // Standard Call
> +---------------------------------------+
> | TwoAsyncs |
> | +-------------+ +-------------+ |
> | | AsyncCall | | AsyncCall | |
> | +-------------+ +-------------+ |
> +---------------------------------------+
> One might reason that this function would fire-and-forget those calls, but
> instead the compiler is rectifying them as a single async.
>
> // Single Asynchronous Call
> +---------------------------------------+
> | TwoAsyncs (Actually) |
> | +-------------+ +-------------+ |
> | | Standard | | Standard | |
> | +-------------+ +-------------+ |
> +---------------------------------------+
> This behavior is the same for synchronous-only functions; however, instead
> of rehashing lets look at the more interesting complex case. We start with
> this:
>
> // Standard Call
> +---------------------------------------+
> | AnyAsync/SyncCombination |
> | +-------------+ +-------------+ |
> | | AsyncCall | | SyncCall | |
> | +-------------+ +-------------+ |
> +---------------------------------------+
> But in actuality the compiler has composed a single synchronous call,
> since there is a sync call at any point in the function.
>
> // Single Synchronous Call
> +---------------------------------------+
> | AnyAsync/SyncCombination (Actually) |
> | +-------------+ +-------------+ |
> | | Standard | | Standard | |
> | +-------------+ +-------------+ |
> +---------------------------------------+
> Let's do one more for added clarity.
> +-----------------------------------------------------------
> --------------------------+
> | Combination
> |
> | +---------------------------------------+
> +---------------------------------------+ |
> | | TwoAsyncs | |
> AnyAsync/SyncCombination | |
> | | +-------------+ +-------------+ | | +-------------+
> +-------------+ | |
> | | | AsyncCall | | AsyncCall | | | | AsyncCall | |
> SyncCall | | |
> | | +-------------+ +-------------+ | | +-------------+
> +-------------+ | |
> | +---------------------------------------+
> +---------------------------------------+ |
> +-----------------------------------------------------------
> --------------------------+
> Becomes:
> +-----------------------------------------------------------
> --------------------------+
> | Combination //Sync
> |
> | +---------------------------------------+
> +---------------------------------------+ |
> | | TwoAsyncs //Async | | AnyAsync/SyncCombination
> //Sync | |
> | | +-------------+ +-------------+ | | +-------------+
> +-------------+ | |
> | | | Standard | | Standard | | | | Standard | |
> Standard | | |
> | | +-------------+ +-------------+ | | +-------------+
> +-------------+ | |
> | +---------------------------------------+
> +---------------------------------------+ |
> +-----------------------------------------------------------
> --------------------------+
>
> *We've already made great decisions about thread-safety by implementing *
> *SE-0035*
> <https://github.com/apple/swift-evolution/blob/master/proposals/0035-limit-inout-capture.md>*
> for Value Types*
> SE-0035 Limiting Inout Capture
> <https://github.com/apple/swift-evolution/blob/master/proposals/0035-limit-inout-capture.md>
> actually provides us with a proof-of-concept as to why Value Types should
> not be asynchronously mutated inside a closure.
>
>
> *Source compatibility*
> By making the conformance opt-in, this is a purely additive change that
> should not affect existing code and should be easily applicable to stdlib
> types.
> Some current types using Grand Central Dispatch
> <https://github.com/apple/swift-corelibs-libdispatch> should be audited
> for recursive-implementation, if a user wishes to replace their own
> implementation with this synthesized one.
>
>
> *Effect on ABI stability*
> This feature is purely additive and should not change ABI.
> (Additionally, see Explicit Manglings for Sync/Async below for more on
> this.)
>
>
> *Effect on API resilience*
> N/A.
>
>
> *Alternatives considered*
> In order to realistically scope this proposal, we considered but
> ultimately deferred the following items, some of which could be proposed
> additively in the future.
>
>
> *Synthesis in extensions*
> Requirements will be synthesized only for protocol conformances that are *part
> of the type declaration itself;* conformances added in extensions will
> not be synthesized.
> However, to align with Codable in the context of SR-4920
> <https://bugs.swift.org/browse/SR-4920>, we will also currently forbid
> synthesized requirements in extensions in the same file; this specific case
> can be revisited later for all derived conformances.
>
>
> *Explicit Manglings*
> Because accesses are compiled and their async or sync wrappers are
> deterministic from use case, it may be useful to create specific Manglings;
> this is *optional*.
>
>
> *Embedding or Building Dispatch*
> Dispatch already provides us with a very powerful and exacting standard
> for concurrency in Swift, it would be even more useful if embedded directly
> in the runtime with replacements like that currently in the Runtime
> <https://github.com/apple/swift/blob/master/stdlib/public/runtime/Once.cpp#L31>
> .
> I feel as if most of the reason this would be frowned upon is a Swift
> desire for style, code cleanliness and some hope of a 'better' (whatever
> that means to you) solution.
> Yet, we could add the existing library to the Runtime or even rewrite Grand
> Central Dispatch <https://github.com/apple/swift-corelibs-libdispatch> as
> a Swift project and embed it in the Standard Library.
> Ideally, this would be deferred to a separate Swift Evolution Proposal.
>
>
> *Keyword Overrides*
> It is worth mentioning a Keyword could be used for overriding a function
> and defining explicit behavior as sync or async. However, this opens the
> door to misuse and incorrect code that can only be debugged at runtime with
> TSAN. And while we love TSAN:
> TSAN how I love thee. Let me non atomically count the ways…
> - Philippe Hausler
> This is *not* a good idea.
>
>
> *Acknowledgments*
> Thanks to everyone in the Swift Community working to make it an even more
> vibrant place. And especially to those who worked on SE-0166
> <https://github.com/apple/swift-evolution/blob/master/proposals/0166-swift-archival-serialization.md>
> and SE-0185
> <https://github.com/apple/swift-evolution/blob/master/proposals/0185-synthesize-equatable-hashable.md>.
> Whom might notice large parts of a shared ideal, that made this proposal
> much easier to write.
>
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20171117/905ec923/attachment.html>
More information about the swift-evolution
mailing list