future

repository·develop·Indexed 21 days ago

https://github.com/futureverse/future

A unified framework for parallel and distributed processing in R. It provides a consistent API to evaluate expressions asynchronously across various backends—including sequential, multisession, multicore, and cluster—with minimal changes to source code. The package supports both implicit futures via the %<-% operator and explicit futures using future() and value(), and integrates with the futurize package to parallelize functional programming patterns like lapply and purrr::map.

Tokens
18.5K
Snippets
85
Records
103
Agent score
76%

What's inside future

  1. What is a future and how does it work?

    develop

    In programming, a future is an abstraction for a value that may become available at some point in the future. A future exists in one of two states:

    1. Unresolved: The value is not yet available. If you attempt to access the value while the future is unresolved, the current R process will block (wait) until the future is resolved.
    2. Resolved: The value is available and can be accessed instantaneously.

    How and when a future is resolved depends on the chosen strategy (via plan()). Strategies can range from sequential (evaluating in the current R session) to asynchronous (evaluating in parallel on the local machine or on a compute cluster).

    Using asynchronous futures allows the main R process to continue executing other tasks while the future is being resolved in the background, providing a powerful mechanism for parallel and distributed processing.

  2. Configure evaluation topologies with nested futures

    develop

    By default, futures follow a "flat topology" where all futures are created in the same environment. However, you can use a "nested topology" where a future creates other futures internally.

    Important Behavior:

    • By default, nested futures use sequential evaluation to prevent accidentally spawning too many background processes (e.g., in recursive calls).
    • To enable parallel evaluation at multiple levels, you must provide a list of strategies to plan() and use tweak() to explicitly force the number of workers at each level.
    # Default behavior: top-level is multisession, nested is sequential
    plan(list(multisession, sequential))
    
    # Forcing nested multisession evaluation
    plan(list(tweak(multisession, workers = 2), tweak(multisession, workers = 2)))
  3. Understand and manage global objects in futures

    develop

    When evaluating R expressions asynchronously (parallel) or via lazy evaluation, future must identify and pass 'global' (free) objects to the evaluator.

    • Automatic Identification: future uses the globals package to perform static code inspection to identify global variables.
    • Package Globals: If a global is defined within a package, future ensures that the corresponding package is attached during evaluation rather than exporting the variable itself. This saves memory and bandwidth.
    • Manual Specification: Static analysis can fail in complex corner cases. If globals are missing (causing runtime errors) or incorrectly identified, you can manually specify them using the globals argument in future creation functions.

    You can specify globals as a character vector of names or as a named list of name-value pairs.

    # Manual specification examples:
    # Using names
    globals = c("a", "slow_sum")
    
    # Using name-value pairs
    globals = list(a = 42, slow_sum = my_sum)
  4. Manage global variables in futures

    develop

    When evaluating expressions asynchronously (parallel) or via lazy evaluation, global (free) objects must be identified and passed to the evaluator. The future package uses the globals package to automatically identify these variables via static-code inspection.

    Key Behaviors:

    • Automatic Capture: Identified globals are captured and exported to the evaluating process.
    • Package Attachment: If a global is defined within a package, future ensures that package is attached in the evaluator instead of exporting the variable, saving memory and bandwidth.
    • Manual Specification: If automatic identification fails (a known limitation of static inspection), you can manually specify globals using names or name-value pairs.

    Manual Specification Options:

    • globals = c("var1", "var2") (vector of names)
    • globals = list(var1 = value1, var2 = value2) (name-value pairs)
  5. Avoid non-exportable references in futures

    develop

    When using future-based packages (like future.apply, future.batchtools, or future.callr), you must ensure that the global objects used in your future expressions do not contain non-exportable references.

    A common error is Error: Detected a non-exportable reference ('externalptr') in one of the globals (<unknown>) used in the future expression. This typically occurs when an object contains an externalptr (external pointer), such as a database connection, a file connection, or a processx_connection. These objects cannot be serialized and sent to worker nodes.

    To fix this, ensure that any objects passed into functions like future_by(), flapply(), or future_lapply() are composed of standard R data types that can be safely exported to parallel workers.

    # Example of what causes the error:
    # If 'conn' is a database connection (externalptr), this will fail:
    plan(multisession)
    future_lapply(1:10, function(i) use_connection(conn))
    
    # Error message:
    # Error: Detected a non-exportable reference ('externalptr') in one of the globals (<unknown>) used in the future expression
  6. How nested futures and evaluation topologies work

    develop

    A flat topology is when all futures are created in the same environment. A nested topology is when one future creates other futures internally.

    By default, nested futures use sequential evaluation to prevent an accidental explosion of background processes (e.g., in recursive calls).

    To define a specific hierarchy of backends, pass a list to plan(). For example, plan(list(multisession, sequential)) will run the first level of futures in parallel, but any futures created inside those workers will run sequentially.

    To allow nested futures to also run in parallel, you must use tweak() to explicitly specify the number of workers at each level of the hierarchy.

    # Explicitly defining a nested topology: 
    # Level 1: Multisession, Level 2: Sequential
    plan(list(multisession, sequential))
    
    # Allowing nested multisession workers by tweaking
    plan(list(tweak(multisession, workers = 2), tweak(multisession, workers = 2)))
  7. Create implicit vs explicit futures

    develop

    You can create futures using two different styles:

    1. Implicit Futures

    Uses the %<-% operator to create a future and a promise to its value in a single assignment step. This style is similar to standard R assignment (<-).

    v %<-% { expr }

    2. Explicit Futures

    Uses the future() function to create a future object and the value() function to retrieve the result. This style makes it explicitly clear in the code that asynchronous processing is occurring.

    f <- future({ expr })
    v <- value(f)

    Both styles work equally well, but the explicit style can reduce the risk of mistakes when working with asynchronous backends.

    library(future)
    # Explicit style
    f <- future({ 3.14 })
    v <- value(f)
    
    # Implicit style
    v %<-% { 3.14 }
  8. Consistent behavior across future backends

    develop

    The Future API is designed so that code remains agnostic of the underlying backend. Whether running sequentially or on a remote cluster, the following behaviors are enforced:

    1. Local Environment Evaluation: All evaluations are performed within a local environment (using local({ expr })). This ensures that assignments made inside a future do not affect the calling environment.
    2. Global Variable Identification: When a future is constructed, global variables are identified and exported to the evaluating process. To prevent accidental exporting of massive objects, there is a built-in size threshold (configurable via future.options).
    3. Single Evaluation: Future expressions are evaluated only once. Once the value or error is collected, it is cached for all subsequent requests.
    # Demonstrating that assignments inside a future do not affect the calling environment
    plan(sequential)
    > a <- 1
    > x %<-% {
    +     a <- 2
    +     2 * a
    + }
    > x
    [1] 4
    > a
    [1] 1
  9. Core behaviors of the Future API

    develop

    The Future API is designed to encapsulate differences between strategies so that code behaves consistently regardless of whether it is running locally or on a remote cluster. This allows developers to prototype with sequential evaluation and switch to asynchronous processing later without changing the core logic.

    All futures follow these rules:

    1. Local Environment Evaluation: Expressions are evaluated using local({ expr }), meaning assignments inside a future do not affect the calling environment.
    2. Global Variable Handling: When a future is created, global variables are identified. For asynchronous evaluation, they are exported to the worker process. For lazy = TRUE sequential futures, they are "frozen" (cloned to a local environment). There is a built-in size threshold for globals to prevent accidental large exports (configurable via future.options).
    3. Single Evaluation: A future expression is evaluated only once. Once the value or error is collected, it is available for all subsequent requests.
    plan(sequential)
    a <- 1
    x %<-% {
        a <- 2
        2 * a
    }
    x
    [1] 4
    a
    [1] 1
  10. Create futures using implicit and explicit styles

    develop

    The future package provides two ways to create futures: an implicit style and an explicit style. Both styles behave identically in terms of execution, but they differ in syntax and code readability.

    Implicit Style

    This style is most similar to standard R assignment. You replace the standard assignment operator <- with %<-%. This creates a future and a promise to its value.

    Explicit Style

    This style makes it very clear to anyone reading the code that a future is being used, which reduces the risk of accidentally assuming code is running sequentially. It uses two distinct steps: creating the future and then retrieving its value.

    • f <- future({ expr }): Creates the future.
    • v <- value(f): Retrieves the value (blocks if the future is not yet resolved).

    Note: When using futures, output (like cat()) is typically relayed after the future is resolved (when the value is queried), rather than during the evaluation of the expression.

    # Implicit style
    library(future)
    v %<-% {
      cat("Hello world!\n")
      3.14
    }
    
    # Explicit style
    library(future)
    f <- future({
      cat("Hello world!\n")
      3.14
    })
    v <- value(f)