Parallel Computing in R

Reviewed & published by Brayan K

R runs on one core by default, but many tasks are made of independent pieces. Parallel computing spreads those pieces across cores to finish the same work in a fraction of the time.

Part of the free R course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end of this lesson you'll use the parallel package, run loops with foreach and doParallel, reach for future and furrr, and know when parallelism actually pays off.

What You'll Learn in This Lesson

1️⃣ How Many Cores? detectCores()

The base parallel package ships with R. detectCores() reports how many logical cores you have; leaving one free keeps the rest of the system responsive.

library(parallel)

# How many cores can we use? detectCores() reports logical cores.
detectCores()        # -> e.g. [1] 8

# A polite default: leave one core free for the rest of the system.
n_workers <- detectCores() - 1
n_workers            # -> e.g. [1] 7

2️⃣ Clusters and parLapply()

makeCluster() starts worker processes; parLapply() runs a function over a list across them — a parallel lapply(). Always finish with stopCluster().

library(parallel)

# 1) Start a cluster of worker processes.
cl <- makeCluster(2)

# 2) parLapply() is a parallel lapply() across the workers.
#    Each element is processed independently -- embarrassingly parallel.
slow_square <- function(x) { Sys.sleep(0.1); x^2 }
result <- parLapply(cl, 1:4, slow_square)

# 3) Always release the workers when done.
stopCluster(cl)

unlist(result)       # -> [1]  1  4  9 16

3️⃣ Loops with foreach + doParallel

foreach writes loops that return results; doParallel registers a backend so %dopar% runs iterations in parallel. .combine sets how results are glued together.

library(foreach)
library(doParallel)

# Register a parallel backend, then foreach runs iterations across cores.
cl <- makeCluster(2)
registerDoParallel(cl)

# .combine says HOW to glue the per-iteration results together.
out <- foreach(i = 1:4, .combine = c) %dopar% {
  i^2
}

stopCluster(cl)
out                  # -> [1]  1  4  9 16
# Use %do% instead of %dopar% to run the same loop sequentially.

4️⃣ One API: future & furrr

The future framework lets you choose a backend once with plan(); furrr's future_map() is a parallel drop-in for purrr::map().

library(furrr)

# The future framework gives one API over many backends.
# plan() chooses how to run; multisession uses background R processes.
plan(multisession, workers = 2)

# future_map() is a parallel drop-in for purrr::map().
result <- future_map_dbl(1:4, ~ .x^2)

plan(sequential)     # reset to normal afterwards
result               # -> [1]  1  4  9 16

Your turn. Fill in the ___ blank, run it, and compare with the expected output.

library(parallel)

# 🎯 YOUR TURN — replace each ___ then run it.

# 1) Start a cluster with 2 workers
cl <- makeCluster(2)

# 2) Run a parallel lapply that cubes each number 1:5
cubes <- parLapply(cl, 1:5, function(x) x^___)   # 👉 replace ___ with 3

# 3) ALWAYS release the workers
stopCluster(cl)

print(unlist(cubes))

# ✅ Expected output:
# [1]   1   8  27  64 125

Mini-Challenge: Parallel Simulation

Use foreach with a parallel backend to run four independent simulations and collect their means. Independent random draws are the textbook embarrassingly parallel task.

# 🎯 MINI-CHALLENGE: parallelize a simulation with foreach.
#
# 1. library(foreach) and library(doParallel).
# 2. makeCluster(2) and registerDoParallel(cl).
# 3. With foreach(i = 1:4, .combine = c) %dopar% { ... },
#    return the mean of rnorm(1000) for each i.
# 4. stopCluster(cl) and print the four means.
#
# ✅ Example output (values near 0, your exact numbers vary):
#    [1]  0.012 -0.008  0.031 -0.015

# your code here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Parallel R

TaskR Syntax
Count coresdetectCores()
Start / stop clustercl <- makeCluster(4); stopCluster(cl)
Parallel applyparLapply(cl, x, f)
Parallel loopforeach(i = x, .combine = c) %dopar% { ... }
Register backendregisterDoParallel(cl)
future mapplan(multisession); future_map(x, f)

🎉 Lesson Complete!

Practice quiz

Which base R function reports the number of CPU cores?

  • detectCores()
  • numCores()
  • cpus()
  • coreCount()

Answer: detectCores(). parallel::detectCores() returns the number of logical CPU cores available.

