diff --git a/Makefile.in b/Makefile.in index b446405125..43a330d5d3 100755 --- a/Makefile.in +++ b/Makefile.in @@ -266,10 +266,10 @@ SEARCHSRC0 = \ dfs bab lds \ seq/rbs seq/dead seq/pbs par/pbs \ rbs pbs nogoods exception tracer \ - cpprofiler/tracer + cpprofiler/tracer worker-control SEARCHHDR0 = \ statistics.hpp stop.hpp options.hpp cutoff.hpp \ - support.hh worker.hh exception.hpp engine.hpp base.hpp \ + support.hh worker.hh worker-control.hh exception.hpp engine.hpp base.hpp \ nogoods.hh nogoods.hpp build.hpp traits.hpp sebs.hpp \ seq/path.hh seq/path.hpp seq/dfs.hh seq/dfs.hpp \ seq/bab.hh seq/bab.hpp seq/lds.hh seq/lds.hpp \ diff --git a/README.md b/README.md index a75b48fbf3..59cea0c05c 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ In particular, Gecode comes with [extensive tutorial and reference documentation](https://gecode.github.io/documentation.html). +The [adjustable-worker guide](docs/worker-control.md) describes external, +asynchronous control of parallel-search worker allocation. + ## CMake Build Options CMake exposes options aligned with the Autoconf build switches. diff --git a/changelog.in b/changelog.in index 4f8129b317..1d801f68ec 100755 --- a/changelog.in +++ b/changelog.in @@ -67,6 +67,22 @@ # optional section in the html page. # +[RELEASE] +Version: 6.5.0 +Date: unreleased +[DESCRIPTION] +This is the development changelog for the next Gecode release. + +[ENTRY] +Module: search +What: new +Rank: major +[DESCRIPTION] +Add Search::WorkerControl for asynchronously adjusting how many pre-created +workers a parallel DFS or BAB engine may execute. Controls also apply to leaf +engines owned by RBS and PBS, allowing callers to redistribute a fixed worker +budget among engines without rebuilding them. + [RELEASE] Version: 6.4.0 Date: 2026-07-15 diff --git a/cmake/GecodeSources.cmake b/cmake/GecodeSources.cmake index 0c9defc0f1..c0d389f116 100644 --- a/cmake/GecodeSources.cmake +++ b/cmake/GecodeSources.cmake @@ -48,6 +48,7 @@ set(GECODE_SEARCH_SOURCES gecode/search/seq/rbs.cpp gecode/search/stop.cpp gecode/search/tracer.cpp + gecode/search/worker-control.cpp ) set(GECODE_INT_SOURCES diff --git a/docs/worker-control.md b/docs/worker-control.md new file mode 100644 index 0000000000..268e71f11c --- /dev/null +++ b/docs/worker-control.md @@ -0,0 +1,117 @@ +# Adjustable parallel-search workers + +## Purpose + +`Search::WorkerControl` lets an external scheduler change how many workers a +running DFS or BAB engine may use. Its main use is a portfolio that has a fixed +thread budget but wants to move that budget among independently useful search +engines as conditions change. + +The engine still starts with a fixed maximum: + +```cpp +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 engine(root, options); + +// Safe from another thread while engine.next() is running. +control.request(6); +control.request(1); +``` + +The `threads` option is expanded once and becomes the engine's immutable +capacity. A request must be between one and that capacity, inclusive. Zero is +not a pause operation: at least one worker remains available to make progress. + +## Asynchronous semantics + +`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 can +make parked workers eligible immediately. A shrink request does not interrupt +a worker in the middle of a search action: excess workers finish their current +action and park before beginning another. Consequently, the old and new +requests may overlap briefly. + +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 are deliberately 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. Applications should therefore choose capacity as a real +resource maximum: parked workers retain stacks and other per-worker state. + +## Handle lifetime and ownership + +A control is a copyable shared-identity handle. Copies made before or after +engine construction publish to the same request state: + +```cpp +Gecode::Search::WorkerControl portfolio_control(4); +Gecode::Search::Options options; +options.threads = 8; +options.worker_control = portfolio_control; + +Gecode::DFS 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`. + +One shared identity can bind to one logical leaf engine once. It cannot be +shared by two simultaneously constructed DFS/BAB engines, reused for a +replacement engine after destruction, or attached directly to an enclosing +meta-engine. Such reuse raises `Search::WorkerControlInUse`. Construct a new +control for each leaf-engine lifetime. + +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. + +## Meta-search + +Restart-based search transports one leaf control through restart construction +and reset. The same underlying DFS or BAB leaf engine remains the adjustment +target across episodes. + +Portfolio-based search does not divide a global thread budget. Give each PBS +asset its own control in the corresponding sequential-engine builder options: + +```cpp +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. Make the +decrease before the increase if even a brief oversubscription is unacceptable; +because resizing is cooperative, applications needing a strict observed +handoff must also provide their own acknowledgement or accounting layer. +Gecode intentionally exposes the scheduling seam without embedding a +portfolio allocation policy. diff --git a/gecode/search.hh b/gecode/search.hh index 38668804ad..692ab5ad6d 100755 --- a/gecode/search.hh +++ b/gecode/search.hh @@ -709,6 +709,43 @@ namespace Gecode { namespace Search { namespace Gecode { namespace Search { + class WorkerControlAccess; + + /** + * \brief External control for the requested number of search workers + * + * A worker control is a copyable handle. Copies share the same request + * and can be used concurrently. The worker capacity is fixed when the + * handle is first attached to a search engine. + * + * \ingroup TaskModelSearch + */ + class GECODE_SEARCH_EXPORT WorkerControl { + private: + class State; + State* state; + friend class WorkerControlAccess; + public: + /// Construct an empty handle + WorkerControl(void) noexcept; + /// Construct an engaged handle with initial request \a requested + explicit WorkerControl(unsigned int requested); + /// Copy constructor + WorkerControl(const WorkerControl& control); + /// Assignment operator + WorkerControl& operator =(const WorkerControl& control); + /// Destructor + ~WorkerControl(void); + /// Whether this handle is engaged + explicit operator bool(void) const noexcept; + /// Return the requested number of workers, or zero for an empty handle + unsigned int requested(void) const noexcept; + /// Request \a workers workers + void request(unsigned int workers); + /// Return the fixed worker capacity, or zero before attachment + unsigned int capacity(void) const noexcept; + }; + class Stop; /** @@ -754,6 +791,13 @@ namespace Gecode { namespace Search { bool clone; /// Number of threads to use double threads; + /** + * External worker control + * + * After option expansion, \a threads is the immutable worker capacity. + * Requests through this handle never change \a threads. + */ + WorkerControl worker_control; /// Create a clone after every \a c_d commits (commit distance) unsigned int c_d; /// Create a clone during recomputation if distance is greater than \a a_d (adaptive distance) @@ -783,6 +827,8 @@ namespace Gecode { namespace Search { }} +#include + #include namespace Gecode { namespace Search { @@ -941,6 +987,13 @@ namespace Gecode { namespace Search { * \brief %Search engine implementation interface */ class GECODE_SEARCH_EXPORT Engine : public HeapAllocated { + protected: + /// Control retained for the lifetime of a leaf engine + WorkerControl worker_control; + /// Construct a meta engine without worker control + Engine(void); + /// Construct a leaf engine and bind worker control to \a capacity + Engine(const Options& o, unsigned int capacity); public: /// Return next solution (nullptr, if none exists or search has been stopped) virtual Space* next(void) = 0; @@ -1265,6 +1318,10 @@ namespace Gecode { * The engine will run a portfolio with a number of assets as defined * by the options \a o. The engine supports parallel execution of * assets by using the number of threads as defined by the options. + * An external worker control in \a o is supported only for a single + * homogeneous asset. Multiple controlled assets require explicit engine + * builders, each with its own control and immutable worker capacity. + * PBS does not allocate workers or adjust those controls. * * The class \a T can implement member functions * \code virtual bool master(const MetaInfo& mi) \endcode @@ -1285,8 +1342,15 @@ namespace Gecode { public: /// Initialize with engines running copies of \a s with options \a o PBS(T* s, const Search::Options& o=Search::Options::def); - /// Initialize with engine builders \a sebs + /** + * Initialize with engine builders \a sebs + * + * The outer options must not contain a worker control. Each builder can + * instead supply a distinct control for its underlying engine. + */ PBS(T* s, SEBs& sebs, const Search::Options& o=Search::Options::def); + /// Constrain future portfolio solutions to be better than \a b + void constrain(const T& b); /// Whether engine does best solution search static const bool best = E::best; }; @@ -1297,6 +1361,8 @@ namespace Gecode { * The engine will run a portfolio with a number of assets as defined * by the options \a o. The engine supports parallel execution of * assets by using the number of threads as defined by the options. + * An external worker control is supported only when \a o selects one + * asset. PBS does not implement worker-allocation policy. * * The class \a T can implement member functions * \code virtual bool master(const MetaInfo& mi) \endcode diff --git a/gecode/search/engine.cpp b/gecode/search/engine.cpp index ac62b44484..e7551ddcdf 100644 --- a/gecode/search/engine.cpp +++ b/gecode/search/engine.cpp @@ -32,9 +32,18 @@ */ #include +#include namespace Gecode { namespace Search { + Engine::Engine(void) + : worker_control() {} + + Engine::Engine(const Options& o, unsigned int capacity) + : worker_control(o.worker_control) { + WorkerControlAccess::attach(worker_control,capacity); + } + void Engine::constrain(const Space& b) { (void) b; @@ -49,6 +58,10 @@ namespace Gecode { namespace Search { return NoGoods::eng; } + Engine::~Engine(void) { + WorkerControlAccess::detach(worker_control); + } + }} // STATISTICS: search-other diff --git a/gecode/search/engine.hpp b/gecode/search/engine.hpp index a7003217c2..5eba732c3a 100644 --- a/gecode/search/engine.hpp +++ b/gecode/search/engine.hpp @@ -31,11 +31,4 @@ * */ -namespace Gecode { namespace Search { - - forceinline - Engine::~Engine(void) {} - -}} - // STATISTICS: search-other diff --git a/gecode/search/exception.cpp b/gecode/search/exception.cpp index eb95353ad8..773262f828 100644 --- a/gecode/search/exception.cpp +++ b/gecode/search/exception.cpp @@ -48,6 +48,15 @@ namespace Gecode { namespace Search { NoBest::NoBest(const char* l) : Exception(l,"Best solution search is not supported") {} + InvalidWorkerRequest::InvalidWorkerRequest(const char* l) + : Exception(l,"Invalid number of requested search workers") {} + + UninitializedWorkerControl::UninitializedWorkerControl(const char* l) + : Exception(l,"Worker control is not initialized") {} + + WorkerControlInUse::WorkerControlInUse(const char* l) + : Exception(l,"Worker control has already been bound") {} + }} // STATISTICS: search-other diff --git a/gecode/search/exception.hpp b/gecode/search/exception.hpp index 01ff8cbd50..13bdda7748 100644 --- a/gecode/search/exception.hpp +++ b/gecode/search/exception.hpp @@ -62,6 +62,24 @@ namespace Gecode { namespace Search { /// Initialize with location \a l NoBest(const char* l); }; + /// %Exception: Invalid requested number of search workers + class GECODE_SEARCH_EXPORT InvalidWorkerRequest : public Exception { + public: + /// Initialize with location \a l + InvalidWorkerRequest(const char* l); + }; + /// %Exception: Request through an empty worker control + class GECODE_SEARCH_EXPORT UninitializedWorkerControl : public Exception { + public: + /// Initialize with location \a l + UninitializedWorkerControl(const char* l); + }; + /// %Exception: Worker control is already bound to a search engine + class GECODE_SEARCH_EXPORT WorkerControlInUse : public Exception { + public: + /// Initialize with location \a l + WorkerControlInUse(const char* l); + }; //@} }} diff --git a/gecode/search/options.hpp b/gecode/search/options.hpp index b85d969008..bb442c6786 100644 --- a/gecode/search/options.hpp +++ b/gecode/search/options.hpp @@ -37,6 +37,7 @@ namespace Gecode { namespace Search { Options::Options(void) : clone(Config::clone), threads(Config::threads), + worker_control(), c_d(Config::c_d), a_d(Config::a_d), d_l(Config::d_l), assets(0), slice(Config::slice), nogoods_limit(0), diff --git a/gecode/search/par/bab.hh b/gecode/search/par/bab.hh index 5284f131cf..4a1911b208 100644 --- a/gecode/search/par/bab.hh +++ b/gecode/search/par/bab.hh @@ -80,9 +80,11 @@ namespace Gecode { namespace Search { namespace Par { int mark; /// Best solution found so far Space* best; + /// Fixed worker index + unsigned int index; public: /// Initialize for space \a s with engine \a e - Worker(Space* s, BAB& e); + Worker(Space* s, BAB& e, unsigned int index); /// Provide access to engine BAB& engine(void) const; /// Start execution of worker @@ -90,9 +92,11 @@ namespace Gecode { namespace Search { namespace Par { /// Accept better solution \a b void better(Space* b); /// Try to find some work - void find(void); + bool find(void); /// Reset engine to restart at space \a s void reset(Space* s, unsigned int ngdl); + /// Whether this worker currently owns local search + bool owns_work(void) const; /// Destructor virtual ~Worker(void); }; diff --git a/gecode/search/par/bab.hpp b/gecode/search/par/bab.hpp index 8e6d3f047d..98e717cdae 100755 --- a/gecode/search/par/bab.hpp +++ b/gecode/search/par/bab.hpp @@ -71,14 +71,20 @@ namespace Gecode { namespace Search { namespace Par { Search::Worker::reset(); } + template + forceinline bool + BAB::Worker::owns_work(void) const { + return (cur != nullptr) || !path.empty(); + } + /* * Engine: initialization */ template forceinline - BAB::Worker::Worker(Space* s, BAB& e) - : Engine::Worker(s,e), mark(0), best(nullptr) {} + BAB::Worker::Worker(Space* s, BAB& e, unsigned int index0) + : Engine::Worker(s,e), mark(0), best(nullptr), index(index0) {} template forceinline @@ -90,10 +96,11 @@ namespace Gecode { namespace Search { namespace Par { _worker = static_cast (heap.ralloc(workers() * sizeof(Worker*))); // The first worker gets the entire search tree - _worker[0] = new Worker(s,*this); + _worker[0] = new Worker(s,*this,0U); // All other workers start with no work for (unsigned int i=1U; ischeduler_enable(_worker[0]->owns_work()); // Block all workers block(); // Create and start threads @@ -148,7 +155,7 @@ namespace Gecode { namespace Search { namespace Par { * Worker: finding and stealing working */ template - forceinline void + forceinline bool BAB::Worker::find(void) { // Try to find new work (even if there is none) for (unsigned int i=0U; ischeduler_reset(worker(0U)->owns_work()); } diff --git a/gecode/search/par/dfs.hh b/gecode/search/par/dfs.hh index ead09e2c90..326db42664 100644 --- a/gecode/search/par/dfs.hh +++ b/gecode/search/par/dfs.hh @@ -76,16 +76,20 @@ namespace Gecode { namespace Search { namespace Par { using Engine::Worker::start; using Engine::Worker::tracer; using Engine::Worker::stop; + /// Fixed worker index + unsigned int index; /// Initialize for space \a s with engine \a e - Worker(Space* s, DFS& e); + Worker(Space* s, DFS& e, unsigned int index); /// Provide access to engine DFS& engine(void) const; /// Start execution of worker virtual void run(void); /// Try to find some work - void find(void); + bool find(void); /// Reset worker to restart at space \a s void reset(Space* s, unsigned int ngdl); + /// Whether this worker currently owns local search + bool owns_work(void) const; }; /// Array of worker references Worker** _worker; diff --git a/gecode/search/par/dfs.hpp b/gecode/search/par/dfs.hpp index 2108b0ee75..d50e771462 100755 --- a/gecode/search/par/dfs.hpp +++ b/gecode/search/par/dfs.hpp @@ -53,8 +53,8 @@ namespace Gecode { namespace Search { namespace Par { */ template forceinline - DFS::Worker::Worker(Space* s, DFS& e) - : Engine::Worker(s,e) {} + DFS::Worker::Worker(Space* s, DFS& e, unsigned int index0) + : Engine::Worker(s,e), index(index0) {} template forceinline DFS::DFS(Space* s, const Options& o) @@ -65,10 +65,11 @@ namespace Gecode { namespace Search { namespace Par { _worker = static_cast (heap.ralloc(workers() * sizeof(Worker*))); // The first worker gets the entire search tree - _worker[0] = new Worker(s,*this); + _worker[0] = new Worker(s,*this,0U); // All other workers start with no work for (unsigned int i=1; ischeduler_enable(_worker[0]->owns_work()); // Block all workers block(); // Create and start threads @@ -97,6 +98,12 @@ namespace Gecode { namespace Search { namespace Par { Search::Worker::reset(); } + template + forceinline bool + DFS::Worker::owns_work(void) const { + return (cur != nullptr) || !path.empty(); + } + /* * Engine: search control @@ -118,7 +125,7 @@ namespace Gecode { namespace Search { namespace Par { * Worker: finding and stealing working */ template - forceinline void + forceinline bool DFS::Worker::find(void) { // Try to find new work (even if there is none) for (unsigned int i=0U; ischeduler_reset(worker(0U)->owns_work()); } diff --git a/gecode/search/par/engine.hh b/gecode/search/par/engine.hh index e3b59077be..a8a7cf16f9 100644 --- a/gecode/search/par/engine.hh +++ b/gecode/search/par/engine.hh @@ -41,6 +41,7 @@ #include #include #include +#include #include #include @@ -86,11 +87,56 @@ namespace Gecode { namespace Search { namespace Par { }; /// Search options Options _opt; + /// Logical worker state used by adjustable parallel admission + enum class SchedulerLogical { + OWNER, + PENDING, + IDLE + }; + /// Per-worker adjustable parallel admission state + struct SchedulerWorker { + bool lease; + bool parked; + SchedulerLogical logical; + }; + /// Whether adjustable parallel admission is enabled + bool scheduler_enabled; + /// Unchanged-capacity admission fast path + const std::atomic* scheduler_fast_admit; + /// Mutex for adjustable parallel admission + Support::Mutex scheduler_mutex; + /// Per-worker adjustable parallel admission state + SchedulerWorker* scheduler_worker; + /// Current requested lease count + unsigned int scheduler_requested; + /// Current lease count + unsigned int scheduler_leases; + /// Round-robin cursor for lease handoff + unsigned int scheduler_cursor; + /// Last request generation reconciled by the scheduler + std::atomic scheduler_generation; + /// Select a no-lease worker by logical state + unsigned int scheduler_select(SchedulerLogical logical, + unsigned int exclude) const; + /// Grant leases up to the current request + bool scheduler_grow(void); public: /// Provide access to search options const Options& opt(void) const; /// Return number of workers unsigned int workers(void) const; + /// Enable adjustable parallel admission + void scheduler_enable(bool root_owner); + /// Reset adjustable parallel state while all workers are blocked + void scheduler_reset(bool root_owner); + /// Admit worker \a worker for one exploration action + bool scheduler_admit(unsigned int worker); + /// Record that worker \a worker owns search + void scheduler_owner(unsigned int worker); + /// Record that worker \a worker is idle + void scheduler_idle(unsigned int worker); + /// Hand worker \a worker's lease to a parked logical worker + void scheduler_handoff(unsigned int worker, bool work_remains); /// \name Commands from engine to workers and wait management //@{ @@ -187,6 +233,8 @@ namespace Gecode { namespace Search { namespace Par { void busy(void); /// Report that worker has been stopped void stop(void); + /// Whether logical search work remains + bool work_remains(void); //@} /// \name Engine interface diff --git a/gecode/search/par/engine.hpp b/gecode/search/par/engine.hpp index 8ff5ed66e9..75d7737fa2 100644 --- a/gecode/search/par/engine.hpp +++ b/gecode/search/par/engine.hpp @@ -82,6 +82,9 @@ namespace Gecode { namespace Search { namespace Par { forceinline void Engine::release(Cmd c) { _cmd.store(c, std::memory_order_release); + if (scheduler_enabled && ((c == C_WORK) || (c == C_RESET) || + (c == C_TERMINATE))) + WorkerControlAccess::signal_all(worker_control); Support::Thread::releaseGlobalMutex(&_m_wait); } template @@ -118,7 +121,12 @@ namespace Gecode { namespace Search { namespace Par { template forceinline Engine::Engine(const Options& o) - : _opt(o), _cmd(C_WAIT), solutions(heap) { + : Search::Engine(o,static_cast(o.threads)), + _opt(o), scheduler_enabled(false), + scheduler_fast_admit(nullptr), + scheduler_worker(nullptr), scheduler_requested(workers()), + scheduler_leases(0U), scheduler_cursor(0U), scheduler_generation(0U), + _cmd(C_WAIT), solutions(heap) { // Initialize termination information _n_term_not_ack = workers(); _n_not_terminated = workers(); @@ -129,6 +137,189 @@ namespace Gecode { namespace Search { namespace Par { _n_reset_not_ack = workers(); } + template + unsigned int + Engine::scheduler_select(SchedulerLogical logical, + unsigned int exclude) const { + for (unsigned int offset=0U; offset + bool + Engine::scheduler_grow(void) { + bool wake = false; + while (scheduler_leases < scheduler_requested) { + unsigned int i = scheduler_select(SchedulerLogical::OWNER,workers()); + if (i == workers()) + i = scheduler_select(SchedulerLogical::PENDING,workers()); + if (i == workers()) + i = scheduler_select(SchedulerLogical::IDLE,workers()); + if (i == workers()) { + for (i=0U; i + void + Engine::scheduler_enable(bool root_owner) { + if (!WorkerControlAccess::engaged(worker_control)) + return; + scheduler_fast_admit = + WorkerControlAccess::fast_admission(worker_control); + scheduler_worker = static_cast + (heap.ralloc(workers() * sizeof(SchedulerWorker))); + unsigned long long int generation; + WorkerControlAccess::snapshot( + worker_control,scheduler_requested,generation); + scheduler_leases = scheduler_requested; + for (unsigned int i=0U; i + void + Engine::scheduler_reset(bool root_owner) { + if (!scheduler_enabled) + return; + unsigned int requested; + unsigned long long int generation; + WorkerControlAccess::snapshot(worker_control,requested,generation); + scheduler_mutex.acquire(); + scheduler_requested = requested; + scheduler_leases = scheduler_requested; + scheduler_cursor = 0U; + for (unsigned int i=0U; i + bool + Engine::scheduler_admit(unsigned int worker) { + if (!scheduler_enabled) + return true; + if (scheduler_fast_admit->load(std::memory_order_acquire)) + return true; + while (cmd() == C_WORK) { + unsigned long long int generation = + WorkerControlAccess::generation(worker_control); + if ((generation == + scheduler_generation.load(std::memory_order_acquire)) && + (WorkerControlAccess::requested(worker_control) == workers())) + return true; + + bool wake; + bool admitted; + unsigned int requested; + WorkerControlAccess::snapshot(worker_control,requested,generation); + scheduler_mutex.acquire(); + if (generation > + scheduler_generation.load(std::memory_order_relaxed)) { + scheduler_requested = requested; + scheduler_generation.store(generation,std::memory_order_release); + } + wake = scheduler_grow(); + if (scheduler_worker[worker].lease && + (scheduler_leases > scheduler_requested)) { + scheduler_worker[worker].lease = false; + scheduler_leases--; + } + admitted = scheduler_worker[worker].lease; + scheduler_worker[worker].parked = !admitted; + scheduler_mutex.release(); + + if (requested == workers()) + WorkerControlAccess::fast_admission(worker_control,generation); + if (wake) + WorkerControlAccess::signal_all(worker_control); + if (admitted) + return true; + WorkerControlAccess::wait(worker_control,worker); + } + return false; + } + + template + void + Engine::scheduler_owner(unsigned int worker) { + if (!scheduler_enabled) + return; + scheduler_mutex.acquire(); + scheduler_worker[worker].logical = SchedulerLogical::OWNER; + scheduler_mutex.release(); + } + + template + void + Engine::scheduler_idle(unsigned int worker) { + if (!scheduler_enabled) + return; + scheduler_mutex.acquire(); + scheduler_worker[worker].logical = SchedulerLogical::IDLE; + scheduler_mutex.release(); + } + + template + void + Engine::scheduler_handoff(unsigned int worker, + bool work_remains) { + if (!scheduler_enabled) + return; + unsigned int target = workers(); + scheduler_mutex.acquire(); + if (scheduler_worker[worker].lease) { + if (scheduler_leases > scheduler_requested) { + scheduler_worker[worker].lease = false; + scheduler_worker[worker].parked = true; + scheduler_leases--; + } else if (work_remains) { + target = scheduler_select(SchedulerLogical::OWNER,worker); + if (target == workers()) + target = scheduler_select(SchedulerLogical::PENDING,worker); + if (target != workers()) { + scheduler_worker[worker].lease = false; + scheduler_worker[worker].parked = true; + scheduler_worker[target].lease = true; + scheduler_worker[target].parked = false; + scheduler_cursor = (target+1U) % workers(); + } + } + } + scheduler_mutex.release(); + if (target != workers()) + WorkerControlAccess::signal(worker_control,target); + } /* * Statistics @@ -183,6 +374,15 @@ namespace Gecode { namespace Search { namespace Par { m_search.release(); } + template + forceinline bool + Engine::work_remains(void) { + m_search.acquire(); + bool remains = n_busy > 0; + m_search.release(); + return remains; + } + /* * Engine: termination control @@ -371,6 +571,8 @@ namespace Gecode { namespace Search { namespace Par { Engine::~Engine(void) { while (!solutions.empty()) delete solutions.pop(); + if (scheduler_worker != nullptr) + heap.rfree(scheduler_worker); } }}} diff --git a/gecode/search/par/pbs.hpp b/gecode/search/par/pbs.hpp index 1fa593aead..00d7f42ca5 100755 --- a/gecode/search/par/pbs.hpp +++ b/gecode/search/par/pbs.hpp @@ -244,29 +244,29 @@ namespace Gecode { namespace Search { namespace Par { Space* PBS::next(void) { m.acquire(); - if (solutions.empty()) { - // Clear all - tostop.store(false, std::memory_order_release); + if (solutions.empty()) slave_stop.store(false, std::memory_order_release); + while (solutions.empty() && (n_active > 0) && + !slave_stop.load(std::memory_order_acquire)) { + // Clear the internal stop used to interrupt sibling slaves + tostop.store(false, std::memory_order_release); // Invariant: all slaves are idle! assert(n_busy == 0); assert(!tostop.load(std::memory_order_acquire)); - if (n_active > 0) { - // Run all active slaves - n_busy = n_active; - for (unsigned int i=0U; iwait(); - Support::Thread::run(slaves[i]); - } - m.release(); - // Wait for all slaves to become idle - idle.wait(); - m.acquire(); + // Run all active slaves + n_busy = n_active; + for (unsigned int i=0U; iwait(); + Support::Thread::run(slaves[i]); } + m.release(); + // Wait for all slaves to become idle + idle.wait(); + m.acquire(); } // Invariant all slaves are idle! diff --git a/gecode/search/pbs.hpp b/gecode/search/pbs.hpp index aa626cf750..258afafa20 100644 --- a/gecode/search/pbs.hpp +++ b/gecode/search/pbs.hpp @@ -33,9 +33,65 @@ #include #include +#include namespace Gecode { namespace Search { + /// Dispose partially constructed portfolio assets + forceinline void + pbscleanup(Engine** slaves, Stop** stops, unsigned int n_slaves, + Space* master) { + for (unsigned int i=0U; i 1U) && + WorkerControlAccess::engaged(opt.worker_control)) + throw WorkerControlInUse("PBS::PBS"); + } + + /// Return the number of explicit builders admitted by this portfolio + forceinline int + pbsadmitted(const SEBs& sebs, const Options& opt) { +#ifdef GECODE_HAS_THREADS + if (opt.threads > 1.0) + return std::min(static_cast(opt.threads),sebs.size()); +#endif + return sebs.size(); + } + + /// Validate worker-control placement for an explicit portfolio + forceinline void + pbscontrol(const SEBs& sebs, const Options& opt) { + if (WorkerControlAccess::engaged(opt.worker_control)) + throw WorkerControlInUse("PBS::PBS"); + int admitted = pbsadmitted(sebs,opt); + for (int i=0; ioptions().worker_control; + if (!WorkerControlAccess::engaged(x)) + continue; + for (int j=0; joptions().worker_control; + if (WorkerControlAccess::engaged(y) && + WorkerControlAccess::same_identity(x,y)) + throw WorkerControlInUse("PBS::PBS"); + } + } + } + /// A PBS engine builder template class E> class PbsBuilder : public Builder { @@ -103,19 +159,38 @@ namespace Gecode { namespace Search { unsigned int n_slaves = opt.assets; Engine** slaves = r.alloc(n_slaves); Stop** stops = r.alloc(n_slaves); + for (unsigned int i=0U; iclone(); - (void) slave->slave(i); - slaves[i] = build(slave,opt); + Space* unowned_master = master; + try { + for (unsigned int i=0U; iclone(); + try { + (void) slave->slave(i); + slaves[i] = build(slave,opt); + } catch (...) { + delete slave; + if (slave == master) + unowned_master = nullptr; + throw; + } + if (slave == master) + unowned_master = nullptr; + } + return Seq::pbsengine( + slaves,stops,n_slaves,stat,opt,E::best); + } catch (...) { + pbscleanup(slaves,stops,n_slaves,unowned_master); + throw; } - - return Seq::pbsengine(slaves,stops,n_slaves,stat,opt,E::best); } template class E> @@ -127,25 +202,48 @@ namespace Gecode { namespace Search { int n_slaves = sebs.size(); Engine** slaves = r.alloc(n_slaves); Stop** stops = r.alloc(n_slaves); + for (int i=0; i(n_slaves)); - for (int i=0; ioptions().stop); - sebs[i]->options().stop = stops[i]; - sebs[i]->options().clone = false; - Space* slave = (i == n_slaves-1) ? - master : master->clone(); - (void) slave->slave(static_cast(i)); - slaves[i] = (*sebs[i])(slave); - delete sebs[i]; + Space* unowned_master = master; + bool builders_owned = true; + try { + for (int i=0; ioptions().stop); + sebs[i]->options().stop = stops[i]; + sebs[i]->options().clone = false; + Space* slave = (i == n_slaves-1) ? + master : master->clone(); + try { + (void) slave->slave(static_cast(i)); + slaves[i] = (*sebs[i])(slave); + } catch (...) { + delete slave; + if (slave == master) + unowned_master = nullptr; + throw; + } + if (slave == master) + unowned_master = nullptr; + } + pbscleanup(sebs); + builders_owned = false; + return Seq::pbsengine( + slaves,stops,static_cast(n_slaves),stat,opt,best); + } catch (...) { + if (builders_owned) + pbscleanup(sebs); + pbscleanup(slaves,stops,static_cast(n_slaves), + unowned_master); + throw; } - - return Seq::pbsengine(slaves,stops,static_cast(n_slaves), - stat,opt,best); } #ifdef GECODE_HAS_THREADS @@ -158,7 +256,7 @@ namespace Gecode { namespace Search { // Limit the number of slaves to the number of threads unsigned int n_slaves = std::min(static_cast(opt.threads), - opt.assets); + opt.assets); // Redistribute additional threads to slaves opt.threads = floor(opt.threads / static_cast(n_slaves)); @@ -167,16 +265,34 @@ namespace Gecode { namespace Search { Engine** slaves = r.alloc(n_slaves); Stop** stops = r.alloc(n_slaves); - for (unsigned int i=0U; iclone(); - (void) slave->slave(static_cast(i)); - slaves[i] = build(slave,opt); + slaves[i] = nullptr; + stops[i] = nullptr; } - return Par::pbsengine(slaves,stops,n_slaves,stat,E::best); + Space* unowned_master = master; + try { + for (unsigned int i=0U; iclone(); + try { + (void) slave->slave(static_cast(i)); + slaves[i] = build(slave,opt); + } catch (...) { + delete slave; + if (slave == master) + unowned_master = nullptr; + throw; + } + if (slave == master) + unowned_master = nullptr; + } + return Par::pbsengine(slaves,stops,n_slaves,stat,E::best); + } catch (...) { + pbscleanup(slaves,stops,n_slaves,unowned_master); + throw; + } } template class E> @@ -187,7 +303,7 @@ namespace Gecode { namespace Search { // Limit the number of slaves to the number of threads int n_slaves = std::min(static_cast(opt.threads), - sebs.size()); + sebs.size()); WrapTraceRecorder::engine(opt.tracer, SearchTracer::EngineType::PBS, @@ -195,24 +311,44 @@ namespace Gecode { namespace Search { Engine** slaves = r.alloc(n_slaves); Stop** stops = r.alloc(n_slaves); - for (int i=0; ioptions().stop); - sebs[i]->options().stop = stops[i]; - sebs[i]->options().clone = false; - Space* slave = (i == n_slaves-1) ? - master : master->clone(); - (void) slave->slave(static_cast(i)); - slaves[i] = (*sebs[i])(slave); - delete sebs[i]; + slaves[i] = nullptr; + stops[i] = nullptr; } - // Delete excess builders - for (int i=n_slaves; i(n_slaves), - stat,best); + Space* unowned_master = master; + bool builders_owned = true; + try { + for (int i=0; ioptions().stop); + sebs[i]->options().stop = stops[i]; + sebs[i]->options().clone = false; + Space* slave = (i == n_slaves-1) ? + master : master->clone(); + try { + (void) slave->slave(static_cast(i)); + slaves[i] = (*sebs[i])(slave); + } catch (...) { + delete slave; + if (slave == master) + unowned_master = nullptr; + throw; + } + if (slave == master) + unowned_master = nullptr; + } + pbscleanup(sebs); + builders_owned = false; + return Par::pbsengine( + slaves,stops,static_cast(n_slaves),stat,best); + } catch (...) { + if (builders_owned) + pbscleanup(sebs); + pbscleanup(slaves,stops,static_cast(n_slaves), + unowned_master); + throw; + } } #endif @@ -227,6 +363,7 @@ namespace Gecode { if (opt.assets == 0) throw Search::NoAssets("PBS::PBS"); + Search::pbscontrol(opt); Search::Statistics stat; @@ -264,6 +401,9 @@ namespace Gecode { template class E> void PBS::build(T* s, SEBs& sebs, const Search::Options& o) { + Search::Options opt(o.expand()); + Search::pbscontrol(sebs,opt); + // Check whether all sebs do either best solution search or not bool best; { @@ -275,7 +415,6 @@ namespace Gecode { best = (b == sebs.size()); } - Search::Options opt(o.expand()); Search::Statistics stat; if (s->status(stat) == SS_FAILED) { @@ -308,6 +447,12 @@ namespace Gecode { build(s,sebs,o); } + template class E> + inline void + PBS::constrain(const T& b) { + e->constrain(b); + } + template class E> inline T* pbs(T* s, const Search::Options& o) { diff --git a/gecode/search/support.hh b/gecode/search/support.hh index 1d0fc78172..fe25d487b2 100644 --- a/gecode/search/support.hh +++ b/gecode/search/support.hh @@ -75,7 +75,7 @@ namespace Gecode { namespace Search { template WorkerToEngine::WorkerToEngine(Space* s, const Options& o) - : w(s,o) {} + : Engine(o,1U), w(s,o) {} template Space* WorkerToEngine::next(void) { diff --git a/gecode/search/worker-control.cpp b/gecode/search/worker-control.cpp new file mode 100644 index 0000000000..17f1d6957d --- /dev/null +++ b/gecode/search/worker-control.cpp @@ -0,0 +1,279 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include + +namespace Gecode { namespace Search { + + class WorkerControl::State : public Support::RefCount { + public: + enum Lifecycle { + NEVER_BOUND, + ATTACHED, + DETACHED + }; + + std::atomic requested; + std::atomic capacity; + std::atomic generation; + std::atomic fast_admit; + Support::Mutex mutex; + Lifecycle lifecycle; + Support::Event* events; + + State(unsigned int workers) + : RefCount(1U), requested(workers), capacity(0U), generation(0U), + fast_admit(false), lifecycle(NEVER_BOUND), events(nullptr) {} + ~State(void) { + delete [] events; + } + }; + + WorkerControl::WorkerControl(void) noexcept + : state(nullptr) {} + + WorkerControl::WorkerControl(unsigned int requested) + : state(nullptr) { + if (requested == 0U) + throw InvalidWorkerRequest("WorkerControl::WorkerControl"); + state = new State(requested); + } + + WorkerControl::WorkerControl(const WorkerControl& control) + : state(control.state) { + if (state != nullptr) + state->inc(); + } + + WorkerControl& + WorkerControl::operator =(const WorkerControl& control) { + if (control.state != nullptr) + control.state->inc(); + if ((state != nullptr) && state->dec()) + delete state; + state = control.state; + return *this; + } + + WorkerControl::~WorkerControl(void) { + if ((state != nullptr) && state->dec()) + delete state; + } + + WorkerControl::operator bool(void) const noexcept { + return state != nullptr; + } + + unsigned int + WorkerControl::requested(void) const noexcept { + return (state == nullptr) ? 0U : + state->requested.load(std::memory_order_acquire); + } + + void + WorkerControl::request(unsigned int workers) { + if (state == nullptr) + throw UninitializedWorkerControl("WorkerControl::request"); + if (workers == 0U) + throw InvalidWorkerRequest("WorkerControl::request"); + + bool changed = false; + { + Support::Lock lock(state->mutex); + unsigned int capacity = + state->capacity.load(std::memory_order_relaxed); + if ((capacity != 0U) && (workers > capacity)) + throw InvalidWorkerRequest("WorkerControl::request"); + unsigned int old = + state->requested.load(std::memory_order_relaxed); + if (old != workers) { + state->fast_admit.store(false,std::memory_order_release); + state->requested.store(workers,std::memory_order_release); + (void) state->generation.fetch_add(1U,std::memory_order_release); + changed = true; + } + } + if (changed) + WorkerControlAccess::signal_all(*this); + } + + unsigned int + WorkerControl::capacity(void) const noexcept { + return (state == nullptr) ? 0U : + state->capacity.load(std::memory_order_acquire); + } + + void + WorkerControlAccess::attach(WorkerControl& control, + unsigned int capacity) { + if (control.state == nullptr) + return; + WorkerControl::State* state = control.state; + Support::Lock lock(state->mutex); + if (state->lifecycle != WorkerControl::State::NEVER_BOUND) + throw WorkerControlInUse("WorkerControlAccess::attach"); + if ((capacity == 0U) || + (state->requested.load(std::memory_order_relaxed) > capacity)) + throw InvalidWorkerRequest("WorkerControlAccess::attach"); + state->events = new Support::Event[capacity]; + state->capacity.store(capacity,std::memory_order_release); + state->fast_admit.store( + state->requested.load(std::memory_order_relaxed) == capacity, + std::memory_order_release); + state->lifecycle = WorkerControl::State::ATTACHED; + } + + void + WorkerControlAccess::detach(WorkerControl& control) { + if (control.state == nullptr) + return; + WorkerControl::State* state = control.state; + { + Support::Lock lock(state->mutex); + if (state->lifecycle == WorkerControl::State::ATTACHED) + state->lifecycle = WorkerControl::State::DETACHED; + } + signal_all(control); + } + + unsigned long long int + WorkerControlAccess::generation(const WorkerControl& control) { + return (control.state == nullptr) ? 0U : + control.state->generation.load(std::memory_order_acquire); + } + + bool + WorkerControlAccess::engaged(const WorkerControl& control) { + return control.state != nullptr; + } + + const std::atomic* + WorkerControlAccess::fast_admission(const WorkerControl& control) { + return (control.state == nullptr) ? nullptr : + &control.state->fast_admit; + } + + void + WorkerControlAccess::fast_admission( + WorkerControl& control, unsigned long long int generation) { + if (control.state == nullptr) + return; + WorkerControl::State* state = control.state; + Support::Lock lock(state->mutex); + if ((state->generation.load(std::memory_order_relaxed) == generation) && + (state->requested.load(std::memory_order_relaxed) == + state->capacity.load(std::memory_order_relaxed))) + state->fast_admit.store(true,std::memory_order_release); + } + + bool + WorkerControlAccess::same_identity(const WorkerControl& x, + const WorkerControl& y) { + return x.state == y.state; + } + + unsigned int + WorkerControlAccess::requested(const WorkerControl& control) { + return (control.state == nullptr) ? 0U : + control.state->requested.load(std::memory_order_acquire); + } + + void + WorkerControlAccess::snapshot(const WorkerControl& control, + unsigned int& requested, + unsigned long long int& generation) { + WorkerControl::State* state = control.state; + if (state == nullptr) { + requested = 0U; + generation = 0U; + return; + } + Support::Lock lock(state->mutex); + requested = state->requested.load(std::memory_order_relaxed); + generation = state->generation.load(std::memory_order_relaxed); + } + + void + WorkerControlAccess::wait(WorkerControl& control, unsigned int worker) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return; + Support::Event* event = nullptr; + { + Support::Lock lock(state->mutex); + unsigned int capacity = + state->capacity.load(std::memory_order_relaxed); + assert((state->events == nullptr) || (worker < capacity)); + if ((state->events != nullptr) && (worker < capacity)) + event = &state->events[worker]; + } + if (event != nullptr) + event->wait(); + } + + void + WorkerControlAccess::signal(WorkerControl& control, unsigned int worker) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return; + Support::Event* event = nullptr; + { + Support::Lock lock(state->mutex); + unsigned int capacity = + state->capacity.load(std::memory_order_relaxed); + assert((state->events == nullptr) || (worker < capacity)); + if ((state->events != nullptr) && (worker < capacity)) + event = &state->events[worker]; + } + if (event != nullptr) + event->signal(); + } + + void + WorkerControlAccess::signal_all(WorkerControl& control) { + WorkerControl::State* state = control.state; + if (state == nullptr) + return; + Support::Event* events; + unsigned int capacity; + { + Support::Lock lock(state->mutex); + events = state->events; + capacity = state->capacity.load(std::memory_order_relaxed); + } + if (events != nullptr) + for (unsigned int i=0U; i + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef GECODE_SEARCH_WORKER_CONTROL_HH +#define GECODE_SEARCH_WORKER_CONTROL_HH + +namespace Gecode { namespace Search { + + /// Internal access to worker-control state + class GECODE_SEARCH_EXPORT WorkerControlAccess { + public: + /// Attach \a control to a leaf engine with fixed \a capacity + static void attach(WorkerControl& control, unsigned int capacity); + /// Detach \a control from its leaf engine + static void detach(WorkerControl& control); + /// Return the request generation + static unsigned long long int generation(const WorkerControl& control); + /// Return whether \a control has state + static bool engaged(const WorkerControl& control); + /// Return the unchanged-capacity fast-admission flag + static const std::atomic* + fast_admission(const WorkerControl& control); + /// Publish fast admission after reconciling \a generation at capacity + static void fast_admission(WorkerControl& control, + unsigned long long int generation); + /// Return whether \a x and \a y share one control identity + static bool same_identity(const WorkerControl& x, + const WorkerControl& y); + /// Return the current request + static unsigned int requested(const WorkerControl& control); + /// Return a consistent request and generation snapshot + static void snapshot(const WorkerControl& control, unsigned int& requested, + unsigned long long int& generation); + /// Wait for the event belonging to worker \a worker + static void wait(WorkerControl& control, unsigned int worker); + /// Signal the event belonging to worker \a worker + static void signal(WorkerControl& control, unsigned int worker); + /// Signal every worker event + static void signal_all(WorkerControl& control); + }; + +}} + +#endif diff --git a/test/search.cpp b/test/search.cpp index 48e0bdfdcb..81c0ea4353 100644 --- a/test/search.cpp +++ b/test/search.cpp @@ -40,6 +40,8 @@ #include "test/test.hh" +#include +#include #include static_assert(std::is_copy_constructible::value, @@ -62,6 +64,12 @@ static_assert(std::is_copy_constructible::value, "RestartStop must remain copy constructible"); static_assert(std::is_copy_assignable::value, "RestartStop must remain copy assignable"); +static_assert( + std::is_copy_constructible::value, + "WorkerControl must remain copy constructible"); +static_assert( + std::is_copy_assignable::value, + "WorkerControl must remain copy assignable"); namespace Test { @@ -381,6 +389,100 @@ namespace Test { htb1(_htb1), htb2(_htb2), htb3(_htb3), htc(_htc) {} }; + /// Randomly resize a live leaf engine from another thread + class WorkerResizer { + private: + Gecode::Search::WorkerControl control; + unsigned int capacity; + std::atomic done; + std::thread worker; + void resize(unsigned int seed) { + Gecode::Support::RandomGenerator rand(seed); + while (!done.load(std::memory_order_acquire)) { + control.request(1U + rand(capacity)); + std::this_thread::yield(); + } + } + public: + WorkerResizer(const Gecode::Search::WorkerControl& control0, + unsigned int capacity0, unsigned int seed, bool enabled) + : control(control0), capacity(capacity0), done(false) { + if (enabled) + worker = std::thread(&WorkerResizer::resize,this,seed); + } + ~WorkerResizer(void) { + done.store(true,std::memory_order_release); + if (worker.joinable()) + worker.join(); + } + }; + + /// Return the effective leaf capacity for this build + static unsigned int + worker_capacity(unsigned int threads) { +#ifdef GECODE_HAS_THREADS + return threads; +#else + (void) threads; + return 1U; +#endif + } + + /// Focused API and binding checks not covered by randomized search + class WorkerControlAPI : public Base { + private: + template + static bool throws(Function function) { + try { + function(); + } catch (const Exception&) { + return true; + } catch (...) { + } + return false; + } + public: + WorkerControlAPI(void) : Base("Search::WorkerControl::API") {} + bool run(void) override { + using Gecode::Search::WorkerControl; + WorkerControl empty; + bool ok = !empty && (empty.requested() == 0U) && + (empty.capacity() == 0U) && + throws( + [&] { empty.request(1U); }) && + throws( + [] { WorkerControl invalid(0U); }); + + const unsigned int capacity = worker_capacity(2U); + WorkerControl control(capacity); + WorkerControl copy(control); + control.request(1U); + ok = ok && copy && (copy.requested() == 1U); + + Gecode::Search::Options o; + o.threads = 2.0; + o.worker_control = control; + SolveImmediate* root = + new SolveImmediate(HTB_NONE,HTB_NONE,HTB_NONE); + { + Gecode::DFS engine(root,o); + delete root; + root = nullptr; + ok = ok && (control.capacity() == capacity) && + throws( + [&] { control.request(capacity+1U); }); + delete engine.next(); + } + root = new SolveImmediate(HTB_NONE,HTB_NONE,HTB_NONE); + ok = ok && throws( + [&] { Gecode::DFS engine(root,o); }); + delete root; + return ok; + } + }; + + WorkerControlAPI worker_control_api; + /// %Test for depth-first search template class DFS : public Test { @@ -408,7 +510,17 @@ namespace Test { o.a_d = a_d; o.threads = t; o.stop = &f; + const unsigned int capacity = worker_capacity(t); + const bool adjustable = _rand(2U) != 0U; + Gecode::Search::WorkerControl control; + if (adjustable) { + control = Gecode::Search::WorkerControl(1U + _rand(capacity)); + o.worker_control = control; + } Gecode::DFS dfs(m,o); + WorkerResizer resize(control,capacity,_rand.next(), + adjustable && (capacity > 1U) && + (_rand(64U) == 0U)); int n = m->solutions(); delete m; while (true) { @@ -489,7 +601,17 @@ namespace Test { o.a_d = a_d; o.threads = t; o.stop = &f; + const unsigned int capacity = worker_capacity(t); + const bool adjustable = _rand(2U) != 0U; + Gecode::Search::WorkerControl control; + if (adjustable) { + control = Gecode::Search::WorkerControl(1U + _rand(capacity)); + o.worker_control = control; + } Gecode::BAB bab(m,o); + WorkerResizer resize(control,capacity,_rand.next(), + adjustable && (capacity > 1U) && + (_rand(64U) == 0U)); delete m; Model* b = nullptr; while (true) { @@ -527,7 +649,16 @@ namespace Test { o.stop = &f; o.d_l = 100; o.cutoff = Gecode::Search::Cutoff::geometric(1,2); + const unsigned int capacity = worker_capacity(t); + const bool adjustable = _rand(2U) != 0U; + Gecode::Search::WorkerControl control; + if (adjustable) { + control = Gecode::Search::WorkerControl(1U + _rand(capacity)); + o.worker_control = control; + } Gecode::RBS rbs(m,o); + WorkerResizer resize(control,capacity,_rand.next(), + adjustable && (capacity > 1U)); int n = m->solutions(); delete m; while (true) { @@ -567,7 +698,15 @@ namespace Test { o.threads = t; o.d_l = 100; o.stop = &f; + Gecode::Search::WorkerControl control; + if (a == 1U) { + const unsigned int capacity = worker_capacity(t); + control = Gecode::Search::WorkerControl(1U + _rand(capacity)); + o.worker_control = control; + } Gecode::PBS pbs(m,o); + WorkerResizer resize(control,worker_capacity(t),_rand.next(), + (a == 1U) && (worker_capacity(t) > 1U)); if (best) { Model* b = nullptr; while (true) { @@ -629,16 +768,27 @@ namespace Test { so.threads = st; so.d_l = 100; so.cutoff = Gecode::Search::Cutoff::constant(1000000); + const unsigned int capacity = worker_capacity(st); + Gecode::Search::WorkerControl controls[3]; + Gecode::Search::Options leaf[3] = {so,so,so}; + for (unsigned int i=0U; i<3U; i++) + if (best || (i != 1U)) { + controls[i] = + Gecode::Search::WorkerControl(1U + _rand(capacity)); + leaf[i].worker_control = controls[i]; + } if (best) { SEBs sebs(3); - sebs[0] = bab(so); - sebs[1] = bab(so); - sebs[2] = rbs(so); + sebs[0] = bab(leaf[0]); + sebs[1] = bab(leaf[1]); + sebs[2] = rbs(leaf[2]); Gecode::PBS pbs(m, sebs, mo); delete m; Model* b = nullptr; while (true) { + for (unsigned int i=0U; i<3U; i++) + controls[i].request(1U + _rand(capacity)); Model* s = pbs.next(); if (s != nullptr) { delete b; b=s; @@ -652,15 +802,17 @@ namespace Test { return ok; } else { SEBs sebs(3); - sebs[0] = dfs(so); - sebs[1] = lds(so); - sebs[2] = rbs(so); + sebs[0] = dfs(leaf[0]); + sebs[1] = lds(leaf[1]); + sebs[2] = rbs(leaf[2]); Gecode::PBS pbs(m, sebs, mo); int n = 3 * m->solutions(); delete m; while (true) { + controls[0].request(1U + _rand(capacity)); + controls[2].request(1U + _rand(capacity)); Model* s = pbs.next(); if (s != nullptr) { n--; delete s;