Rework: Modularize Mind

Now we have modularized the Mind class to contain all of its
ComponentThreads. This enables us to run multiple mind instances
within the same SMO process, at least in theory.

We probably won't actually do this, but we want to ensure that the
design is clean enough to enable it.
This commit is contained in:
2025-09-03 14:43:00 -04:00
parent eb069c4a96
commit 0dc8abaa28
6 changed files with 292 additions and 305 deletions
+1
View File
@@ -2,6 +2,7 @@
#include <componentThread.h>
#include <marionette/marionette.h>
int main(int argc, char *argv[], char *envp[])
{
/* We don't do anything inside of main()
+30 -208
View File
@@ -11,74 +11,15 @@ namespace smo {
thread_local std::shared_ptr<ComponentThread> thisComponentThread;
const std::string ComponentThread::threadNames[N_ITEMS] =
{
"mrntt",
"director",
"simulator",
"subconscious",
"body",
"world"
};
namespace mrntt {
std::shared_ptr<ComponentThread> mrntt =
std::make_shared<ComponentThread>(ComponentThread::MRNTT);
}
namespace director {
/* The director is the seat of volition in Salmanoff. It receives sensor
* events from the body and world, and uses them to direct its implexors
* to implex new menties. It then loads the menties into canvas for simulation
* and correlation with intrins, in order to form new attrimotions and
* menties.
*/
std::shared_ptr<ComponentThread> director =
std::make_shared<ComponentThread>(ComponentThread::DIRECTOR);
}
namespace simulator {
/* The canvas is the simulation engine in Salmanoff. It receives menties and
* simulates them in accordance with the instructions from director. It then
* re-renders them into perception for director to get feedback.
*/
std::shared_ptr<ComponentThread> canvas =
std::make_shared<ComponentThread>(ComponentThread::SIMULATOR);
}
namespace subconscious {
/* The subconscious is the seat of memory in Salmanoff. It receives menties
* from director and stores them in memory for later recall.
*/
std::shared_ptr<ComponentThread> subconscious =
std::make_shared<ComponentThread>(ComponentThread::SUBCONSCIOUS);
}
namespace body {
/* The body is a thread that polls, processes, and sends interoceptive sensor
* events to director. It enables these events to occur asynchronously,
* indepdendent any actions that the other threads are taking.
*/
std::shared_ptr<ComponentThread> body =
std::make_shared<ComponentThread>(ComponentThread::BODY);
}
namespace world {
/* The world performs the same functions as the body, but for extrospective
* sensor events.
*/
std::shared_ptr<ComponentThread> world =
std::make_shared<ComponentThread>(ComponentThread::WORLD);
extern std::shared_ptr<ComponentThread> mrntt;
}
// Initialize static state
std::atomic<bool> ComponentThread::threadsHaveBeenJolted{false};
std::array<std::shared_ptr<ComponentThread>, ComponentThread::N_ITEMS>
ComponentThread::componentThreads =
// Implementation of static method
std::shared_ptr<ComponentThread> ComponentThread::getMrntt()
{
mrntt::mrntt,
director::director,
simulator::canvas,
subconscious::subconscious,
body::body,
world::world
};
return mrntt::mrntt;
}
void ComponentThread::initializeTls(void)
{
@@ -239,125 +180,6 @@ void ComponentThread::joltThreadReq(std::function<void()> callback)
});
}
struct AllMindThreadsOpReqContext {
AllMindThreadsOpReqContext() : nThreadsProcessed(0) {}
int nThreadsProcessed;
};
static const std::string getOpName(ComponentThread::ThreadOp op)
{
if (op < (ComponentThread::ThreadOp)0
|| op > ComponentThread::ThreadOp::JOLT)
{
throw std::runtime_error(std::string(__func__)
+ ": Invalid operation");
}
switch (op)
{
case ComponentThread::ThreadOp::START: return "starting";
case ComponentThread::ThreadOp::PAUSE: return "pausing";
case ComponentThread::ThreadOp::RESUME: return "resuming";
case ComponentThread::ThreadOp::EXIT: return "exiting";
case ComponentThread::ThreadOp::JOLT: return "jolting";
default: return "unknown";
}
}
void ComponentThread::execOpOnAllMindThreadsReq(
ThreadOp op, std::function<void()> callback
)
{
std::shared_ptr<ComponentThread> self = getSelf();
// Check that we're being called from the marionette thread
if (self->id != MRNTT)
{
throw std::runtime_error(std::string(__func__)
+ ": invoked on non-mrntt thread " + self->name);
}
std::cout << "Mrntt: " << getOpName(op) << " all mind threads." << "\n";
auto context = std::make_shared<AllMindThreadsOpReqContext>();
const int N_THREADS_EXCEPT_MRNTT = ComponentThread::N_ITEMS - 1;
for (auto &currThread : ComponentThread::componentThreads)
{
if (currThread->id == ComponentThread::MRNTT)
{ continue; }
auto threadCallback = [context, callback, N_THREADS_EXCEPT_MRNTT, op]()
{
++context->nThreadsProcessed;
if (context->nThreadsProcessed < N_THREADS_EXCEPT_MRNTT)
{ return; }
if (op == ThreadOp::EXIT)
{
// Special cleanup for exit operations
for (auto &currThreadJ : ComponentThread::componentThreads)
{
if (currThreadJ->id == ComponentThread::MRNTT)
{ continue; }
currThreadJ->thread.join();
}
}
std::cout << "Mrntt: all mind threads done " << getOpName(op) << "."
<< "\n";
if (callback) { callback(); }
};
switch (op) {
case ThreadOp::START:
currThread->startThreadReq(threadCallback);
break;
case ThreadOp::PAUSE:
currThread->pauseThreadReq(threadCallback);
break;
case ThreadOp::RESUME:
currThread->resumeThreadReq(threadCallback);
break;
case ThreadOp::EXIT:
currThread->exitThreadReq(threadCallback);
break;
case ThreadOp::JOLT:
currThread->joltThreadReq(threadCallback);
break;
default:
throw std::runtime_error("Invalid operation");
}
}
}
void ComponentThread::startAllMindThreadsReq(std::function<void()> callback)
{
execOpOnAllMindThreadsReq(ThreadOp::START, callback);
}
void ComponentThread::pauseAllMindThreadsReq(std::function<void()> callback)
{
execOpOnAllMindThreadsReq(ThreadOp::PAUSE, callback);
}
void ComponentThread::resumeAllMindThreadsReq(std::function<void()> callback)
{
execOpOnAllMindThreadsReq(ThreadOp::RESUME, callback);
}
void ComponentThread::exitAllMindThreadsReq(std::function<void()> callback)
{
execOpOnAllMindThreadsReq(ThreadOp::EXIT, callback);
}
void ComponentThread::joltAllMindThreadsReq(std::function<void()> callback)
{
execOpOnAllMindThreadsReq(ThreadOp::JOLT, callback);
}
/* This shouldn't take a callback because the caller shouldn't expect to
* Mrntt to send a reply signal to it. Sending this Indication means that
* Mrntt will send the calling thread an exitThreadReq. When the caller
@@ -382,8 +204,18 @@ void ComponentThread::exceptionInd(ComponentThread& thread)
std::cerr << "Mrntt: Exception occurred: in thread "
<< thread.name << ". Killing Salmanoff." << "\n";
// Delegate to common shutdown request
mind.finalizeReq(smo::mrntt::exitMarionetteLoop);
/** EXPLANATION:
* An exception has occurred in one of a mind's threads. We need to
* shut down all of that particular mind's threads.
*/
thread.parent.finalizeReq([]() {
/** FIXME:
* When we eventually support multiple minds, we should remove this
* since it causes marionette to exit, even if there are other minds
* that are still running.
*/
smo::mrntt::exitMarionetteLoop();
});
});
}
@@ -397,13 +229,23 @@ void ComponentThread::userShutdownInd()
// Post the user shutdown to the mrntt thread.
this->getIoService().post(
[]()
[this]()
{
std::cerr << "Mrntt: User requested shutdown (SIGINT)."
<< " Killing Salmanoff." << "\n";
// Delegate to common shutdown request
mind.finalizeReq(smo::mrntt::exitMarionetteLoop);
/** EXPLANATION:
* A user has requested a shutdown. We need to shut down all of the
* threads in all running Minds.
*/
parent.finalizeReq([]() {
/** FIXME:
* When we eventually support multiple minds, we should remove this
* since it causes marionette to exit, even if there are other minds
* that are still running.
*/
smo::mrntt::exitMarionetteLoop();
});
});
}
@@ -455,24 +297,4 @@ void ComponentThread::pinToCpu(int cpuId)
std::cout << name << ": Pinned to CPU " << cpuId << "\n";
}
void ComponentThread::distributeAndPinThreadsAcrossCpus()
{
int cpuCount = getAvailableCpuCount();
std::cout << "Available CPUs: " << cpuCount << "\n";
// Skip the marionette thread (MRNTT) as it's the control thread
int threadIndex = 0;
for (auto& thread : componentThreads)
{
if (thread->id == MRNTT) { continue; }
int targetCpu = threadIndex % cpuCount;
thread->pinToCpu(targetCpu);
++threadIndex;
}
std::cout << "Distributed " << (threadIndex) << " threads across "
<< cpuCount << " CPUs\n";
}
} // namespace smo
+20 -78
View File
@@ -11,9 +11,12 @@
#include <pthread.h>
#include <sched.h>
#include <unistd.h>
#include <memory>
namespace smo {
class Mind; // Forward declaration
class ComponentThread
: public std::enable_shared_from_this<ComponentThread>
{
@@ -29,8 +32,8 @@ public:
N_ITEMS
};
ComponentThread(ThreadId id)
: id(id), name(getThreadName(id)),
ComponentThread(ThreadId _id, Mind& parent)
: id(_id), name(getThreadName(_id)), parent(parent),
work(io_service), pause_work(pause_io_service),
pinnedCpuId(-1),
thread(
@@ -45,34 +48,8 @@ public:
void initializeTls(void);
static const std::shared_ptr<ComponentThread> getSelf(void);
static std::shared_ptr<ComponentThread> getComponentThread(
ThreadId id = N_ITEMS)
{
if (id < 0 || id > N_ITEMS)
{
throw std::runtime_error(std::string(__func__)
+ ": Invalid thread ID");
}
return componentThreads[id];
}
// Overload: search by name
static std::shared_ptr<ComponentThread> getComponentThread(
const std::string& name)
{
for (auto& thread : componentThreads) {
if (thread->name == name) { return thread; }
}
throw std::runtime_error(std::string(__func__)
+ ": Thread name not found in componentThreads map");
}
static boost::asio::io_service& getEventLoop(
ThreadId id = MRNTT)
{
return getComponentThread(id)->getIoService();
}
static std::shared_ptr<ComponentThread> getMrntt();
Mind& getParent() const { return parent; }
typedef void (mainFn)(ComponentThread &self);
static mainFn main, marionetteMain;
@@ -91,17 +68,9 @@ public:
*/
void joltThreadReq(std::function<void()> callback = nullptr);
// Convenience wrappers
static void startAllMindThreadsReq(std::function<void()> callback = nullptr);
static void pauseAllMindThreadsReq(std::function<void()> callback = nullptr);
static void resumeAllMindThreadsReq(std::function<void()> callback = nullptr);
static void exitAllMindThreadsReq(std::function<void()> callback = nullptr);
static void joltAllMindThreadsReq(std::function<void()> callback = nullptr);
// CPU management methods
static int getAvailableCpuCount();
void pinToCpu(int cpuId);
static void distributeAndPinThreadsAcrossCpus();
enum class ThreadOp
{
@@ -112,8 +81,6 @@ public:
JOLT,
N_ITEMS
};
static void execOpOnAllMindThreadsReq(
ThreadOp op, std::function<void()> callback = nullptr);
// Intentionally doesn't take a callback.
void exceptionInd(ComponentThread& thread);
@@ -123,41 +90,20 @@ public:
public:
ThreadId id;
std::string name;
Mind &parent;
boost::asio::io_service io_service;
boost::asio::io_service::work work;
boost::asio::io_service pause_io_service;
boost::asio::io_service::work pause_work;
std::atomic<bool> keepLooping;
int pinnedCpuId;
/**
* Indicates whether all mind threads have been JOLTed at least once.
*
* JOLTing serves two critical purposes:
*
* 1. **Global Constructor Sequencing**: Since pthreads begin executing while
* global constructors are still being executed, globally defined pthreads
* cannot depend on global objects having been constructed. JOLTing is done
* by the CRT's main thread within main(), which provides a sequencing
* guarantee that global constructors have been called.
*
* 2. **shared_from_this Safety**: shared_from_this() requires a prior
* shared_ptr handle to be established. The global list of
* shared_ptr<ComponentThread> guarantees that at least one shared_ptr to
* each ComponentThread has been initialized before JOLTing occurs.
*
* This atomic flag ensures that JOLTing happens exactly once and provides
* a synchronization point for the entire system initialization.
*/
static std::atomic<bool> threadsHaveBeenJolted;
/* Always ensure that this is last so that the thread is spawned after
* everything else is constructed.
*/
std::thread thread;
static std::array<std::shared_ptr<ComponentThread>, ComponentThread::N_ITEMS>
componentThreads;
static const std::string threadNames[ComponentThread::N_ITEMS];
static const std::string getThreadName(ThreadId id)
{
if (id < 0 || id >= ComponentThread::N_ITEMS)
@@ -165,6 +111,17 @@ public:
throw std::runtime_error(std::string(__func__)
+ ": Invalid thread ID");
}
// Use function-local static to ensure proper initialization order
static const std::string threadNames[N_ITEMS] = {
"mrntt",
"director",
"simulator",
"subconscious",
"body",
"world"
};
return threadNames[id];
}
};
@@ -172,21 +129,6 @@ public:
namespace mrntt {
extern std::shared_ptr<ComponentThread> mrntt;
}
namespace director {
extern std::shared_ptr<ComponentThread> director;
}
namespace simulator {
extern std::shared_ptr<ComponentThread> canvas;
}
namespace subconscious {
extern std::shared_ptr<ComponentThread> subconscious;
}
namespace body {
extern std::shared_ptr<ComponentThread> body;
}
namespace world {
extern std::shared_ptr<ComponentThread> world;
}
} // namespace smo
+51 -3
View File
@@ -4,21 +4,44 @@
#include <config.h>
#include <thread>
#include <functional>
#include <memory>
#include <unordered_map>
#include <string>
#include <director/director.h>
#include <simulator/simulator.h>
#include <componentThread.h>
namespace smo {
class Mind
class Mind : public std::enable_shared_from_this<Mind>
{
public:
Mind(void) {}
Mind(void);
~Mind(void) = default;
void initialize(void);
void execute(void);
void finalizeReq(std::function<void()> callback);
// ComponentThread access methods
std::shared_ptr<ComponentThread> getComponentThread(
ComponentThread::ThreadId id) const;
std::shared_ptr<ComponentThread> getComponentThread(
const std::string& name) const;
// Get all this Mind's component threads.
std::vector<std::shared_ptr<ComponentThread>> getMindThreads() const;
// Thread management methods (moved from ComponentThread)
void startAllMindThreadsReq(std::function<void()> callback = nullptr);
void pauseAllMindThreadsReq(std::function<void()> callback = nullptr);
void resumeAllMindThreadsReq(std::function<void()> callback = nullptr);
void exitAllMindThreadsReq(std::function<void()> callback = nullptr);
void joltAllMindThreadsReq(std::function<void()> callback = nullptr);
// CPU distribution method
void distributeAndPinThreadsAcrossCpus();
public:
std::thread directorThread;
std::thread simulatorThread;
@@ -26,9 +49,34 @@ public:
director::Director director;
simulator::Simulator canvas;
private:
/**
* Indicates whether all mind threads have been JOLTed at least once.
*
* JOLTing serves two critical purposes:
*
* 1. **Global Constructor Sequencing**: Since pthreads begin executing while
* global constructors are still being executed, globally defined pthreads
* cannot depend on global objects having been constructed. JOLTing is done
* by the CRT's main thread within main(), which provides a sequencing
* guarantee that global constructors have been called.
*
* 2. **shared_from_this Safety**: shared_from_this() requires a prior
* shared_ptr handle to be established. The global list of
* shared_ptr<ComponentThread> guarantees that at least one shared_ptr to
* each ComponentThread has been initialized before JOLTing occurs.
*
* This flag ensures that JOLTing happens exactly once and provides
* a synchronization point for the entire system initialization.
*/
bool threadsHaveBeenJolted = false;
// Collection of ComponentThread instances (excluding marionette)
std::vector<std::shared_ptr<ComponentThread>> componentThreads;
};
extern Mind mind;
// Global Mind instance will be defined in marionette.cpp
extern std::shared_ptr<Mind> globalMind;
} // namespace smo
+12 -4
View File
@@ -3,15 +3,22 @@
#include <iostream>
#include <exception>
#include <opts.h>
#include <typeinfo>
#include <boost/asio/signal_set.hpp>
#include <mind.h>
#include <componentThread.h>
#include <marionette/marionette.h>
#include <salmanoff.h>
#include <boost/asio/signal_set.hpp>
#include <typeinfo>
namespace smo {
// Global Mind instance
std::shared_ptr<Mind> globalMind = std::make_shared<Mind>();
// Global marionette thread instance
std::shared_ptr<ComponentThread> mrntt::mrntt =
std::make_shared<ComponentThread>(ComponentThread::MRNTT, *globalMind);
CrtCommandLineArgs crtCommandLineArgs(0, nullptr, nullptr);
void CrtCommandLineArgs::set(int argc, char *argv[], char *envp[])
@@ -71,7 +78,8 @@ void ComponentThread::marionetteMain(ComponentThread& self)
initializeSalmanoff();
self.getIoService().post([]()
{
mind.initialize();
// Initialize the global Mind object
globalMind->initialize();
});
std::cout << __func__ << ": Entering event loop" << "\n";
@@ -143,7 +151,7 @@ void ComponentThread::marionetteMain(ComponentThread& self)
if (callFinalizeReq)
{
mind.finalizeReq([]{
globalMind->finalizeReq([]{
mrntt::mrntt->getIoService().stop();
});
self.getIoService().reset();
+178 -12
View File
@@ -4,14 +4,23 @@
namespace smo {
Mind mind;
Mind::Mind(void)
: componentThreads{
std::make_shared<ComponentThread>(ComponentThread::DIRECTOR, *this),
std::make_shared<ComponentThread>(ComponentThread::SIMULATOR, *this),
std::make_shared<ComponentThread>(ComponentThread::SUBCONSCIOUS, *this),
std::make_shared<ComponentThread>(ComponentThread::BODY, *this),
std::make_shared<ComponentThread>(ComponentThread::WORLD, *this)
}
{
}
void Mind::initialize()
{
/* Distribute threads across available CPUs */
try
{
ComponentThread::distributeAndPinThreadsAcrossCpus();
distributeAndPinThreadsAcrossCpus();
}
catch (const std::exception& e)
{
@@ -21,12 +30,12 @@ void Mind::initialize()
}
/* Jolt the threads, then start them */
ComponentThread::joltAllMindThreadsReq(
[]()
joltAllMindThreadsReq(
[this]()
{
ComponentThread::threadsHaveBeenJolted.store(true);
std::cout << "Mrntt: All mind threads JOLTed." << "\n";
ComponentThread::startAllMindThreadsReq(
// Start all threads after JOLTing
startAllMindThreadsReq(
[]()
{
std::cout << "Mrntt: All mind threads started." << "\n";
@@ -42,13 +51,12 @@ void Mind::finalizeReq(std::function<void()> callback)
* otherwise they'll just enter their main loops and wait for control
* messages from mrntt after processing the exit request.
*/
if (!ComponentThread::threadsHaveBeenJolted.load())
if (!threadsHaveBeenJolted)
{
ComponentThread::joltAllMindThreadsReq(
[callback]()
joltAllMindThreadsReq(
[this, callback]()
{
ComponentThread::threadsHaveBeenJolted.store(true);
ComponentThread::exitAllMindThreadsReq(
exitAllMindThreadsReq(
[callback]()
{
std::cout << "Mrntt: All mind threads exited." << "\n";
@@ -60,7 +68,7 @@ void Mind::finalizeReq(std::function<void()> callback)
}
else
{
ComponentThread::exitAllMindThreadsReq(
exitAllMindThreadsReq(
[callback]()
{
std::cout << "Mrntt: All mind threads exited." << "\n";
@@ -70,4 +78,162 @@ void Mind::finalizeReq(std::function<void()> callback)
}
}
void Mind::joltAllMindThreadsReq(std::function<void()> callback)
{
// Create a counter to track when all threads have been jolted
auto counter = std::make_shared<std::atomic<int>>(componentThreads.size());
for (auto& thread : componentThreads)
{
thread->joltThreadReq([counter, callback, this]() {
if (--(*counter) == 0)
{
// Set the flag only after all threads have ACKed their JOLT
threadsHaveBeenJolted = true;
if (callback) { callback(); }
}
});
}
// If no threads, set flag and call callback immediately
if (componentThreads.empty())
{
threadsHaveBeenJolted = true;
if (callback) { callback(); }
}
}
std::shared_ptr<ComponentThread>
Mind::getComponentThread(ComponentThread::ThreadId id) const
{
// Access the global marionette thread using ComponentThread::getMrntt()
if (id == ComponentThread::MRNTT) { return ComponentThread::getMrntt(); }
// Search through the vector for the thread with matching id
for (auto& thread : componentThreads) {
if (thread->id == id) { return thread; }
}
// Throw exception if no thread found
throw std::runtime_error(std::string(__func__) +
": No ComponentThread found with ID "
+ std::to_string(static_cast<int>(id)));
}
std::shared_ptr<ComponentThread>
Mind::getComponentThread(const std::string& name) const
{
if (name == "mrntt") { return ComponentThread::getMrntt(); }
for (auto& thread : componentThreads) {
if (thread->name == name) { return thread; }
}
// Throw exception if no thread found
throw std::runtime_error(std::string(__func__) +
": No ComponentThread found with name '" + name + "'");
}
std::vector<std::shared_ptr<ComponentThread>>
Mind::getMindThreads() const
{
return componentThreads;
}
void Mind::distributeAndPinThreadsAcrossCpus()
{
int cpuCount = ComponentThread::getAvailableCpuCount();
std::cout << "Available CPUs: " << cpuCount << "\n";
// Distribute and pin threads across CPUs
int threadIndex = 0;
for (auto& thread : componentThreads)
{
int targetCpu = threadIndex % cpuCount;
thread->pinToCpu(targetCpu);
++threadIndex;
}
std::cout << "Distributed " << threadIndex << " threads across "
<< cpuCount << " CPUs\n";
}
// Thread management methods (moved from ComponentThread)
void Mind::startAllMindThreadsReq(std::function<void()> callback)
{
// Create a counter to track when all threads have started
auto counter = std::make_shared<std::atomic<int>>(componentThreads.size());
for (auto& thread : componentThreads)
{
thread->startThreadReq([counter, callback]() {
if (--(*counter) == 0 && callback) { callback(); }
});
}
// If no threads, call callback immediately
if (componentThreads.empty() && callback) { callback(); }
}
void Mind::pauseAllMindThreadsReq(std::function<void()> callback)
{
// Create a counter to track when all threads have paused
auto counter = std::make_shared<std::atomic<int>>(componentThreads.size());
for (auto& thread : componentThreads)
{
thread->pauseThreadReq([counter, callback]() {
if (--(*counter) == 0 && callback) { callback(); }
});
}
// If no threads, call callback immediately
if (componentThreads.empty() && callback) {
callback();
}
}
void Mind::resumeAllMindThreadsReq(std::function<void()> callback)
{
// Create a counter to track when all threads have resumed
auto counter = std::make_shared<std::atomic<int>>(componentThreads.size());
for (auto& thread : componentThreads)
{
thread->resumeThreadReq([counter, callback]() {
if (--(*counter) == 0 && callback) { callback(); }
});
}
// If no threads, call callback immediately
if (componentThreads.empty() && callback) {
callback();
}
}
void Mind::exitAllMindThreadsReq(std::function<void()> callback)
{
// Create a counter to track when all threads have exited
auto counter = std::make_shared<std::atomic<int>>(componentThreads.size());
for (auto& thread : componentThreads)
{
thread->exitThreadReq([counter, callback, this]() {
if (--(*counter) == 0)
{
// All threads have exited their loops, now join them
for (auto& t : componentThreads) {
t->thread.join();
}
if (callback) { callback(); }
}
});
}
// If no threads, call callback immediately
if (componentThreads.empty() && callback) {
callback();
}
}
} // namespace smo