내가 읽기 위해서, 내맘대로 번역한 글.

원문 : www.process-one.net/blog/swiftnio-futures-and-promises/

 

SwiftNIO is Apple non-blocking networking library. It can be used to write either client libraries or server frameworks and works on macOS, iOS and Linux.
SwiftNIO 는 애플의 비차단 네트워크 라이브러리이다.
이것은 클라이언트 라이브러리 또는 서버 프레임워크를 만드는데 사용할수 있고, macOs, iOS, Linux에서 동작한다.

It is built by some of the Netty team members. It is a port of Netty, a high performance networking framework written in Java and adapted in Swift. SwiftNIO thus reuses years of experience designing a proven framework.
이것은 Netty 팀원 일부에 의해서 만들어졌다. Netty의 포트이고, 자바로 작성된 고성능 네트워크 프레임워크이고, swift를 채택했다.
따라서 SwiftNIO 는 입증된 프레임워크를 설계하는 수년간의 경험을 재사용한다.

If you want to understand in depth how SwiftNIO works, you first have to understand underlying concept. I will start in this article by explaining the concept of futures and promises. The ‘future’ concept is available in many languages, including Javascript and C#, under the name async / await, or in Java and Scala, under the name ‘future’.
SwiftNIO 가 어떻게 동작하는지 깊게 이해하고 싶으면, 먼저 기본 개념을 이해야 한다.
future와 promise의 개념을 설명하는 이 글로써 시작한다.

future 개념은 많은 언어에서 사용가능하다,
javascript와 c#에서는 aync/await 라는 이름으로, java와 scala에서는 future라는 이름으로.

 

Futures and promises

Futures and promises are a set of programming abstractions to write asynchronous code. The principle is quite simple: Your asynchronous code will return a promise instead of the final result. The code calling your asynchronous function is not blocked and can do other operations before it finally decides to block and wait for the result, if / when it really needs to.
future 와 promise 는 비동기 코드를 작성하기 위한 프로그래밍 추상화의 집합이다.
원칙은 매우 단순하다: 너의 비동기 코드는 최종 결과 대신에 promise를 리턴한다.
너의 비동기 함수를 호출하는 코드는 최종적으로 차단되기 전에 다른 작업을 할수 있고 결과를 기다릴수 있다.

Even if the words ‘futures’ and ‘promises’ are often use interchangeably, there is a slight difference in meaning. They represent different points of view on the same value placeholder. As explained in Wikipedia page:
A future is a read-only placeholder view of a variable, while a promise is a writable, single assignment container which sets the value of the future.
비록 future 와 promise 가 종종 비슷한 의미로 사용되더라도, 의미에 약간 다른점이 있다.
그것들은 같은 값 표시자에 대한 다른 시각을 나타낸다.
위키피디아 페이지의 설명은:
future는 변수의 읽기전용 표시자이고,
반면 promise는 쓰기가능한 미래의 값을 설정할수 있는 단일 할당 컨테이너이다.

In other words, the future is what the client code receives and can use as a handler to access a future value when it has been defined. The promise is the handler the asynchronous code will keep to write the value when it is ready and thus fulfill the promise by returning the future value.
다른말로 하자면, future는 클라이언트 코드가 수신하고, 미래의 값이 정의되었을때 접근할수 있는 핸들러로서 사용가능하다.
promise는 비동기 코드가 값이 준비되었을때 쓰기위해 유지하는 핸들러이고, 미래 값을 반환해서 promise를 수행한다.

Let’s see in practice how futures and promises work.
future와 promise가 어떻게 동작하는지 살펴보자.

SwiftNIO comes with a built-in futures and promises library. The code lies in EventLoopFuture. Don’t be fooled by the name: It is a full-featured ‘future’ library that you can use in your code to handle asynchronous operations.
SwiftNIO 는 내장된 future와 promise 라이브러리를 제공한다.
코드는 EventLoopFuture 여기에 있다.
이름에 현혹되지 마라: 이것은 완전한 기능의 future 라이브러리이고 너의 코드에 비동기 동작을 제어하기 위해 사용할수 있다.

