Advanced Pool Patterns
This page covers advanced usage patterns for experienced users.
Calling Other @with_pool Functions
Each @with_pool function manages its own checkpoint. They can call each other freely:
@with_pool pool function step1(n)
A = zeros!(pool, Float64, n)
fill!(A, 1.0)
return sum(A)
end
@with_pool pool function step2(n)
B = zeros!(pool, Float64, n)
fill!(B, 2.0)
return sum(B)
end
@with_pool pool function pipeline(n)
a = step1(n) # step1's arrays marked for reuse when it returns
b = step2(n) # step2's arrays marked for reuse when it returns
C = acquire!(pool, Float64, n)
fill!(C, a + b)
return sum(C)
endPassing Pool as Argument
For complex call hierarchies, use @with_pool only at the top level and pass the pool through function arguments:
# Inner functions receive pool as argument - no @with_pool needed
function compute_step!(pool, data, result)
temp = acquire!(pool, Float64, length(data))
temp .= data .* 2
result[] += sum(temp)
end
function process_chunk!(pool, chunk, result)
temp = zeros!(pool, Float64, length(chunk))
compute_step!(pool, chunk, temp)
result[] += sum(temp)
end
# Only the entry point uses @with_pool
@with_pool pool function main_computation(chunks)
result = Ref(0.0)
for chunk in chunks
process_chunk!(pool, chunk, result)
end
return result[]
endBenefits:
- Single checkpoint/rewind at top level
- Inner functions are simpler (no macro overhead)
- Pool lifetime is explicit and controlled
Direct Pool Access in Inner Functions
An alternative to passing pool as argument: inner functions call get_task_local_pool() directly, while a top-level @with_pool function controls the lifecycle.
# Inner functions access pool directly - no argument needed
function compute_step!(data, result)
pool = get_task_local_pool() # Direct access
temp = acquire!(pool, Float64, length(data))
temp .= data .* 2
result[] += sum(temp)
# temp NOT released here - stays active
end
function process_chunk!(chunk, accumulator)
pool = get_task_local_pool() # Direct access
buffer = zeros!(pool, Float64, length(chunk))
compute_step!(chunk, buffer)
accumulator[] += sum(buffer)
# buffer NOT released here - stays active
end
# Top-level controls lifecycle with @with_pool
@with_pool pool function main_pipeline(chunks)
# checkpoint!() ─────────────────────────────────┐
accumulator = Ref(0.0) # │
for chunk in chunks # │
process_chunk!(chunk, accumulator) # │ All arrays from
# └─ compute_step! allocates temp # │ inner functions
# └─ process_chunk! allocates buffer # │ accumulate here
end # │
return accumulator[] # │
# rewind!() ─────────────────────────────────────┘
# └─ ALL arrays (temp, buffer, ...) marked for reuse
endMemory Flow Visualization
main_pipeline(chunks) Inner Functions
│
checkpoint!()
│
├──► process_chunk!()
│ │
│ ├──► get_task_local_pool() ──► buffer allocated
│ │
│ └──► compute_step!()
│ │
│ └──► get_task_local_pool() ──► temp allocated
│
├──► process_chunk!() (next iteration)
│ └──► ... more allocations ...
│
▼
rewind!() ◄─────── ALL arrays marked for reuse⚠️ User Responsibility Warning
This pattern requires you to guarantee that inner functions are always called through a @with_pool entry point:
# SAFE: Called through main_pipeline
main_pipeline(my_chunks) # ✓ Lifecycle managed
# DANGEROUS: Direct call without @with_pool wrapper
compute_step!(some_data, some_ref) # ✗ No checkpoint/rewind!
# └─ Arrays allocated but NEVER marked for reuse → pool grows unboundedlyWhen to use this pattern:
- Deep call hierarchies where threading pool through every function is tedious
- Performance-critical code where you want to avoid argument passing overhead
- You can enforce that all entry points use
@with_pool
When to prefer "Passing Pool as Argument":
- Functions may be called from various contexts (some pooled, some not)
- Library code where you can't control the caller
- You want explicit documentation of pool dependency in function signatures
Manual Checkpoint/Rewind
For fine-grained control, use checkpoint! and rewind! directly:
function manual_control()
pool = get_task_local_pool()
checkpoint!(pool)
try
A = acquire!(pool, Float64, 100)
B = acquire!(pool, Float64, 100)
# ... compute ...
return sum(A) + sum(B)
finally
rewind!(pool)
end
endThis is what @with_pool generates internally. Use manual control when:
- Integrating with existing try/catch blocks
- Conditional checkpoint/rewind logic needed
- Building custom pool management abstractions
Scope-Only @with_pool
You can omit the pool name when inner functions handle their own acquire:
@with_pool p function step1()
v = acquire!(p, Float64, 100)
sum(v)
end
@with_pool p function step2()
v = acquire!(p, Float64, 200)
sum(v)
end
# Outer function just provides scope management
@with_pool function orchestrate()
a = step1()
b = step2()
return a + b
endThe name-less @with_pool still performs checkpoint/rewind but doesn't expose the pool variable. This is useful when you're orchestrating other @with_pool functions.
See Also
@with_poolPatterns - Basic usage patterns- Safety Rules - Scope rules