Search::WorkerControl lets an external scheduler change how many workers a
running DFS or BAB engine may use. A portfolio with a fixed thread budget can
move workers among its engines at runtime.
The engine still starts with a fixed maximum:
Gecode::Search::Options options;
options.threads = 8; // Resident worker capacity
Gecode::Search::WorkerControl control(2); // Initially request two workers
options.worker_control = control;
Gecode::DFS<MySpace> engine(root, options);
// Safe from another thread while engine.next() is running.
control.request(6);
control.request(0); // Pause
control.request(1); // ResumeGecode resolves the threads option when it constructs the engine. The result
is the fixed worker capacity. A request must be between zero and that capacity,
inclusive. Zero pauses the engine without discarding its search state.
This also applies when the resolved capacity is one: sequential DFS and BAB
wait at search boundaries until the control resumes them.
Pausing requires a build with thread support. Without thread support, attaching
an initially paused control or calling request(0) raises
Search::InvalidWorkerRequest; controls must be used from one thread.
request is thread-safe and non-blocking. It publishes a desired worker count
and wakes parked workers when necessary; it does not wait for the engine to
reach that count.
Changes take effect cooperatively at scheduler boundaries. A grow request makes
parked workers eligible immediately. A shrink request cannot interrupt a
worker in the middle of a search action. Excess workers finish that action and
park before beginning another, so the engine may briefly use more workers than
requested. Once a request for zero has taken effect, next remains blocked
until a positive request resumes the engine.
TimeStop follows the same cooperative rule. The engine checks stop objects at
search boundaries; there is no timer thread to wake a paused engine. If a
TimeStop expires while the worker request is zero, next remains blocked.
After the engine resumes, it observes the expired stop at a normal stop check.
Requests affect scheduling, not search correctness. DFS still enumerates the same solution set and BAB still returns the same optimum. Parallel exploration order, solution order, node counts, failure counts, and the time at which a request becomes visible remain nondeterministic.
Shrinking parks resident operating-system threads. It does not destroy their thread objects or discard their engine-local search state. Growing wakes those threads again. Set the capacity to the largest allocation the engine may receive. Parked workers retain their stacks and other per-worker state.
A control is a copyable handle with shared identity. Copies made before or after engine construction publish to the same request state:
Gecode::Search::WorkerControl portfolio_control(4);
Gecode::Search::Options options;
options.threads = 8;
options.worker_control = portfolio_control;
Gecode::DFS<MySpace> engine(root, options);
auto scheduler_control = portfolio_control;
scheduler_control.request(3);An empty default-constructed handle means that worker adjustment is disabled.
Calling request on an empty handle raises
Search::UninitializedWorkerControl.
A shared identity can be bound to only one leaf engine at a time. It cannot be
shared by two DFS/BAB engines or attached directly to an enclosing meta-engine.
Ordinary reuse for a replacement engine after destruction is also rejected.
These uses raise Search::WorkerControlInUse.
Destroying an engine safely detaches its state, but it does not make that identity reusable. A copied handle may outlive the engine; further in-range requests are harmless and cannot access destroyed scheduler state.
An internal engine owner that constructs successive leaf engines can call
WorkerControlAccess::prepare_reuse after destroying the previous engine and
joining its workers. The replacement must have the same resolved capacity.
The control retains its shared identity, latest request (including pause),
request generation, and event storage. Preparing an attached control raises
Search::WorkerControlInUse; attaching with a different capacity raises
Search::InvalidWorkerRequest. No public reuse operation is provided.
Restart-based search keeps one leaf control through construction and reset. The same DFS or BAB leaf engine remains the adjustment target across restarts.
Portfolio-based search does not divide a global thread budget. Give each PBS
asset its own control in the corresponding sequential-engine builder options.
Controlled assets require a parallel outer PBS (threads resolving above one).
Sequential PBS rejects them with Search::WorkerControlInUse, since a paused
asset would prevent it from advancing to another asset. This check occurs before
PBS consumes the root or builders.
For example, configure the leaf builders as follows:
constexpr unsigned int budget = 8;
Gecode::Search::WorkerControl asset_a(6);
Gecode::Search::WorkerControl asset_b(2);
Gecode::Search::Options a;
a.threads = budget;
a.worker_control = asset_a;
Gecode::Search::Options b;
b.threads = budget;
b.worker_control = asset_b;
// Construct the PBS assets from builders carrying a and b.
// Later, preserve the external invariant sum(requests) <= budget.
asset_a.request(2);
asset_b.request(6);The portfolio controller must enforce its own active-worker budget. Making the decrease before the increase keeps the requested counts within the budget, but does not prevent temporary oversubscription while workers finish their current actions. A strict active-worker limit requires independent confirmation that the decrease has taken effect before increasing another asset's request. Gecode does not provide that acknowledgement or choose an allocation policy.
PBS completes a next round only after every active asset has reported. An
asset at zero still observes the internal stop used to close the round. It
reports without doing more search, and its worker request remains zero. A
solution from another asset returns while the asset stays paused. If every
active asset is paused, next blocks until at least one resumes.