Let’s see how you can use it to write asynchronous code, without specific reference to SwiftNIO-oriented networking operations.
비동기 코드를 작성하기 위해서 어떻게 사용하는지 보자 (SwiftNIO 지향 네트워크 작업에 대한 특정 참조없이)

Note: The examples in this blog post should work both on macOS and Linux.
Note: 이 블로그에 게시된 예제들은  macOS와 Linux 둘다에서 동작한다.

 

Anatomy of SwiftNIO future / promise implementation

Step 1: Create an EventLoopGroup

The basic skeleton for our example is as follow:
우리 예제의 기본 뼈대는 아래와 같다.

import NIO

let evGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)

// Do things

try evGroup.syncShutdownGracefully()

We create an EventLoopGroup and shut it down gracefully at the end. A graceful shutdown means it will properly terminate the asynchronous jobs being executed.
EventLoopGroup 을 만들고, 마지막에 이것을 정상 종료시킨다.
정상종료는 실행중인 비동기 작업들이 올바르게 종료됨을 의미한다.

An EventLoopGroup can be seen as a provider of an execution context for your asynchronous code. You can ask the EventLoopGroup for an execution context: an EventLoop. Basically, each execution context, each EventLoop is a thread. EventLoops are used to provide an environment to run your your concurrent code.
EventLoopGroup은 너의 비동기 코드에 대한 실행 컨텍스트 공급자로 볼수 있다.
EventLoopGroup에 실행 컨텍스트인 EventLoop를 요청할수 있다.
기본적으로 실행 컨텍스트인 각각의 EventLoop는 스레드이다.

EventLoop들은 너의 동시 코드를 실행할수 있는 환경을 제공한다.

In the previous example, we create as many threads as we have cores on our computer (System.coreCount), but the number of threads could be as low as 1.
이전 예제에서, 우리는 우리컴퓨터에 있는 코어 수 만큼 쓰레드를 만들었지만 (System.coreCount) 쓰레드의 최소값은 1 이다.

Step 2: Getting an EventLoop to execute your promise

In SwiftNIO, you cannot model concurrent execution without at least an event loop. For more info on what I mean by concurrency, you can watch Rob Pike excellent talk: Concurrency is not parallelism.
SwiftNIO 에서는 최소한 이벤트 루프 없이 동시 실행을 모델링할수 없다.
내가 의미하는 동시성에 대한 더 많은 정보는, Rob Pike의 훌륭한 강연에서 볼수 있다.

To execute your asynchronous code, you need to ask the EventLoopGroup for an EventLoop. You can use the method next() to get a new EventLoop, in a round-robin fashion.
너의 비동기 코드를 실행하려면, EventLoopGroup에 EventLoop를 요청해야만 한다.
next() 메소드를 이용해서 라운드-로빈 방식으로 새로운 EventLoop를 얻을수 있다.

The following code gets 10 event loops, using the next() method and prints the event loops information.
다음 코드는 next() 메소드를 사용해서 10개의 이벤트 루프들을 얻고,이벤트 루프들의 정보를 출력한다.

import NIO

print("System cores: \(System.coreCount)\n")
let evGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)

for _ in 1...10 {
    let ev = evGroup.next()
    print(ev)
}

// Do things

try evGroup.syncShutdownGracefully()

On my system, with 8 cores, I get the following result:
8 코어를 가진 내 시스템에서는 다음과 같은 결과를 얻었다.

System cores: 8

SelectableEventLoop { selector = Selector { descriptor = 3 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 4 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 5 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 6 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 7 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 8 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 9 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 10 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 3 }, scheduledTasks = PriorityQueue(count: 0): [] }
SelectableEventLoop { selector = Selector { descriptor = 4 }, scheduledTasks = PriorityQueue(count: 0): [] }

