What are the trade-offs between rayon::ThreadPoolBuilder::num_threads and build_global for thread pool configuration?
num_threads configures the number of threads in a thread pool without creating it, returning a builder for further configuration, while build_global actually constructs and installs the thread pool as the global default, enabling parallel iterators to use it automatically. num_threads is a configuration step that can be chained with other builder methods before calling build() or build_global(). build_global() is the final action that creates the pool and registers it globally. Understanding this distinction is essential for controlling rayon's parallel execution behaviorānum_threads sets pool size but doesn't create anything, while build_global creates the pool with all configured settings and makes it available to all parallel operations.
The ThreadPoolBuilder API
use rayon::ThreadPoolBuilder;
fn builder_api() {
// Create a builder with default settings
let builder = ThreadPoolBuilder::new();
// num_threads() configures the pool size
// Returns &mut Self for chaining
let builder = builder.num_threads(4);
// build_global() creates and installs the pool
builder.build_global().unwrap();
// Now all parallel operations use this pool
let sum: i32 = (0..100).into_par_iter().sum();
// Uses 4 threads
}num_threads is a configuration method; build_global is an action that creates the pool.
num_threads: Configuration Method
use rayon::ThreadPoolBuilder;
fn num_threads_configuration() {
// num_threads() sets the thread count
// It returns &mut ThreadPoolBuilder for method chaining
let builder = ThreadPoolBuilder::new()
.num_threads(8); // Configure but don't create
// At this point, no threads are created
// The global pool still uses default settings
// Can chain more configuration:
let builder = ThreadPoolBuilder::new()
.num_threads(4)
.thread_name(|i| format!("worker-{}", i))
.stack_size(4 * 1024 * 1024);
// Still no pool created
// Just configuration ready to build
}num_threads returns a builder for further configuration; no pool exists yet.
build_global: Installation Method
use rayon::ThreadPoolBuilder;
fn build_global_usage() {
// build_global() creates the pool and installs it globally
// After this, all par_iter() calls use this pool
ThreadPoolBuilder::new()
.num_threads(4)
.build_global()
.expect("Failed to create thread pool");
// Now the global pool exists with 4 threads
// All parallel operations use it automatically
(0..1000).into_par_iter().for_each(|i| {
// Runs on the configured global pool
println!("Processing {}", i);
});
// Can only call build_global() once
// Subsequent calls return error
}build_global creates and installs the pool; can only be called once per process.
Chaining Configuration
use rayon::ThreadPoolBuilder;
use std::panic::catch_unwind;
fn chaining_configuration() {
// Common pattern: chain all configuration then build
ThreadPoolBuilder::new()
.num_threads(num_cpus::get()) // Set thread count
.thread_name(|i| format!("rayon-worker-{}", i)) // Name threads
.stack_size(2 * 1024 * 1024) // Stack size per thread
.start_handler(|i| {
println!("Thread {} starting", i);
})
.exit_handler(|i| {
println!("Thread {} exiting", i);
})
.panic_handler(|panic_info| {
eprintln!("Thread panicked: {:?}", panic_info);
})
.build_global()
.expect("Failed to build global pool");
// num_threads is just one of many configuration options
// build_global applies all of them
}num_threads is one configuration among many; build_global applies all settings.
Creating a Local ThreadPool
use rayon::ThreadPoolBuilder;
fn local_thread_pool() {
// build() creates a local pool (not global)
let pool = ThreadPoolBuilder::new()
.num_threads(2)
.build()
.expect("Failed to build pool");
// This pool is separate from the global pool
// Use it explicitly:
pool.install(|| {
// This closure runs on the local pool
(0..100).into_par_iter().for_each(|i| {
// Uses only 2 threads
println!("Local pool: {}", i);
});
});
// Outside install(), global pool is used (if configured)
(0..100).into_par_iter().for_each(|i| {
// Uses global pool (or default if not configured)
println!("Global pool: {}", i);
});
}Use build() for a local pool; build_global() for the global default.
The Global Pool Default
use rayon::ThreadPoolBuilder;
fn default_behavior() {
// If you never call build_global():
// Rayon creates a default global pool on first use
// Default pool uses:
// - num_threads = number of CPU cores
// - Default thread names
// - Default stack size
// - No custom handlers
// This uses the implicit default pool:
let sum: i32 = (0..1000).into_par_iter().sum();
// Equivalent to explicit configuration:
ThreadPoolBuilder::new()
.num_threads(num_cpus::get())
.build_global()
.unwrap();
// After explicit build_global, default creation is skipped
}Rayon creates a default pool if you don't configure one explicitly.
Thread Count Trade-offs
use rayon::ThreadPoolBuilder;
fn thread_count_tradeoffs() {
// num_threads(1): Single-threaded execution
ThreadPoolBuilder::new()
.num_threads(1)
.build_global()
.unwrap();
// Pros: No threading overhead, deterministic
// Cons: No parallelism, slow for parallel workloads
// num_threads(num_cpus::get()): Match CPU cores (default)
ThreadPoolBuilder::new()
.num_threads(num_cpus::get())
.build_global()
.unwrap();
// Pros: Good utilization, efficient for CPU-bound work
// Cons: May not be optimal for I/O-bound work
// num_threads(num_cpus::get() * 2): Oversubscription
ThreadPoolBuilder::new()
.num_threads(num_cpus::get() * 2)
.build_global()
.unwrap();
// Pros: Better for I/O-bound work with blocking
// Cons: Context switching overhead, cache pressure
// Choosing the right number depends on workload:
// - CPU-bound: Match physical cores
// - I/O-bound: More threads for blocking operations
// - Memory-bound: Fewer threads to reduce cache pressure
}Thread count affects performance; choose based on workload characteristics.
When to Use build_global vs build
use rayon::ThreadPoolBuilder;
fn global_vs_local() {
// build_global(): Use when
// - You want all parallel iterators to use your config
// - Application-wide settings make sense
// - You control initialization (startup)
// build(): Use when
// - You need multiple pools with different settings
// - Library code that shouldn't affect global state
// - Isolation for specific workloads
// - Different thread counts for different tasks
// Example: Multiple pools
let cpu_pool = ThreadPoolBuilder::new()
.num_threads(4)
.build()
.unwrap();
let io_pool = ThreadPoolBuilder::new()
.num_threads(16) // More threads for I/O
.build()
.unwrap();
cpu_pool.install(|| {
// CPU-intensive work
});
io_pool.install(|| {
// I/O-bound work
});
}Use build_global for application-wide configuration; build for isolated pools.
Error Handling
use rayon::ThreadPoolBuilder;
fn error_handling() {
// build_global() returns Result<(), ThreadPoolBuildError>
let result = ThreadPoolBuilder::new()
.num_threads(4)
.build_global();
match result {
Ok(()) => println!("Global pool created"),
Err(e) => eprintln!("Failed: {:?}", e),
}
// Common error: calling build_global() twice
// First call succeeds
let first = ThreadPoolBuilder::new()
.num_threads(4)
.build_global();
assert!(first.is_ok());
// Second call fails
let second = ThreadPoolBuilder::new()
.num_threads(8)
.build_global();
assert!(second.is_err()); // Global pool already exists
// build() can also fail (e.g., invalid thread count)
let local = ThreadPoolBuilder::new()
.num_threads(0) // Invalid!
.build();
assert!(local.is_err());
}Both build and build_global return Result; handle errors appropriately.
Configuration Options Overview
use rayon::ThreadPoolBuilder;
fn all_options() {
let builder = ThreadPoolBuilder::new()
// Core configuration
.num_threads(4) // Thread count
// Thread customization
.thread_name(|i| format!("worker-{}", i)) // Thread names
.stack_size(4 * 1024 * 1024) // Stack size
// Lifecycle handlers
.start_handler(|i| {
println!("Thread {} starting", i);
})
.exit_handler(|i| {
println!("Thread {} exiting", i);
})
// Error handling
.panic_handler(|info| {
eprintln!("Thread panicked: {:?}", info);
});
// num_threads() is often the most important setting
// Others are for advanced use cases
// After all configuration, create the pool
builder.build_global().unwrap();
}num_threads is the primary configuration; other options are for specialized needs.
num_threads Without build_global
use rayon::ThreadPoolBuilder;
fn without_build_global() {
// num_threads() is meaningless without build() or build_global()
let builder = ThreadPoolBuilder::new().num_threads(4);
// At this point:
// - No threads are created
// - No global pool is configured
// - rayon will still use default global pool
// The builder is just configuration data
// To use the configuration:
let pool = builder.build().unwrap(); // Local pool
// OR
// builder.build_global().unwrap(); // Global pool
}num_threads alone does nothing; you must call build or build_global.
Environment Variables
use rayon::ThreadPoolBuilder;
fn environment_config() {
// Rayon respects RAYON_NUM_THREADS environment variable
// If set, it's used as the default thread count
// Set via:
// RAYON_NUM_THREADS=4 ./myprogram
// This affects the implicit default pool
// But explicit num_threads() overrides it
// Without explicit num_threads:
// let pool = ThreadPoolBuilder::new()
// .build_global()
// .unwrap();
// Uses RAYON_NUM_THREADS or num_cpus
// With explicit num_threads:
ThreadPoolBuilder::new()
.num_threads(8) // Overrides RAYON_NUM_THREADS
.build_global()
.unwrap();
}Explicit num_threads overrides environment variables.
Practical Example: Server Application
use rayon::ThreadPoolBuilder;
fn server_example() {
// At application startup, configure the global pool
fn init_thread_pool() {
// Use number of physical cores, not logical
let physical_cores = num_cpus::get_physical();
ThreadPoolBuilder::new()
.num_threads(physical_cores)
.thread_name(|i| format!("rayon-worker-{}", i))
.panic_handler(|info| {
// Log panics instead of crashing
log::error!("Rayon thread panicked: {:?}", info);
})
.build_global()
.expect("Failed to initialize thread pool");
}
// Later, all parallel operations use the configured pool
fn process_request(data: &[u8]) -> Vec<u8> {
// Uses global pool with configured thread count
data.par_iter()
.map(|&b| b.wrapping_add(1))
.collect()
}
}Configure the global pool once at application startup.
Practical Example: Multiple Thread Pools
use rayon::ThreadPoolBuilder;
fn multiple_pools() {
// Scenario: Different workloads with different optimal thread counts
// CPU-bound pool (fewer threads, more CPU time each)
let cpu_pool = ThreadPoolBuilder::new()
.num_threads(4)
.thread_name(|i| format!("cpu-worker-{}", i))
.build()
.unwrap();
// I/O-bound pool (more threads, handles blocking)
let io_pool = ThreadPoolBuilder::new()
.num_threads(16)
.thread_name(|i| format!("io-worker-{}", i))
.build()
.unwrap();
// Use appropriate pool for each workload
fn cpu_intensive(data: Vec<u64>) -> Vec<u64> {
// Run on CPU pool
cpu_pool.install(|| {
data.into_par_iter()
.map(|n| expensive_calculation(n))
.collect()
})
}
fn io_intensive(urls: Vec<String>) -> Vec<String> {
// Run on I/O pool
io_pool.install(|| {
// More threads for blocking network calls
urls.into_par_iter()
.map(|url| fetch_url(&url))
.collect()
})
}
fn expensive_calculation(n: u64) -> u64 {
// Simulate expensive calculation
n * n
}
fn fetch_url(url: &str) -> String {
// Simulate network fetch
url.to_string()
}
}Use local pools for different workload characteristics.
Thread Safety and Global State
use rayon::ThreadPoolBuilder;
use std::sync::Once;
static INIT: Once = Once::new();
fn safe_initialization() {
// build_global() should only be called once
// Use Once to ensure thread-safe initialization
INIT.call_once(|| {
ThreadPoolBuilder::new()
.num_threads(4)
.build_global()
.expect("Failed to initialize global thread pool");
});
// Alternative: Check if global pool exists
// rayon::current_thread_index() returns Some if pool exists
// but this doesn't let you configure it
// Best practice: Configure at program start before
// any parallel operations
}Initialize the global pool once, preferably at application startup.
Performance Considerations
use rayon::ThreadPoolBuilder;
fn performance() {
// Thread count affects performance:
// Too few threads:
// - Underutilized CPU cores
// - Longer execution time for parallel work
// Too many threads:
// - Context switching overhead
// - Cache thrashing
// - Memory overhead
// Optimal for CPU-bound:
let cpu_pool = ThreadPoolBuilder::new()
.num_threads(num_cpus::get_physical()) // Physical cores
.build()
.unwrap();
// Optimal for I/O-bound:
let io_pool = ThreadPoolBuilder::new()
.num_threads(num_cpus::get() * 2) // More threads
.build()
.unwrap();
// num_threads(0) uses rayon's default (num_cpus)
let default_pool = ThreadPoolBuilder::new()
.build()
.unwrap();
}Choose thread count based on workload type (CPU vs I/O bound).
Accessing the Global Pool After Build
use rayon::ThreadPoolBuilder;
fn after_build() {
// After build_global(), the global pool is fixed
// You cannot change its configuration
ThreadPoolBuilder::new()
.num_threads(4)
.build_global()
.unwrap();
// Trying to build again fails
let result = ThreadPoolBuilder::new()
.num_threads(8)
.build_global();
assert!(result.is_err());
// To "reconfigure", you'd need to:
// 1. Use local pools with install()
// 2. Restart the program
// Workaround: Use local pools for different settings
let other_pool = ThreadPoolBuilder::new()
.num_threads(8)
.build()
.unwrap();
other_pool.install(|| {
// Use different thread count
(0..100).into_par_iter().for_each(|i| {
// Runs on 8-thread pool
});
});
}Global pool is immutable after creation; use local pools for different configurations.
Synthesis
Quick reference:
use rayon::ThreadPoolBuilder;
fn quick_reference() {
// num_threads: Configuration method
// - Sets the thread count
// - Returns &mut Self for chaining
// - Does NOT create threads
// - Does NOT install globally
let builder = ThreadPoolBuilder::new().num_threads(4);
// Nothing created yet
// build_global: Action method
// - Creates the thread pool
// - Installs it as the global default
// - Can only be called once
// - Returns Result<(), ThreadPoolBuildError>
ThreadPoolBuilder::new()
.num_threads(4)
.build_global()
.unwrap();
// Pool created and installed
// Common pattern:
ThreadPoolBuilder::new()
.num_threads(num_cpus::get())
.build_global()
.expect("Failed to create thread pool");
// Key insight:
// - num_threads: "Set how many threads"
// - build_global: "Create and install the pool"
// - Chain: configure (num_threads) -> build (build_global)
}Key insight: num_threads and build_global serve fundamentally different roles in rayon's configuration API. num_threads is a configuration method that sets a parameter on the builderāit returns &mut Self for method chaining and creates no threads. build_global is an action method that instantiates the pool with all configured settings and installs it as the global default, enabling all subsequent par_iter() calls to use it. You cannot have a functioning pool without calling build() or build_global(); you cannot control thread count without num_threads() (or environment variables). The typical pattern chains num_threads() with other configuration methods, ending with build_global() to create and install the pool in one expression. Use build_global() once at application startup for the global default; use build() for multiple pools with different configurations.
