Skip to content

Demand-driven asynchronous programming in Swift

License

Notifications You must be signed in to change notification settings

dfunckt/swift-futures

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

64 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Futures

Futures is a lightweight, general-purpose library for asynchronous programming in Swift, that provides a set of interoperable primitives to aid development of highly concurrent, performant and safe programs, both on the server and the desktop or mobile. Futures adopts a "pull"-based, demand-driven approach to asynchronous programming which, besides offering astounding performance, makes dealing with traditionally difficult to solve problems such as backpressure, cancellation and threading, trivial.

Documentation / A Quick Example / Requirements / Installation


Why Futures?

There is a swath of libraries for asynchronous programming in Swift already. Many are fine libraries that you should be seriously considering if you're looking for an abstraction that fits your needs. So why should you care about Futures?

You will find that Futures...

  • exposes APIs that feel natural in the context of the language (there is no Subject, thank you).
  • does not gloss over cancellation and backpressure; handling them is a core concern.
  • approaches asynchronous programming holistically, exposing a wide range of primitives: streams, sinks, channels and more, all built on top of the fundamental abstraction of a future. (Kitchens coming soon!)
  • only cares about the completion of a computation and does not artificially terminate streams on failure, but provides extensions to get the same semantics if you so desire.
  • requires no memory allocations for task execution, coordination or communication.
  • aims to be a solid foundation upon which other libraries covering disparate use-cases are built and, to that end, is easily extensible.
  • can be used for UI programming but thrives on the server and supports all platforms Swift itself supports.

Features

  • Low-cost abstractions: The pull-based runtime model of Futures, combined with extensive use of value types and generics, assists the compiler in producing extremely optimized code, effectively removing the abstraction completely.
  • Scalability and performance: Futures can efficiently drive hundreds of thousands of concurrent tasks on a single OS thread and scale almost linearly with the number of CPU cores assigned to the system.
  • Safety and correctness: The simple runtime model and well-thought out APIs ensure reasoning about the lifetime and the context in which your code executes is straightforward.
  • Cancellation and backpressure: In Futures, dealing with cancellation and backpressure is extremely easy, as their solution falls naturally out of the adopted demand-driven approach.

Read on, there's more in store:

  • Soft real-time safety: Futures encourages a clear separation of acquiring the needed resources for a task, from actually performing the task. This separation, combined with the fact that Futures itself requires no locks or memory allocations for task execution, coordination or communication, allows you to write asynchronous code in a soft real-time context such as a high-priority audio thread, like you would everywhere else.
  • Debugability and testability: Seen in the abstract, Futures is a DSL for building state machines, resulting in a system that can be easily inspected. Combined with the demand-driven approach that puts the consumer in control, testing and debugging Futures-based code is straightforward.
  • Flexibility and extensibility: Futures is not intrusive. You may adopt it in your programs incrementally and even then you can opt to use only the parts that best fit your use-case. In addition, you can extend the system by providing custom implementations of core protocols.
  • Zero dependencies: Futures does not depend on anything other than Swift's standard library and compiler.

A Quick Example

Here's a small program that computes the Answer to the Ultimate Question of Life, the Universe, and Everything:

import Futures

let integers = Stream.sequence(0...)
let primes = integers.filter(isPrime)

var answer = primes.buffer(4)
  .map { $0[0] * $0[1] * $0[3] }
  .first(where: isPronic)

print(answer.wait()) // prints 42

Here's the same program contrived enough to showcase several types and concepts prevalent in Futures -- segregating tasks into different execution contexts, inter-task communication with channels, extracting the result from a remote task, and more. Find out more in the documentation. Share and Enjoy.

import Futures

// Create *executors* to spawn *tasks* on. Each QueueExecutor
// is backed by a private serial DispatchQueue.
let deepThought = (
  cpu0: QueueExecutor(label: "CPU 0"),
  cpu1: QueueExecutor(label: "CPU 1"),
  cpu2: QueueExecutor(label: "CPU 2")
)

// Create two *channels* via which values can be communicated
// between parts of the program.
let pipe1 = Channel.makeUnbuffered(itemType: Int.self)
let pipe2 = Channel.makeUnbuffered(itemType: Int.self)

// Spawn a task that produces positive integers on one
// executor and sends the values to one of the channels.
let integers = Stream.sequence(0...)
deepThought.cpu1.submit(integers.forward(to: pipe1))

// Spawn another task on the second executor that receives
// these values, filters out non-prime integers and sends
// the remaining down the second channel.
let primes = pipe1.makeStream().filter(isPrime)
deepThought.cpu2.submit(primes.forward(to: pipe2))

// Spawn a third task on the third executor that receives
// the prime numbers via the channel and performs the actual
// computation of the answer. For this task, we ask for a
// handle back, with which we can get the final result or
// cancel it.
var answer = deepThought.cpu0.spawn(
  pipe2.makeStream()
    .buffer(4)
    .map { $0[0] * $0[1] * $0[3] }
    .first(where: isPronic)
)

// At this point, everything happens asynchronously in
// secondary background threads, so the program would just
// exit. We need the answer first however, so we block the
// current thread waiting for the result of the computation
// which we just print to standard output.
print(answer.wait()) // prints "Result.success(42)"

You'll need the following functions if you want to run the program yourself and experiment:

import Foundation

func isPrime(_ n: Int) -> Bool {
  return n == 2 || n > 2 && (2...(n - 1)).allSatisfy {
    !n.isMultiple(of: $0)
  }
}

func isPronic(_ n: Int) -> Bool {
  let f = floor(Double(n).squareRoot())
  let c = ceil(Double(n).squareRoot())
  return n == Int(f) * Int(c)
}

Getting Started

Requirements

Futures requires Swift 5.0 (or newer) and can be deployed to any of the following platforms:

  • macOS 10.12+
  • iOS 10+
  • tvOS 10+
  • watchOS 3+
  • Ubuntu 16.04+

Installation

To integrate Futures with the Swift Package Manager, add the following line in the dependencies list in your Package.swift:

.package(url: "https://github.com/dfunckt/swift-futures.git", .upToNextMinor(from: "0.1.0"))

Note: Futures is in its early days and its public APIs are bound to change significantly. Until version 1.0, breaking changes will come with minor version bumps.

Then add Futures as a dependency of the targets you wish to use it in. Futures exports two separate modules:

  • Futures: the core library.
  • FuturesSync: an assortment of thread synchronization primitives and helpers. This module is currently highly experimental and its use is discouraged.

Here is an example Package.swift:

// swift-tools-version:5.0
import PackageDescription

let package = Package(
  name: "MyApp",
  products: [
    .executable(name: "MyApp", targets: ["MyTarget"]),
  ],
  dependencies: [
    .package(url: "https://github.com/dfunckt/swift-futures.git", .upToNextMinor(from: "0.1.0")),
  ],
  targets: [
    .target(name: "MyTarget", dependencies: ["Futures"]),
  ]
)

Getting Help

  • Have a general question or need help with your code? Check out questions tagged with #swift-futures in Stack Overflow.
  • Have a feature request? Have a look at the issue tracker for issues tagged with #enhancement. Add a comment describing your use-case on an existing issue if it's already been reported, or open a new one describing the feature and how you think it can help you.
  • Found a bug? Open an issue. Don't forget to mention the version of Futures you observed the bug with and include as much information as possible. Bug reports with minimal code examples that reproduce the issue are much appreciated.

References

License

Futures is licensed under the terms of the MIT license. See LICENSE for details.