The description represents the id of the EventLoop. As you can see, you can use 8 different loops before being assigned again an existing EventLoop from the same group. As expected, this matches our number of cores.
설명은 EventLoop의 id를 나타낸다.
너가 볼수 있듯이, 같은 그룹에서 기존 EventLoop 에 재할당 되기전에 너는 8개의 다른 루프를 이용할수 있다.
예상했듯이 이것은 우리의 코어수와 일치한다.

Note: Under the hood, most EventLoops are designed using NIOThread, so that the implementation can be cross-platform: NIO threads are build using Posix Threads. However, some platform specific loops, like NIO Transport service, are free from multiplatform constrains and are using Apple Dispatch library. It means, if you are targeting only MacOS, you can thus use SwiftNIO futures and promises directly with Dispatch library. Libdispatch being shipped with Swift on Linux now, it could also work there, but I did not test it yet.
Note: 내부적으로 대부분의 EventLoops는 NIOThread를 사용하도록 디자인 되어 있어서,
그 구현은 크로스플랫폼이 될수 있다:NIO thread들은 Posix 쓰레드를 사용하여 만들어졌다.
그렇지만 NIO Transport service 같은 일부 플랫폼은 루프들을 특정하고, 다중플랫폼 제약사항에서 자유로우며, Apple Dispatch 라이브러리를 사용한다.
너가 오로지 MacOS만을 대상으로 한다면, SwiftNIO future와 promise를 직접 Dispatch 라이브러리와 함께 사용할수 있다는 뜻이다.
Libdispatch는 현재 Linux에 스위프트와 함께 포함되어 있고, 동작할것이지만, 나는 아직 테스트 못해봤다.

Step 3: Executing async code

If you just want to execute async code without needing to wait back for a result, you can just pass a function closure to the EventLoop.execute(_:):
만약 너가 결과를 돌려받기 위해 기다릴 필요가 없는 비동기화 코드를 실행하기 원하면,
EventLoop.execute(_:) 에 함수 클로저를 전달만 하면 된다.

import NIO

print("System cores: \(System.coreCount)\n")
let evGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)

let ev = evGroup.next()

ev.execute {
    print("Hello, ")
}
// sleep(1)
print("world!")

try evGroup.syncShutdownGracefully()

In the previous code, the order in which “Hello, ” and “world!” are displayed is undetermined.
이 코드에서, "Hello," 와 "world!" 가 표시되는 순서는 미정이다.

Still, on my computer, it is clear that they are not executed in order. The print-out in the execute block is run asynchronously, after the execution of the print-out in the main thread:
내 컴퓨터에서, 순서대로 실행되지 않는다는것을 분명하다.
메인 쓰레드에서 출력의 실행이 끝난후에, 샐행 블록 안에 있는 출력이 비동기적으로 실행된다

You can uncomment the sleep(1) function call to insert one second of delay before the second print-out instruction. It will “force” the ordering by delaying the main thread print-out and have “Hello, world!” be displayed in sequence.
sleep(1) 함수의 주석을 풀어서 두번째 출력 지시문 전에 1초의 딜레이를 넣을수 있다.
이것은 메인 쓰레드 출력을 지연시켜 순서를 강제하고, 순서대로 "Hello, world!"가 표시되도록 한다.

Step 4: Waiting for async code execution

Adding timers in your code to order code execution is a very bad practice. If you want to wait for the async code execution, that’s where ‘futures’ and ‘promises’ comes into play.
코드 실행의 순서를 위해서 타이머를 추가하는것은 매우 안좋은 습관이다.
(이석우 추가 : 위에서 사용한 sleep() 함수 같은거)

너가 비동기 코드의 실행을 기다리길 원한다면, 바로 future와 promise 가 있다.

The following code will submit an async code to run on an EventLoop. The asyncPrint function will wait for a given delay in the EventLoop and then print the passed string.
다음에 보여줄 코드는 EventLoop 위에서 비동기 코드가 실행된다.
asyncPrint 함수는 EventLoop에서 주어진 지연만큼 기다리고 나서, 전달된 문자열을 출력한다.

