diff --git a/include/libcamera/internal/camera_manager.h b/include/libcamera/internal/camera_manager.h
index e6df676bb655..ed90d1d471a0 100644
--- a/include/libcamera/internal/camera_manager.h
+++ b/include/libcamera/internal/camera_manager.h
@@ -15,6 +15,7 @@
 
 #include <libcamera/base/class.h>
 #include <libcamera/base/mutex.h>
+#include <libcamera/base/object.h>
 #include <libcamera/base/thread.h>
 #include <libcamera/base/thread_annotations.h>
 
@@ -30,7 +31,7 @@ class MediaDevice;
 class PipelineHandler;
 class PipelineHandlerFactoryBase;
 
-class CameraManager::Private : public Extensible::Private, public Thread
+class CameraManager::Private : public Extensible::Private, public Thread, public Object
 {
 	LIBCAMERA_DECLARE_PUBLIC(CameraManager)
 
@@ -54,6 +55,7 @@ protected:
 private:
 	int init();
 	std::vector<const PipelineHandlerFactoryBase *> pipelineFactories() const;
+	int startThread() LIBCAMERA_TSA_EXCLUDES(mutex_);
 	void createPipelineHandlers();
 	void pipelineFactoryMatch(const PipelineHandlerFactoryBase *factory);
 	std::shared_ptr<PipelineHandler> findMatchingHandler(const MediaDevice *media);
@@ -72,6 +74,8 @@ private:
 	bool initialized_ LIBCAMERA_TSA_GUARDED_BY(mutex_);
 	int status_ LIBCAMERA_TSA_GUARDED_BY(mutex_);
 
+	bool started_ LIBCAMERA_TSA_GUARDED_BY(mutex_) = false;
+
 	std::unique_ptr<DeviceEnumerator> enumerator_;
 
 	/*
diff --git a/src/libcamera/camera_manager.cpp b/src/libcamera/camera_manager.cpp
index 55cf650b8e66..55532179a61c 100644
--- a/src/libcamera/camera_manager.cpp
+++ b/src/libcamera/camera_manager.cpp
@@ -41,14 +41,45 @@ LOG_DEFINE_CATEGORY(Camera)
 CameraManager::Private::Private()
 	: Thread("CameraManager"), initialized_(false)
 {
+	/*
+	 * Bind this Object to its own thread, so that work can be marshalled
+	 * onto the camera manager thread with invokeMethod().
+	 */
+	moveToThread(this);
 }
 
 int CameraManager::Private::start()
 {
-	int status;
+	return startThread();
+}
+
+/*
+ * Start the camera manager thread if not started yet, and wait for its
+ * initialization to complete. Returns the initialization status.
+ */
+int CameraManager::Private::startThread()
+{
+	bool start = false;
+
+	{
+		MutexLocker locker(mutex_);
 
-	/* Start the thread and wait for initialization to complete. */
-	Thread::start();
+		if (!started_) {
+			started_ = true;
+			initialized_ = false;
+			start = true;
+		}
+	}
+
+	if (start)
+		Thread::start();
+
+	/*
+	 * Wait for initialization to complete, whether this call started the
+	 * thread or another one did, as the caller may otherwise proceed before
+	 * the thread is ready.
+	 */
+	int status;
 
 	{
 		MutexLocker locker(mutex_);
@@ -78,7 +109,7 @@ void CameraManager::Private::run()
 	status_ = ret;
 	initialized_ = true;
 	mutex_.unlock();
-	cv_.notify_one();
+	cv_.notify_all();
 
 	if (ret < 0) {
 		cleanup();
@@ -228,6 +259,11 @@ void CameraManager::Private::cleanup()
 {
 	enumerator_->devicesAdded.disconnect(this);
 
+	{
+		MutexLocker locker(mutex_);
+		started_ = false;
+	}
+
 	/*
 	 * Release all references to cameras to ensure they all get destroyed
 	 * before the device enumerator deletes the media devices. Cameras are
