[swift-evolution] [Pitch] Synthesizing Concurrency: A Pitch For High-Level Abstractions & Low-Level Intelligence
Pierre Habouzit
phabouzit at apple.com
Wed Nov 22 11:41:37 CST 2017
Hi, I'm on vacation and can't quite answer in depth, but this proposal encourages all the wrong things in terms of performance. We know from 10 years of GCD that mixing async/sync, while possible, yields significant performance issues, and I'd rather not have something at the core of the language encouraging it.
Actors seem like a much better way of solving these problems IMO.
-Pierre
> On Nov 16, 2017, at 1:50 PM, 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 <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, __T0So13DispatchQueueC0A0E5asyncySo0A5GroupCSg5group_AC0A3QoSV3qosAC0A13WorkItemFlagsV5flagsyyXB7executetFfA1_(&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/20171122/d491f85b/attachment.html>
More information about the swift-evolution
mailing list