When you call asyncPrint, you get a promise in return. With that promise, you can call the method wait() on it, to wait for the completion of the async code.
asyncPrint 를 호출할때, promise를 반환받는다.
비동기 코드가 완료될때까지 기다리기 위해, 그 promise 의 wait() 메소드를 호출할수 있다.

import NIO

// Async code
func asyncPrint(on ev: EventLoop, delayInSecond: Int, string: String) -> EventLoopFuture<Void> {
    // Do the async work
    let promise = ev.submit {
        sleepAndPrint(delayInSecond: 1, string: string)
        return
    }

    // Return the promise
    return promise
}

func sleepAndPrint(delayInSecond: UInt32, string: String) {
    sleep(delayInSecond)
    print(string)
}

// ===========================
// Main program

print("System cores: \(System.coreCount)\n")
let evGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)

let ev = evGroup.next()

let future = asyncPrint(on: ev, delayInSecond: 1, string: "Hello, ")

print("Waiting...")
try future.wait()

print("world!")

try evGroup.syncShutdownGracefully()

The print-out will pause for one second on the “Waiting…” message and then display the “Hello, ” and “world!” messages in order.
"Waiting..." 메세지 후에 1초 동안 멈추고, "Hello," 와 "world!" 메세지가 순서대로 표시된다.

Step 5: Promises and futures result

When you need a result, you need to return a promise that will give you more than just a signaling letting you know the processing is done. Thus, it will not be a promise of a Void result, but can return a more complex promise.
결과가 필요하다면, 단순히 작업이 끝났음을 알리는것보다는, 더 많은 정보를 제공할수 있는 promise를 반환해야만 한다.
따라서 Void 결과의 promise가 아니고, 좀 더 복잡한 promise를 리턴할수 있다.
(이석우 추가 : 위의 예제에서는 EventLoopFuture<Void> 리턴타입이었는데, Void 말고 다른 타입을 넣으려나 보다)

First, let’s see a promise of a simple result that cannot fail. In your async code, you can return a promise that will return the result of factorial calculation asynchronously. Your code will promise to return a Double and then submit the job to the EventLoop.
먼저, 실패하지 않는 간단한 결과의 promise를 보자.
너의 비동기 코드에서, 비동기적으로 팩토리얼 계산의 결과를 반환하는 promise를 반환할수 있다.
Double 값 반환을 약속하는 너의 코드는 EventLoop에 제출된다.

import NIO

// Async code
func asyncFactorial(on ev: EventLoop, n: Double) -> EventLoopFuture<Double> {
    // Do the async work
    let promise = ev.submit { () -> Double in
        return factorial(n: n)
    }

    // Return the promise
    return promise
}

// I would use a BigInt library to go further small number factorial calculation
// but I do not want to introduce an external dependency.
func factorial(n: Double) -> Double {
    if n >= 0 {
        return n == 0 ? 1 : n * factorial(n: n - 1)
    } else {
        return 0 / 0
    }
}

// ===========================
// Main program

print("System cores: \(System.coreCount)\n")
let evGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)

let ev = evGroup.next()

let n: Double = 10
let future = asyncFactorial(on: ev, n: n)

print("Waiting...")

let result = try future.wait()

print("fact(\(n)) = \(result)")

try evGroup.syncShutdownGracefully()

The code will be executed asynchronously and the wait() method will return the result:
이 코드는 비동기적으로 실행되고 wait() 메소드는 결과를 반환한다.

System cores: 8

Waiting...
fact(10.0) = 3628800.0

 

Step 6: Success and error processing

If you are doing network operations, like downloading a web page for example, the operation can fail. You can thus handle more complex result, that can be either success or error. SwiftNIO offers a ready made type call ResultType.
웹페이지 다운로드 같은 네트워크 작업을 한다면, 그 작업은 실패할수도 있다.
따라서 성공 또는 실패와 같은 좀 더 복잡한 결과를 다룰수 있어야 한다.
SwiftNIO 는 미리 준비된 ResultType을 제공한다.

