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
- How to count cores with detectCores()
- How to run tasks with makeCluster() and parLapply()
- Why you must call stopCluster() when done
- How foreach + doParallel parallelize a loop
- How future and furrr unify parallel backends
- When parallelism helps and when it hurts
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] 72️⃣ 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 163️⃣ 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 16Your 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 125Mini-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 hereCommon Errors (and the fix)
- Forgetting stopCluster() — orphaned workers keep eating memory; always shut the cluster down.
- "object not found" on a worker — workers start fresh; export variables with clusterExport() and load packages with .packages in foreach.
- %dopar% runs sequentially — you forgot to register a backend with registerDoParallel().
- Slower than sequential — the task is too small; the overhead of workers exceeds the savings, so keep it sequential.
Pro Tips
- 💡 Leave a core free: use detectCores() - 1 workers so the system stays responsive.
- 💡 Set parallel seeds: use clusterSetRNGStream() or future's seeding for reproducible random draws.
- 💡 Prefer future for portability: one plan() change switches between laptop and cluster.
📋 Quick Reference — Parallel R
| Task | R Syntax |
|---|---|
| Count cores | detectCores() |
| Start / stop cluster | cl <- makeCluster(4); stopCluster(cl) |
| Parallel apply | parLapply(cl, x, f) |
| Parallel loop | foreach(i = x, .combine = c) %dopar% { ... } |
| Register backend | registerDoParallel(cl) |
| future map | plan(multisession); future_map(x, f) |
🎉 Lesson Complete!
- ✅ detectCores() reports available cores
- ✅ makeCluster() + parLapply() run tasks in parallel
- ✅ Always stopCluster() to free workers
- ✅ foreach + doParallel parallelize loops
- ✅ future and furrr unify backends; parallelize only expensive, independent work
- ✅ You've reached the end of the R course — congratulations!
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
- Previous: Interactive Plots with plotly
- Quick reference: R cheat sheet
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.