当前位置:网站首页>Async/await

Async/await

2022-06-23 17:50:46DerekYuYi

Introduce

At present Swift Used in development closures and completion handlers Handle a lot of asynchronous programming , But these API It's hard to use. . Especially when we need to call multiple asynchronous operations , Multiple error handling (error handling), Or you need to process the control flow when the asynchronous callback completes , In these cases, the code becomes hard to read . This proposal describes a language extension , Make the above problems more natural , It's not easy to make mistakes .

This design will Collaborative program model Introduced to the Swift. Function can optionally use async , It allows programmers to combine complex asynchronous operations using conventional control flow mechanisms . The compiler will convert the asynchronous functions into a set of appropriate closure And state machine .

This proposal defines the semantics of asynchronous functions . in addition , Concurrent programming is not covered in this article . Concurrency is introduced separately in the structured concurrency proposal . Associate asynchronous functions with concurrent execution tasks in the structured concurrency proposal , And provide create 、 Query and cancel the task API.

Swift-evolution Key timeline : Key nodes 1, Key nodes 2

motivation :Completion handlers Non optimal scheme

Use explicit callbacks ( That is to say Completion handlers) There are many problems in asynchronous programming of , This is discussed below . In this article, we suggest introducing asynchronous functions into the language to solve these problems . These asynchronous functions allow asynchronous code to be written synchronously . They also allow the implementation to reason directly about the execution patterns of the code , This enables callbacks to run more efficiently .

Question 1 : Pyramid doom

A series of simple asynchronous operations usually require deeply nested closures . The following example can illustrate this point :

func processImageData1(completionBlock: (_ result: Image) -> Void) {
    loadWebResource("dataprofile.txt") { dataResource in
        loadWebResource("imagedata.dat") { imageResource in
            decodeImage(dataResource, imageResource) { imageTmp in
                dewarpAndCleanupImage(imageTmp) { imageResult in
                    completionBlock(imageResult)
                }
            }
        }
    }
}

processImageData1 { image in
    display(image)
}

This shallow set of code is commonly referred to as “ Pyramid doom ”, Code reading difficulties , It is also difficult to track the code running . besides , Using a bunch of closures can also have some other effects , This point is discussed below .

Question two :Error handling

Callbacks can complicate error handling .Swift 2.0 Introduced an error handling model for synchronous code , But interfaces based on asynchronous callbacks don't get any benefit :

// (2a) Using a `guard` statement for each callback:
func processImageData2a(completionBlock: (_ result: Image?, _ error: Error?) -> Void) {
    loadWebResource("dataprofile.txt") { dataResource, error in
        guard let dataResource = dataResource else {
            completionBlock(nil, error)
            return
        }
        loadWebResource("imagedata.dat") { imageResource, error in
            guard let imageResource = imageResource else {
                completionBlock(nil, error)
                return
            }
            decodeImage(dataResource, imageResource) { imageTmp, error in
                guard let imageTmp = imageTmp else {
                    completionBlock(nil, error)
                    return
                }
                dewarpAndCleanupImage(imageTmp) { imageResult, error in
                    guard let imageResult = imageResult else {
                        completionBlock(nil, error)
                        return
                    }
                    completionBlock(imageResult)
                }
            }
        }
    }
}

processImageData2a { image, error in
    guard let image = image else {
        display("No image today", error)
        return
    }
    display(image)
}