In the next example, we will show an async function performing an asynchronous network operation using callbacks and returning a future result of ResultType. The ResultType will wrap either the content of the downloaded page or a failure callback.
다음 예제에서, callback을 사용해서 네트워크 작업을 비동기적으로 수행하고
ResultType의 future 결과를 반환하는 비동기 함수를 보여준다.
ResultType은 다운로드 페이지의 내용 또는 실패 콜백중 하나를 감싸고 있다.

import NIO
import Foundation

// =============================================================================
// MARK: Helpers

struct CustomError: LocalizedError, CustomStringConvertible {
    var title: String
    var code: Int
    var description: String { errorDescription() }

    init(title: String?, code: Int) {
        self.title = title ?? "Error"
        self.code = code
    }

    func errorDescription() -> String {
        "\(title) (\(code))"
    }
}

// MARK: Async code
func asyncDownload(on ev: EventLoop, urlString: String) -> EventLoopFuture<String> {
    // Prepare the promise
    let promise = ev.makePromise(of: String.self)

    // Do the async work
    let url = URL(string: urlString)!

    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        print("Task done")
        if let error = error {
            promise.fail(error)
            return
        }
        if let httpResponse = response as? HTTPURLResponse {
            if (200...299).contains(httpResponse.statusCode) {
                if let mimeType = httpResponse.mimeType, mimeType == "text/html",
                    let data = data,
                    let string = String(data: data, encoding: .utf8) {
                    promise.succeed(string)
                    return
                }
            } else {
                // TODO: Analyse response for better error handling
                let httpError = CustomError(title: "HTTP error", code: httpResponse.statusCode)
                promise.fail(httpError)
                return
            }
        }
        let err = CustomError(title: "no or invalid data returned", code: 0)
        promise.fail(err)
    }
    task.resume()

    // Return the promise of a future result
    return promise.futureResult
}

// =============================================================================
// MARK: Main

print("System cores: \(System.coreCount)\n")
let evGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)

let ev = evGroup.next()

print("Waiting...")

let future = asyncDownload(on: ev, urlString: "https://www.process-one.net/en/")
future.whenSuccess { page in
    print("Page received")
}
future.whenFailure { error in
    print("Error: \(error)")
}

// Timeout: As processing is async, we can handle timeout by just waiting in
// main thread before quitting.
// => Waiting 10 seconds for completion
sleep(10)

try evGroup.syncShutdownGracefully()

The previous code will either print “Page received” when the page is downloaded or print the error. As your success handler receives the page content itself, you could do something with it (print it, analyse it, etc.)
이 코드는 페이지가 다운로드 되면 "Page received" 를 출력하거나 또는 에러를 출력한다.
성공 핸들러가 페이지 내용 자체를 받기 때문에, 이걸로 뭔가를 할수 있다 (출력하거나, 분석하거나, 기타등등)

Step 7: Combining async work results

Where promises really shine is when you would like to chain several async calls that depend on each other. You can thus write a code that appear logically in a sequence, but that is actually asynchronous.
서로 의존적인 여러개의 비동기 호출을 묶을때 promise가 더욱 빛난다.
따라서 논리적으로 순차적인 코드를 작성할수 있지만, 사실은 비동기적이다.

In the following code, we reuse the previous async download function and process several pages by counting the number of div elements in all pages.
다음 코드에서, 이전의 다운로드 함수를 재사용해서 여러개의 페이지를 처리하고 모든 페이지안의 div 요소를 센다.

By wrapping this processing in a reduce function, we can download all web pages in parallel. We receive the page data as they are downloaded and we keep track of a counter of the number of div per page. Finally, we return the total as the future result.
reduce 함수안에서 이 처리를 감싸서, 모든 웹 페이지들을 병렬적으로 다운로드 할수 있다.
우리는 다운로드된 페이지 데이타를 받고 각 페이지의 div 숫자 카운터를 추적할수 있다.
최종적으로, future 결과로써 총갯수를 반환한다.