What does makeCluster() do in the parallel package?

  • Deletes a cluster
  • Reads a CSV
  • Creates a set of worker processes to run tasks in parallel
  • Plots a dendrogram

Answer: Creates a set of worker processes to run tasks in parallel. makeCluster() starts worker R processes that the main session can send work to.

What is parLapply()?

  • A string function
  • A parallel version of lapply() that runs over a cluster
  • A function to plot lists
  • A way to load packages

Answer: A parallel version of lapply() that runs over a cluster. parLapply() applies a function over a list across cluster workers, like a parallel lapply().

Why should you call stopCluster() when finished?

  • It speeds up detectCores()
  • To save the plot
  • To sort the results
  • To free the worker processes and their resources

Answer: To free the worker processes and their resources. stopCluster() shuts down the workers so their memory and processes are released.

Which combination provides the foreach loop with parallel backends?

  • foreach and doParallel
  • dplyr and tidyr
  • ggplot2 and plotly
  • knitr and rmarkdown

Answer: foreach and doParallel. foreach defines the loop and doParallel registers a parallel backend for it.

In a foreach loop, what does the .combine argument control?

  • The number of cores
  • How the per-iteration results are combined (e.g. c, rbind)
  • The random seed
  • The package to load

Answer: How the per-iteration results are combined (e.g. c, rbind). .combine tells foreach how to aggregate results, such as c, rbind, or cbind.

Which modern packages offer a unified parallel API across backends?

  • stringr and lubridate
  • readr and haven
  • MASS and car
  • future and furrr

Answer: future and furrr. The future framework and furrr (future_map) give a consistent API over many backends.

What kind of task benefits most from parallelism?

  • Tasks where each step depends on the previous one
  • Tiny tasks that finish instantly
  • Embarrassingly parallel tasks with independent pieces
  • Reading a single small file

Answer: Embarrassingly parallel tasks with independent pieces. Independent, embarrassingly parallel tasks split cleanly across cores with little coordination.

Why might parallelizing a very fast task actually be slower?

  • R cannot run in parallel
  • The overhead of starting workers and copying data exceeds the savings
  • detectCores returns zero
  • Parallel code always errors

Answer: The overhead of starting workers and copying data exceeds the savings. Spawning workers and shipping data has overhead; for quick tasks it can outweigh the speedup.

What does furrr's future_map() parallelize?

  • A purrr-style map() over a parallel backend
  • A SQL query
  • A regex match
  • A ggplot

Answer: A purrr-style map() over a parallel backend. future_map() is the parallel drop-in for purrr::map(), running iterations across workers.

Continue this course

Frequently asked questions

What is parallel computing and when does it help in R?

R normally runs on a single CPU core, doing one thing at a time. Parallel computing splits work across multiple cores so several pieces run at once, cutting wall-clock time. It helps most for 'embarrassingly parallel' tasks — jobs made of many independent pieces that do not need to talk to each other, like running the same simulation a thousand times, fitting a model to many subsets, or applying a slow function over a large list. The pieces must be independent; if each step depends on the previous one, there is little to parallelize.

How do the parallel package functions fit together?

The base parallel package gives you the building blocks. detectCores() tells you how many cores you have. makeCluster() starts a set of worker R processes. parLapply() (and friends like parSapply()) ship each element of a list to a worker, run your function, and collect the results — a parallel version of lapply(). When you are finished, stopCluster() shuts the workers down to free memory and processes. A typical pattern is detect, make, parLapply, stop, and forgetting the final stopCluster() leaves orphaned processes behind.

What do foreach, doParallel, and future add?

foreach gives a loop-like syntax that returns results, and with the %dopar% operator it runs iterations in parallel; doParallel registers the cluster as the backend that powers %dopar%. The .combine argument controls how the per-iteration results are stitched together (c, rbind, cbind). The future framework, and furrr built on top of it, take a different angle: you call plan() once to choose a backend (sequential, multisession, multicore, or a cluster) and then future_map() runs purrr-style maps in parallel without changing the rest of your code. future makes switching between sequential and parallel a one-line change.

Why is my parallel code sometimes slower than sequential?

Parallelism is not free. Starting worker processes, loading packages on each one, copying data to them, and collecting results back all take time. For tasks that are already fast, this overhead can exceed the time saved, so the parallel version runs slower. Parallelism pays off when each independent piece does enough work to dwarf the setup cost — heavy simulations, slow model fits, large per-element computations. The fix is to profile first, parallelize the genuinely expensive loops, and keep the number of workers sensible (often cores minus one) rather than maximal.