---
title: "Multi-threading"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Multi-threading}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

```{r setup}
library(a5R)
```

a5R can parallelise vectorised operations using multiple threads via
[rayon](https://docs.rs/rayon). By default a5R uses a single thread, so
there is zero overhead. You opt in to parallelism when you need it.

## Setting the thread count

```{r}
# Check the current setting (default: 1)
a5_get_threads()

# Use 4 threads
a5_set_threads(4)
a5_get_threads()
```

```{r, include = FALSE}
a5_set_threads(1)
```

You can also set threads at package load time via an R option or environment
variable - useful for scripts and batch jobs:

```r
# In .Rprofile or at the top of a script
options(a5R.threads = 4)

# Or as an environment variable
# Sys.setenv(A5R_NUM_THREADS = 4) 
```

`a5_set_threads()` invisibly returns the previous value, making temporary
changes easy:

```{r}
old <- a5_set_threads(4)
# ... parallel work ...
a5_set_threads(old)
```

## What gets parallelised

Threading applies to **vectorised** functions that process each element
independently:

| Function | Per-element cost | Benefit |
|---|---|---|
| `a5_cell_to_boundary()` | Heavy (boundary + WKT/WKB) | High |
| `a5_grid()` | Heavy (boundary filtering) | High |
| `a5_lonlat_to_cell()` | Moderate (projection) | High |
| `a5_cell_distance()` | Moderate (2x projection + distance) | Medium |
| `a5_cell_to_lonlat()` | Moderate (reverse projection) | Medium |
| `a5_cell_to_parent()` | Light (bit ops + hex) | Low |
| `a5_get_resolution()` | Light (bit ops) | Low |
| `a5_is_cell()` | Light (hex parse) | Low |

Scalar and bulk operations (`a5_cell_to_children()`, `a5_compact()`,
`a5_cell_area()`, etc.) are unaffected --- they are already fast or delegate
to algorithms that don't parallelise element-wise.

## When is it worthwhile?

Threading has a small fixed overhead (thread synchronisation, memory
allocation for intermediate results). For small vectors this can outweigh
the benefit. As a rule of thumb:

- **< 1,000 elements**: stick with 1 thread
- **1,000--10,000**: 2-4 threads helps for heavy ops (boundary, indexing)
- **> 10,000**: use as many threads as you have cores

Here's a quick comparison on 100k cells:

```{r, eval = FALSE}
cells <- a5_grid(c(-10, 50, 10, 60), resolution = 12)
length(cells)
#> [1] 704259

a5_set_threads(1)
system.time(a5_cell_to_boundary(cells, format = "wkt"))
#>   user  system elapsed
#>  3.124   0.000   3.122 

a5_set_threads(8)
system.time(a5_cell_to_boundary(cells, format = "wkt"))
#>   user  system elapsed
#> 6.195   1.289   1.667 
```

Note that `user` time increases (total CPU work across all threads) while
`elapsed` (wall-clock) time decreases --- that's the parallelism at work.

## Thread safety

a5R uses a dedicated rayon thread pool, separate from R's own
parallelism. It is safe to use alongside `future`, `mirai`, etc. but think 
carefully about this nested parallelism as it can, if overloaded, degrade 
performance.

The thread pool is rebuilt each time you call `a5_set_threads()`, so
changing the count mid-session is fine (and cheap) but not free - ideally, just
do it once at the start of your workflow rather than toggling per-call.