This is a more involved example that should give you a better taste of what developing with futures and promises looks like.
이것은 future와 promise와 함께 개발하는 모습을 더 잘 맛볼수 있는 복잡한 예제다.

import NIO
import Foundation

// =============================================================================
// MARK: Helpers

struct CustomError: LocalizedError, CustomStringConvertible {
    var title: String
    var code: Int
    var description: String { errorDescription() }

    init(title: String?, code: Int) {
        self.title = title ?? "Error"
        self.code = code
    }

    func errorDescription() -> String {
        "\(title) (\(code))"
    }
}

// MARK: Async code
func asyncDownload(on ev: EventLoop, urlString: String) -> EventLoopFuture<String> {
    // Prepare the promise
    let promise = ev.makePromise(of: String.self)

    // Do the async work
    let url = URL(string: urlString)!

    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        print("Loading \(url)")
        if let error = error {
            promise.fail(error)
            return
        }
        if let httpResponse = response as? HTTPURLResponse {
            if (200...299).contains(httpResponse.statusCode) {
                if let mimeType = httpResponse.mimeType, mimeType == "text/html",
                    let data = data,
                    let string = String(data: data, encoding: .utf8) {
                    promise.succeed(string)
                    return
                }
            } else {
                // TODO: Analyse response for better error handling
                let httpError = CustomError(title: "HTTP error", code: httpResponse.statusCode)
                promise.fail(httpError)
                return
            }
        }
        let err = CustomError(title: "no or invalid data returned", code: 0)
        promise.fail(err)
    }
    task.resume()

    // Return the promise of a future result
    return promise.futureResult
}

// =============================================================================
// MARK: Main

print("System cores: \(System.coreCount)\n")
let evGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)

var futures: [EventLoopFuture<String>] = []

for url in ["https://www.process-one.net/en/", "https://www.remond.im", "https://swift.org"] {
    let ev = evGroup.next()
    let future = asyncDownload(on: ev, urlString: url)
    futures.append(future)
}


let futureResult = EventLoopFuture.reduce(0, futures, on: evGroup.next()) { (count: Int, page: String) -> Int in
    let tok =  page.components(separatedBy:"<div")
    let p_count = tok.count-1
    return count + p_count
}

futureResult.whenSuccess { count in
    print("Result = \(count)")
}
futureResult.whenFailure { error in
    print("Error: \(error)")
}

// Timeout: As processing is async, we can handle timeout by just waiting in
// main thread before quitting.
// => Waiting 10 seconds for completion
sleep(10)

try evGroup.syncShutdownGracefully()

This code actually builds a pipeline as follows:
이 코드는 실질적으로 아래와 같은 흐름으로 만들어졌다.

 

Conclusion

Futures and promises are at the heart of SwiftNIO design. To better understand SwiftNIO architecture, you need to understand the futures and promises mechanism.
future와 promise는 SwiftNIO 디자인의 핵심이다.
SwiftNIO 아키텍처를 더 잘 이해하기 위해서는, future와 promise 메커니즘을 이해해야만 한다.

However, there is more concepts that you need to master to fully understand SwiftNIO. Most notably, inbound and outbound channels allow you to structure your networking code into reusable components executed in a pipeline.
하지만 SwiftNIO 를 완전히 이해라기 위해서 습득해야만 하는 많은 개념들이 있다.
특히 inbound 와 outbound 채널들은 너의 네트워크 코드를 파이프라인 안에서 실행되는 재사용가능한 구성요소로 만들수 있게 해준다.

I will cover more SwiftNIO concepts in a next blog post. In the meantime, please send us your feedback :)

반응형

'MacOS 초보' 카테고리의 다른 글

mac app. terminate.  (0) 2021.04.13
Posted by 돌비
,