hold [Result](https://github.com/apple/swift-evolution/blob/main/proposals/0235-add-result.md) Added to the standard library to improve Swift APIs Error handling of . Asynchronous functions are contributory Result One of the main reasons :

// (2b) Using a `do-catch` statement for each callback:
func processImageData2b(completionBlock: (Result<Image, Error>) -> Void) {
    loadWebResource("dataprofile.txt") { dataResourceResult in
        do {
            let dataResource = try dataResourceResult.get()
            loadWebResource("imagedata.dat") { imageResourceResult in
                do {
                    let imageResource = try imageResourceResult.get()
                    decodeImage(dataResource, imageResource) { imageTmpResult in
                        do {
                            let imageTmp = try imageTmpResult.get()
                            dewarpAndCleanupImage(imageTmp) { imageResult in
                                completionBlock(imageResult)
                            }
                        } catch {
                            completionBlock(.failure(error))
                        }
                    }
                } catch {
                    completionBlock(.failure(error))
                }
            }
        } catch {
            completionBlock(.failure(error))
        }
    }
}

processImageData2b { result in
    do {
        let image = try result.get()
        display(image)
    } catch {
        display("No image today", error)
    }
}
// (2c) Using a `switch` statement for each callback:
func processImageData2c(completionBlock: (Result<Image, Error>) -> Void) {
    loadWebResource("dataprofile.txt") { dataResourceResult in
        switch dataResourceResult {
        case .success(let dataResource):
            loadWebResource("imagedata.dat") { imageResourceResult in
                switch imageResourceResult {
                case .success(let imageResource):
                    decodeImage(dataResource, imageResource) { imageTmpResult in
                        switch imageTmpResult {
                        case .success(let imageTmp):
                            dewarpAndCleanupImage(imageTmp) { imageResult in
                                completionBlock(imageResult)
                            }
                        case .failure(let error):
                            completionBlock(.failure(error))
                        }
                    }
                case .failure(let error):
                    completionBlock(.failure(error))
                }
            }
        case .failure(let error):
            completionBlock(.failure(error))
        }
    }
}

processImageData2c { result in
    switch result {
    case .success(let image):
        display(image)
    case .failure(let error):
        display("No image today", error)
    }
}

Obviously use Result It is easier to deal with errors , But the problem of closure nesting still exists .

Question 3 : Difficult and error prone condition execution

Conditional execution of asynchronous functions has always been a pain point . such as , Suppose we want to rotate the image after we get it , But sometimes before the rotation operation , You must call an asynchronous function to decode the picture . The best way to build this function is in the intermediate helper closure ( Commonly referred to as continuation closure) Write the code of rotating pictures in , This closure is in completion handler In accordance with the conditions . For example, the pseudo code of this example can be written as follows :

func processImageData3(recipient: Person, completionBlock: (_ result: Image) -> Void) {
    let swizzle: (_ contents: Image) -> Void = {
      // ... continuation closure that calls completionBlock eventually
    }
    if recipient.hasProfilePicture {
        swizzle(recipient.profilePicture)
    } else {
        decodeImage { image in
            swizzle(image)
        }
    }
}

This pattern is the opposite of the normal top-down organization function : Code executed in the second half of a function must appear before the first half of the function is executed . To refactor this function , You must think carefully about auxiliary closures (continuation closure) Capture in , Because closures are in completion handler Use in . As the number of asynchronous functions executed by conditions increases , The more complicated the problem becomes , At last, there is an inversion “ The pyramid of doom ” The phenomenon ( problem 1 Mentioned in “ Pyramid doom ”).

Question 4 : It's easy to make mistakes

It is easy for developers to forget to call the correct completion handler block Go straight back , Jump out of asynchronous operation prematurely . When we forget this operation , Programs sometimes become difficult to debug , There will also be some strange problems :

func processImageData4a(completionBlock: (_ result: Image?, _ error: Error?) -> Void) {
    loadWebResource("dataprofile.txt") { dataResource, error in
        guard let dataResource = dataResource else {
            return // <- forgot to call the block
        }
        loadWebResource("imagedata.dat") { imageResource, error in
            guard let imageResource = imageResource else {
                return // <- forgot to call the block
            }
            ...
        }
    }
}

When you remember to call block when , You may still forget to call block After performing return operation :

func processImageData4b(recipient:Person, completionBlock: (_ result: Image?, _ error: Error?) -> Void) {
    if recipient.hasProfilePicture {
        if let image = recipient.profilePicture {
            completionBlock(image) // <-  call  block  after , Forget to execute  return  operation 
        }
    }
}

In fact, I want to thank guard grammar , To some extent, it prevents forgetting to execute when encoding return operation , But there is no guarantee that all .

Question five : because completion handlers It's hard to use. , A lot of API Use synchronization to complete the definition

This is really hard to measure , But the author thinks , Define and use asynchrony APIs ( Use completion handler) The embarrassment of has led to many APIs Is defined as an obvious synchronous behavior , Even if they can be blocked as asynchronously . This will be in UI It causes uncertain performance and response fluency problems . For example, loaders . And when asynchrony is critical to achieving scale , It will also result in the inability to use these api. For example, server side .

Proposed solution : async/await

An asynchronous function , Often called async/await, Allow asynchronous code to be written as linear and synchronous code . It does this by allowing programmers to take full advantage of the same language structures available in synchronous code , Solve most of the problems described above directly .async/await The use of also naturally preserves the semantic structure of the code , Provide at least 3 Information needed for cross domain language improvement :(1) Better performance of asynchronous code ;(2) Better instrumentality , In the debug , evaluating (profiling) Provides a consistent experience with studying code ;(3) Provides a basis for subsequent concurrency features such as task priority and cancel task . The example in the previous section demonstrates async/await How to greatly simplify asynchronous code :

func loadWebResource(_ path: String) async throws -> Resource
func decodeImage(_ r1: Resource, _ r2: Resource) async throws -> Image
func dewarpAndCleanupImage(_ i : Image) async throws -> Image

func processImageData() async throws -> Image {
  let dataResource  = try await loadWebResource("dataprofile.txt")
  let imageResource = try await loadWebResource("imagedata.dat")
  let imageTmp      = try await decodeImage(dataResource, imageResource)
  let imageResult   = try await dewarpAndCleanupImage(imageTmp)
  return imageResult
}

Most of it is about async/await The description of is discussed through a general implementation mechanism : The compilation process of dividing a function into parts . To understand how a machine works , This is important at lower levels of language abstraction , But at a higher level of language abstraction , We encourage you to ignore this mechanism . contrary , Think of asynchronous functions as ordinary functions with the special ability to give up their threads . Asynchronous functions do not use this capability directly , But when they call , Some calls require them to abandon the thread they are in , Then wait for the execution result . When execution is complete , The function continues to execute from the point of waiting .

Asynchronous and synchronous functions look a lot like . Synchronous functions can be called , When a function call is initiated , The synchronization function waits for the call to complete . Once the call is complete , Control returns to the function and continues from where it stopped . The same is true for asynchronous functions : Asynchronous functions can make function calls , When a function call is initiated , Asynchronous functions usually wait directly for the call to complete . Once the call is complete , Control returns to the function and continues from where it stopped . The only difference is , Synchronous functions can take full advantage of their threads and their stack ( part ), Asynchronous functions can discard the stack completely , And use their own storage . This extra functionality for asynchronous functions has an implementation cost , But we can reduce this cost by designing it as a whole .

Because an asynchronous function must be able to abandon its thread , The synchronization function doesn't know how to abandon the thread , So in general , Synchronous functions cannot call asynchronous functions . If you do , An asynchronous function will give up some of the threads it brings , A synchronous function that calls an asynchronous function will treat it as a return and continue to execute from where it stopped , But there is no return value at this time . The most common method is to block the entire thread , Until the asynchronous function has been restored and completed . This is completely contrary to the purpose of asynchronous functions , And have a bad systematic impact .

contrary , Asynchronous functions can call both synchronous and asynchronous functions . When an asynchronous function calls a synchronous function , First of all, the asynchronous function will not abandon the thread . actually , Asynchronous functions never autonomously give up their thread , They are only in asynchronous functions to a hanging point (suspension point) Will give up the thread . Hanging points can occur inside functions , Or it happens inside another asynchronous function called by the current function , But either way case, Both the asynchronous function and its caller will abandon the thread at the same time .( actually , Asynchronous functions are compiled to be thread independent during asynchronous calls , therefore , Only the innermost function needs to do other extra work .)

When the control flow returns an asynchronous function , It will be exactly restored to its original position . This does not mean that it will run on exactly the same thread as before , because swift Languages are not guaranteed to run after suspension . In this design , Threads are almost more like an implementation mechanism , Not part of the concurrent interface . However , Many asynchronous functions are not just asynchronous : They and the specified actors( Used for isolation ) Link together , And is always considered actor Part of .Swift Will ensure that these functions actually return to where they are located actor To complete the function execution . therefore , A library that directly uses threads for state isolation ( for example , By creating your own threads and scheduling tasks sequentially on them ), These threading models should generally be built as Swift Medium actors, So that these basic languages can run normally .

Hang the starting point (Suspension points)

The hanging point is the point at which the thread must be abandoned during the execution of an asynchronous function . Hanging points are often associated with certain and grammatically explicit Events . From a functional point of view , They are never hidden or asynchronous where they occur ( At this point is synchronous behavior ). The prototype of the hanging point is to call an asynchronous function that is related to different execution contexts .

The hanging point is only associated with explicit operational behavior , This is crucial . in fact , This proposal requires that all calls that may be suspended be included in await Expression . These calls are called potential hang points , Because they don't know if they will be suspended : It depends on the code that is not visible at the call ( such as , Callees may rely on asynchrony I/O) And dynamic conditions ( for example , asynchronous I/O Whether it is necessary to wait for completion ).

On a potential hook await The requirements of Swift Precedent , I.e. requirement try Expressions cover calls to functions that may throw errors . It is very important to mark potential hanging points , Because hanging interrupts atomicity (suspensions interrupt atomicity). such as , If the asynchronous function is running in the context of synchronous queue protection , If you reach a certain starting point this time , This means that other code can be interleaved in the same synchronization queue . A classic but somewhat trite example of atomicity is the modeling of banks : If a deposit is deposited into an account , But before processing matching withdrawals , The operation is paused , And it creates another window , In this window , These funds can be used in double . For many Swift For the programmer , A more similar example is UI Threads : The hanging point can be displayed to the user UI The point of , therefore , The building part UI Then the suspended program may present a flashing 、 Partially built UI( For example, in the process of requesting background services ,UI Show rotating chrysanthemums , While waiting for the background data to return and the rendering to complete , This is a starting point ).( Please note that , Hanging points are also explicitly called in code that uses explicit callbacks : The hang occurs between the return point of the external function and the start point of the callback .) All potential hanging points are required to be marked , Allows programmers to safely assume that locations without potential hanging points will run atomically , And it's easier to identify problematic non atomic patterns .

Because potential hanging points can only appear explicitly at points marked inside asynchronous functions , So a long calculation will still block the thread . This can happen when calling a synchronous function that is only used to do a lot of calculations , Or you encounter a particularly large computation loop in an asynchronous function . In the above two scenarios , When these calculations run , No thread can insert code , It is usually correct that there is no code interference , But this can also become an extensibility issue . An asynchronous program that requires a lot of computation should usually be run in a separate context . When this is not possible , The base library will have some tools to pause and allow other operations to be inserted and run .

Asynchronous functions should avoid calling functions that block threads , Especially if they can prevent it from waiting for work that is currently running . such as , Getting a mutex can block , Until the currently running thread releases the mutex . Sometimes this is acceptable , But it must be used with caution to avoid introducing deadlock problems or false extensibility problems . contrary , Waiting for a condition variable may block , Until some arbitrary other task is scheduled , And signal this variable ; This model is quite different from the recommended practice .

Design details

An asynchronous function

Function types can use async Mark , Indicates that the function is asynchronous :

func collect(function: () async -> Int) { ... } 

Function and initialization declaration function (init) You can also use async Mark :

class Teacher {
  init(hiringFrom: College) async throws {
    ...
  }

  private func raiseHand() async -> Bool {
    ...
  }
}

reason :async Following the function parameter list , Because it is part of the function type and its declaration . This one throws The example is the same .

Use async The declared function or initialized reference type is async Function type . If the reference is a static reference to an instance method , It's asynchronous “ Inside ” Function type , Consistent with the general rules for this type of reference .

Some special functions, such as deinit And access accessors ( For example, attributes and subscripts getters and setters) Out of commission async.

reason : Attributes and subscripts only have getter Methods can be declared as async. At the same time async setter The attributes and subscripts of the method mean that the reference can be used as inout Pass on , And drill down to the attributes of the attribute itself , It depends. setter In fact, it is a moment ( synchronous , Not thrown ) operation . Only read-only is allowed async Attributes and subscripts , prohibit async Properties are simpler .

If functions exist at the same time async and throws, When making a statement async Keywords must be in throws front . This rule applies to async and rethrows.

reason : This order restriction is arbitrary , But it does no harm , It eliminates potential format disputes .

If the of a class async The initialization function does not call the initialization function of the parent class , When the initialization function of the parent class has parameters , Synchronous and is the specified initialization function (designated initializer), Then the initialization function of this class will implicitly call super.init().

reason : If the parent class initialization function is asynchronous , Calls to asynchronous initialization functions are a potential starting point , therefore , call ( requirement await) It must be visible where it is called .

Asynchronous function types

Asynchronous function types are different from synchronous function types . however , A synchronous function type can be implicitly converted to its corresponding asynchronous function type . This one non-throwing Function to its corresponding throwing Function implicit conversion is similar to , Can be combined with asynchronous function transformation . for example :

struct FunctionTypes {
  var syncNonThrowing: () -> Void
  var syncThrowing: () throws -> Void
  var asyncNonThrowing: () async -> Void
  var asyncThrowing: () async throws -> Void
  
  mutating func demonstrateConversions() {
    // Okay to add 'async' and/or 'throws'    
    asyncNonThrowing = syncNonThrowing
    asyncThrowing = syncThrowing
    syncThrowing = syncNonThrowing
    asyncThrowing = asyncNonThrowing
    
    // Error to remove 'async' or 'throws'
    syncNonThrowing = asyncNonThrowing // error
    syncThrowing = asyncThrowing       // error
    syncNonThrowing = syncThrowing     // error
    asyncNonThrowing = syncThrowing    // error
  }
}

Await expression

For asynchronous function types ( Contains the async Direct call to function ) The value call of introduces a potential hang point . Any Hang point must occur in an asynchronous context ( such as async function ). and , It must appear in await In the expression .

Look at this example :

// func redirectURL(for url: URL) async -> URL { ... }
// func dataTask(with: URL) async throws -> (Data, URLResponse) { ... }

let newURL = await server.redirectURL(for: url)
let (data, response) = try await session.dataTask(with: newURL)

In this case , Task suspension may occur in redirectURL(for:) and dataTask(with:) After call , Because they are asynchronous functions . therefore , Two call expressions must be included in await In the expression , Each of them contains a potential starting point . One await May contain multiple potential hanging points . for example , We can write this to use a await To cover the 2 A starting point :

let (data, response) = try await session.dataTask(with: server.redirectURL(for: url))

await No other semantics , Follow try It's like . It simply marks that an asynchronous call is in progress .await The type of an expression is the type of its operand , The result is the result of the operand .await There may be no potential hanging points , In this case, the compiler will give a warning , Follow try The expression rules are the same :

let x = await synchronous() // warning: no calls to 'async' functions occur within 'await' expression

reason : Inside the function , It is important that asynchronous function calls be clearly identified , Because asynchronous functions may introduce hanging points , Breaking the atomicity of operations . The hang point may be inherent in the call ( Because asynchronous calls must be executed on different executors ) Or it may just be part of the callee's implementation . But in either case , It is semantically important , Programmers need to acknowledge this .await Expressions are also a representation of asynchronous code , Asynchronous code interacts with reasoning in closures . This can be seen from Closures Section for more information .

No async Functional autoClosure, There must be no hanging point .

deferblock There must be no potential hanging points in the .

If both exist in the subexpression await and try Variants (try! and try?),await Must be with the try/try!/try? after :

let (data, response) = await try session.dataTask(with: server.redirectURL(for: url)) // error: must be `try await`
let (data, response) = await (try session.dataTask(with: server.redirectURL(for: url))) // okay due to parentheses

reason : This restriction is arbitrary , But to prevent style disputes , It follows the same arbitrary async throws Sequence constraints .

Closures

closure You can have async Function type . In this way closure have access to async Mark :

{ () async -> Int in
  print("here")
  return await getInt()
}

anonymous closure If you include await expression , It will be inferred to have async Function type .

let closure = { await getInt() }

let closure2 = { () -> Int in 
    print("here")
      return await getInt()
}

Please note that , For closures async The inference will not be passed to the closed function in the closure , Nested functions or closures , Because these contents are separable, asynchronous or synchronous . for example , In the following example , Only closure6 Will be inferred as async:

// func getInt() async -> Int { ... }

let closure5 = { () -> Int in       // not 'async'
  let closure6 = { () -> Int in     // implicitly async
    if randomBool() {
      print("there")
      return await getInt()
    } else {
      let closure7 = { () -> Int in 7 }  // not 'async'
      return 0
    }
  }
  
  print("here")
  return 5
}

Overload and overload resolution

The existing Swift API Usually by callback Interface to support asynchronous functions , Such as :

func doSomething(completionHandler: ((String) -> Void)? = nil) { ... }

Many like this API You can do it by adding async To update :

func doSomething() async ->String { ... }

These two functions have different names and signatures , Although they both share the same basic function name . however , Both of them can make parameterless calls ( above completion handler Default values are provided ), This can cause problems for existing code :

doSomething() // problem: can call either, unmodified Swift rules prefer the `async` version

A similar problem also exists with functions that provide both synchronous and asynchronous versions , With the same signature api in . In this case... Is allowed API Providing asynchronous functions is more suitable for Swift Asynchronous scenario , It doesn't break backward compatibility . The new asynchronous function can also support cancel operations ( stay Structured Concurrency You can see the definition of asynchronous function cancellation in ).

// Existing synchronous API
func doSomethingElse() { ... }

// New and enhanced asynchronous API
func doSomethingElse() async { ... }

In the first scenario ,Swift The overload rule of will call the function with default parameters first , So add the async Function will destroy the original call doSomething(completionHandler:) Existing code for , This results in the following errors :

error: `async` function cannot be called from non-asynchronous context

This brings problems to code evolution , Because developers of existing asynchronous libraries either have a mandatory compatibility interrupt ( such as , To a new master version ), Or all the new async Versions need to have different names . The latter may produce a scheme , such as C#'s pervasive Async suffix.

In the second scenario , Both functions have the same signature and only async Different keywords , This kind of situation will be generally recognized by the existing Swift Overload rule rejected . It is really not allowed to distinguish two functions by their effect words , For example, you can't define two only throws Different functions :

// error: redeclaration of function `doSomethingElse()`.

This also creates problems for code evolution , Because developers of existing libraries cannot keep their existing synchronization API, To support new asynchronous features .

contrary , We propose an overload resolution rule to give the context of the call to select the appropriate function . For a given call , Overload resolution will preferentially select non in the synchronization context async function ( Because such a context cannot contain calls to asynchronous functions ). and , Overload resolution takes precedence over... In the asynchronous context async function ( Because this context should avoid jumping out of the asynchronous model into blocking API). When overload resolution selects async Function time , The given call is still subject to “ Asynchronous function calls must occur at await In the expression ” The limitation of .

Overload resolution rules depend on the synchronous or asynchronous context , In the corresponding environment , The compiler selects only one function overload . Selecting an asynchronous function overload requires await expression , Introduction as a starting point for all potential applications :

func f() async {
  // In an asynchronous context, the async overload is preferred:
  await doSomething()
  // Compiler error: Expression is 'async' but is not marked with 'await'
  doSomething()
}

In Africa async Functions and without any await In the closure of an expression , Compiler selects non async heavy load :

func f() async {
  let f2 = {
    // In a synchronous context, the non-async overload is preferred:
    doSomething()
  }
  f2()
}

Autoclosures

Functions may not take async Function type autoClosure Parameters , Unless the function itself is asynchronous . for example , The following statement is not standardized :

// error: async autoclosure in a function that is not itself 'async'
func computeArgumentLater<T>(_ fn: @escaping @autoclosure () async -> T) { } 

There are several reasons for this limitation , Here's an example :

// func getIntSlowly() async -> Int { ... }

let closure = {
  computeArgumentLater(await getIntSlowly())
  print("hello")
}

At first glance ,await The expression seems to imply that the programmer , Calling computeArgumentLater(_:) There was a potential starting point , But this is not the actual scenario : Potential hanging points are passed in and used in computeArgumentLater(_:) intra-function autoclosuse in . This also directly brings about some problems . First ,await The fact that occurs before the call means that closure Will be inferred to contain async Function type , This is not true : All in closure The code in is synchronized . secondly , because await The operation only needs to include a potential hanging point within it , So an equivalent rewrite of the call should look like this :

await computeArgumentLater(getIntSlowly())

however , Because the parameter is autoclosure, This rewrite no longer preserves the original semantics . therefore , By ensuring that asyncautoclosure Parameters can only be used in an asynchronous context , Yes asyncautoclosure Parameter constraints avoid these problems .

Agreement consistency

The agreement may also be declared as async. Such an agreement can be reached through async Or the synchronization function . However , Synchronization function requirements cannot be synchronized async Function implementation . for example :

protocol Asynchronous {
  func f() async
}

protocol Synchronous {
  func g()
}

struct S1: Asynchronous {
  func f() async { } // okay, exactly matches
}

struct S2: Asynchronous {
  func f() { } // okay, synchronous function satisfying async requirement
}

struct S3: Synchronous {
  func g() { } // okay, exactly matches
}

struct S4: Synchronous {
  func g() async { } // error: cannot satisfy synchronous requirement with an async function
}

This behavior follows the subtypes of asynchronous functions / Implicit conversion rules , just as throws The same rules of behavior .

Source code compatibility

This proposal is an addition : The existing code doesn't use any new features ( For example, no async Functions and Closures ) And will not be affected . however , Brought in 2 New context keywords ,async and await.

async In grammar ( Function declaration and function type ) The location allows us to put... Without breaking source code compatibility async Treat as context keywords . In well formed code , User defined async Cannot appear in these grammatical positions .

await Contextual keywords are more likely to cause confusion , Because it appears inside the expression . for example , We can do it in Swift It's called await Function of :

func await(_ x: Int, _ y: Int) -> Int { x + y }

let result = await(1, 2)

At present Swift In language , Yes await A function call is a well - structured piece of code . But with the emergence of this proposal , This code becomes a subexpression (1, 2) Of await expression . This code will be shown as a compilation error in the existing program , because await Can only be used in an asynchronous context , It doesn't exist in a context like this . These functions do not seem to be common , So we believe that as an introduction async/await Part of , This is an acceptable source code corruption .

Yes ABI The effect of stability

Asynchronous functions and function types can be added to ABI in , No effect ABI The stability of , Because existing functions ( synchronous ) And function types are not affected .

Yes API The impact of scalability

async Functional ABI With the synchronization function ABI Completely different ( for example , They have completely incompatible invocation rules ), So add or remove from a function or type async, No impact on scalability .

The future direction

reasync

Swift in rethrows It's the same mechanism , Used to indicate that a particular function only passes itself one parameter throw Function of the throw operation . for example Sequence.map utilize rethrows Because this operation can only throw The way is transform Whether the method itself can throws:

extension Sequence {
  func map<Transformed>(transform: (Element) throws -> Transformed) rethrows -> [Transformed] {
    var result = [Transformed]()
    var iterator = self.makeIterator()
    while let element = iterator.next() {
      result.append(try transform(element))   // note: this is the only `try`!
    }
    return result
  }
}

The actual use map:

_ = [1, 2, 3].map { String($0) }  // okay: map does not throw because the closure does not throw
_ = try ["1", "2", "3"].map { (string: String) -> Int in
  guard let result = Int(string) else { throw IntParseError(string) }
  return result
} // okay: map can throw because the closure can throw

This concept can also be applied to async On the function . for example , We can imagine , When map Parameter with reasync when ,map Functions also become asynchronous functions :

extension Sequence {
  func map<Transformed>(transform: (Element) async throws -> Transformed) reasync rethrows -> [Transformed] {
    var result = [Transformed]()
    var iterator = self.makeIterator()
    while let element = iterator.next() {
      result.append(try await transform(element))   // note: this is the only `try` and only `await`!
    }
    return result
  }
}

Conceptually , This is good : When map The entry parameter of is async Function time ,map Would be considered to be async( Results need to be used await), So if the input parameter is not async function ,map Will be considered synchronous ( Will not use await To accept the results ).

actually , There are several problems :

  • This may not be sequential asynchrony map A very good implementation . It's more likely that , We want concurrent implementations ( For example ) The maximum number of core elements processed concurrently .
  • throw Functional ABI Designed to make rethrows It is possible for functions to act as non - throw function , therefore , One ABI The entrance is enough to handle throw Calls and non-throw Function call . This is not the case with asynchronous functions , Asynchronous functions have completely different ABI, Its efficiency must be lower than that of the synchronization function ABI.

image Sequence.map These may become concurrent functions ,reasync More like a wrong tool : Yes async Closure overloading provides separate ( concurrent ) Implementation looks better . therefore , And rethrows comparison ,reasync May not be applicable .

without doubt ,reasync There are some uses . For example, for optional ?? The operator ,async The implementation is now very close to the synchronization implementation :

func ??<T>(
    _ optValue: T?, _ defaultValue: @autoclosure () async throws -> T
) reasync rethrows -> T {
  if let value = optValue {
    return value
  }
  return try await defaultValue()
}

In this case , The above can be solved through two entry points ABI problem : One is when the parameter is async, The other parameter is not async. However, due to the complexity of the implementation , The author has not yet submitted this design proposal .

alternative

stay await Implemented on try

Many asynchronous API Involving documents I/O, The Internet , Or other operations that may fail , So these operations should also be async and throws. This also means that in the place of the call ,try await Will be called repeatedly many times . To reduce this repeated template invocation ,await Can achieve try, Then the effect of the following two lines of code should be the same :

let dataResource  = await loadWebResource("dataprofile.txt")
let dataResource  = try await loadWebResource("dataprofile.txt")

In the end, there was no await Realization try Because they express different considerations : await Represents a potential hanging point , Other code may execute between your call and its return , and try It's about block External control flow .

The other one wants to await Implemented on try Your motivation is related to task cancellation . If you cancel building a task to throw an error , And each potential hold point implicitly checks whether the task has been canceled , Then each potential hook can do a throw operation : such case Next await Can achieve try Because of every await Can exit with errors .Structured Concurrency The proposal includes a description of the task cancellation , It does not model the cancellation task as throwing an error , Nor does it introduce an implicit cancellation check at every potential hang point .

start-up async Mission

Because only async Code can call other async Code , This proposal does not provide a way to initialize asynchronous code . This was done intentionally : All asynchronous code runs on "task" In the context of ."task" yes Structured Concurrency Concepts defined in the proposal . This proposal provides for the adoption of @main The ability to define asynchronous entry points for a program , such as :

@main
struct MyProgram {
  static func main() async { ... }
}

in addition , In this proposal , top floor (top-level) Code cannot be considered context , So the following program format is incorrect :

func f() async -> String { "hello, asynchronously" }

print(await f()) // error: cannot call asynchronous function in top-level code

This will also be addressed in subsequent proposals , This proposal will appropriately consider top-level variables (top-level variables).

Considerations for top-level code do not affect the..., as defined in this proposal async/await The basic mechanism .

hold await As a grammar sugar

The proposal puts async Function as Swift The core of the type system , Distinguish synchronization functions . Another design is to keep the type system unchanged , But in some Future<T, Error> Use... In type async and await grammar . for example :

async func processImageData() throws -> Future<Image, Error> {
  let dataResource  = try loadWebResource("dataprofile.txt").await()
  let imageResource = try loadWebResource("imagedata.dat").await()
  let imageTmp      = try decodeImage(dataResource, imageResource).await()
  let imageResult   = try dewarpAndCleanupImage(imageTmp).await()
  return imageResult
}

This design compares to the proposal in this article , There are many shortcomings :

  • stay Swift In ecosystem , There is no universal Future Types can be built . If Swift The ecosystem has basically identified a single future type ( for example , There is already one in the standard library ), A way similar to the above syntax sugar will appear in the existing code . If there is no such type , People will have to try to abstract all the different types of future types with some future protocol . This may be possible for some types in the future , However, any guarantee of the behavior or performance of asynchronous code is waived .
  • And throws The design of is inconsistent . In this model , The result type of an asynchronous function is future type ( Or something else Futurable type ), Instead of the actual return value . They must always be awaited( So it's suffix syntax ), otherwise , When you really care about the results of asynchronous operations , You will use futures. When many other aspects of asynchronous design are deliberately avoided for future When thinking , This becomes a system with future Model programming , Instead of an asynchronous programming model .
  • take async Deleting from the type system will eliminate the async The ability to overload . Look at the previous section , understand async The reason for the overload on the .
  • Future Is a relatively heavy type , And generating one for each asynchronous operation costs a lot in code size and performance . contrary , High integration with system types allows async The function is designed to build and optimize asynchronous functions , For efficient suspend operations .Swift All levels of the compiler and runtime can future The return function cannot be optimized in a way async function .

Version history

  • Review changes :
    • Use try await Instead of await try.
    • Add syntax sugar alternative design .
    • The modification proposal allows stay async Load up .
  • Other changes :
    • You can no longer directly overload asynchronous and non asynchronous functions , However , There are other reasons to support overloaded solutions .
    • Add implicit conversion from synchronous function to asynchronous function .
    • increase await try Order constraints to match async throws Limit .
    • increase async Initialization support .
    • Added to meet async Support for synchronization functions required by the protocol .
    • increase reasync The discussion of the .
    • increase await It doesn't contain try The reason of .
    • increase async Reason to follow in function argument list .
  • first draft ( file and Community discussion node )

Other relevant proposals

In addition to this proposal , There are a number of related proposals that include Swift Other aspects of the concurrency model :

  • And Objective-C Concurrent interoperation of : Description and Objective-C Interaction , Especially in accepting completion handler The asynchronous Objective-C Methods and @objc asyncSwift The relationship between methods .
  • Structured concurrency : Describe the task structure used in asynchronous invocation , Creation of subtasks and detached tasks 、 Cancel 、 Prioritization and other task management API.
  • Actors: Describe the participant model , Provides state isolation for concurrent programming .
原网站

版权声明
本文为[DerekYuYi]所创,转载请带上原文链接,感谢
https://chowdera.com/2022/01/202201051711021916.html

随机推荐