diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java index cc34bf6ff..0cacb9b47 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java @@ -27,6 +27,7 @@ import com.facebook.react.bridge.queue.ReactQueueConfigurationSpec; import com.facebook.react.bridge.queue.QueueThreadExceptionHandler; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.common.futures.SimpleSettableFuture; import com.facebook.systrace.Systrace; import com.facebook.systrace.TraceListener; @@ -264,6 +265,10 @@ public class CatalystInstanceImpl implements CatalystInstance { // TODO: tell all APIs to shut down mDestroyed = true; mJavaRegistry.notifyCatalystInstanceDestroy(); + + Systrace.unregisterListener(mTraceListener); + + synchronouslyDisposeBridgeOnJSThread(); mReactQueueConfiguration.destroy(); boolean wasIdle = (mPendingJSCalls.getAndSet(0) == 0); if (!wasIdle && !mBridgeIdleListeners.isEmpty()) { @@ -271,12 +276,19 @@ public class CatalystInstanceImpl implements CatalystInstance { listener.onTransitionToBridgeIdle(); } } + } - Systrace.unregisterListener(mTraceListener); - - // We can access the Bridge from any thread now because we know either we are on the JS thread - // or the JS thread has finished via ReactQueueConfiguration#destroy() - mBridge.dispose(); + private void synchronouslyDisposeBridgeOnJSThread() { + final SimpleSettableFuture bridgeDisposeFuture = new SimpleSettableFuture<>(); + mReactQueueConfiguration.getJSQueueThread().runOnQueue( + new Runnable() { + @Override + public void run() { + mBridge.dispose(); + bridgeDisposeFuture.set(null); + } + }); + bridgeDisposeFuture.getOrThrow(); } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/futures/SimpleSettableFuture.java b/ReactAndroid/src/main/java/com/facebook/react/common/futures/SimpleSettableFuture.java index 55fc9eb08..9caf34cd6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/common/futures/SimpleSettableFuture.java +++ b/ReactAndroid/src/main/java/com/facebook/react/common/futures/SimpleSettableFuture.java @@ -30,7 +30,7 @@ public class SimpleSettableFuture implements Future { * Sets the result. If another thread has called {@link #get}, they will immediately receive the * value. set or setException must only be called once. */ - public void set(T result) { + public void set(@Nullable T result) { checkNotSet(); mResult = result; mReadyLatch.countDown(); diff --git a/ReactAndroid/src/main/jni/react/JSCExecutor.cpp b/ReactAndroid/src/main/jni/react/JSCExecutor.cpp index ceb2564d5..5720905e7 100644 --- a/ReactAndroid/src/main/jni/react/JSCExecutor.cpp +++ b/ReactAndroid/src/main/jni/react/JSCExecutor.cpp @@ -3,9 +3,11 @@ #include "JSCExecutor.h" #include -#include +#include +#include #include #include + #include #include #include @@ -88,6 +90,45 @@ JSCExecutor::JSCExecutor(Bridge *bridge, const std::string& cacheDir) : m_bridge(bridge), m_deviceCacheDir(cacheDir), m_messageQueueThread(MessageQueues::getCurrentMessageQueueThread()) { + initOnJSVMThread(); +} + +JSCExecutor::JSCExecutor( + Bridge *bridge, + int workerId, + JSCExecutor *owner, + const std::string& script) : + m_bridge(bridge), + m_workerId(workerId), + m_owner(owner), + m_deviceCacheDir(owner->m_deviceCacheDir), + m_messageQueueThread(MessageQueues::getCurrentMessageQueueThread()) { + // We post initOnJSVMThread here so that the owner doesn't have to wait for + // initialization on its own thread + m_messageQueueThread->runOnQueue([this, script] () { + initOnJSVMThread(); + + installGlobalFunction(m_context, "postMessage", nativePostMessage); + + // TODO(9604438): Protect against script does not exist + std::string scriptSrc = WebWorkerUtil::loadScriptFromAssets(script); + // TODO(9994180): Throw on error + loadApplicationScript(scriptSrc, script); + }); +} + +JSCExecutor::~JSCExecutor() { + *m_isDestroyed = true; + if (m_messageQueueThread->isOnThread()) { + terminateOnJSVMThread(); + } else { + m_messageQueueThread->runOnQueueSync([this] () { + terminateOnJSVMThread(); + }); + } +} + +void JSCExecutor::initOnJSVMThread() { m_context = JSGlobalContextCreateInGroup(nullptr, nullptr); s_globalContextRefToJSCExecutor[m_context] = this; installGlobalFunction(m_context, "nativeFlushQueueImmediate", nativeFlushQueueImmediate); @@ -114,18 +155,20 @@ JSCExecutor::JSCExecutor(Bridge *bridge, const std::string& cacheDir) : #endif } -JSCExecutor::~JSCExecutor() { - // terminateWebWorker mutates m_webWorkers so collect all the workers to terminate first +void JSCExecutor::terminateOnJSVMThread() { + // terminateOwnedWebWorker mutates m_ownedWorkers so collect all the workers + // to terminate first std::vector workerIds; - for (auto it = m_webWorkers.begin(); it != m_webWorkers.end(); it++) { - workerIds.push_back(it->first); + for (auto& it : m_ownedWorkers) { + workerIds.push_back(it.first); } for (int workerId : workerIds) { - terminateWebWorker(workerId); + terminateOwnedWebWorker(workerId); } s_globalContextRefToJSCExecutor.erase(m_context); JSGlobalContextRelease(m_context); + m_context = nullptr; } void JSCExecutor::loadApplicationScript( @@ -162,6 +205,10 @@ void JSCExecutor::loadApplicationUnbundle( } void JSCExecutor::flush() { + if (m_owner != nullptr) { + // Web workers don't support native modules yet + return; + } // TODO: Make this a first class function instead of evaling. #9317773 std::string calls = executeJSCallWithJSC(m_context, "flushedQueue", std::vector()); m_bridge->callNativeModules(calls, true); @@ -253,56 +300,111 @@ void JSCExecutor::loadModule(uint32_t moduleId) { evaluateScript(m_context, source, sourceUrl); } -// WebWorker impl +int JSCExecutor::addWebWorker( + const std::string& script, + JSValueRef workerRef) { + static std::atomic_int nextWorkerId(1); + int workerId = nextWorkerId++; -JSGlobalContextRef JSCExecutor::getContext() { - return m_context; + auto workerMQT = WebWorkerUtil::createWebWorkerThread(workerId, m_messageQueueThread.get()); + std::unique_ptr worker; + workerMQT->runOnQueueSync([this, &worker, &script, workerId] () { + worker.reset(new JSCExecutor(m_bridge, workerId, this, script)); + }); + + Object workerObj = Value(m_context, workerRef).asObject(); + workerObj.makeProtected(); + + m_ownedWorkers.emplace(std::piecewise_construct, std::forward_as_tuple(workerId), std::forward_as_tuple(std::move(worker), std::move(workerObj))); + + return workerId; } -std::shared_ptr JSCExecutor::getMessageQueueThread() { - return m_messageQueueThread; +void JSCExecutor::postMessageToOwnedWebWorker(int workerId, JSValueRef message, JSValueRef *exn) { + auto worker = m_ownedWorkers.at(workerId).getExecutor(); + std::string msgString = Value(m_context, message).toJSONString(); + + std::shared_ptr isWorkerDestroyed = worker->m_isDestroyed; + worker->m_messageQueueThread->runOnQueue([isWorkerDestroyed, worker, msgString] () { + if (*isWorkerDestroyed) { + return; + } + worker->receiveMessageFromOwner(msgString); + }); } -void JSCExecutor::onMessageReceived(int workerId, const std::string& json) { - Object& worker = m_webWorkerJSObjs.at(workerId); +void JSCExecutor::postMessageToOwner(JSValueRef msg) { + std::string msgString = Value(m_context, msg).toJSONString(); + std::shared_ptr ownerIsDestroyed = m_owner->m_isDestroyed; + m_owner->m_messageQueueThread->runOnQueue([workerId=m_workerId, owner=m_owner, ownerIsDestroyed, msgString] () { + if (*ownerIsDestroyed) { + return; + } + owner->receiveMessageFromOwnedWebWorker(workerId, msgString); + }); +} - Value onmessageValue = worker.getProperty("onmessage"); +void JSCExecutor::receiveMessageFromOwnedWebWorker(int workerId, const std::string& json) { + Object* workerObj; + try { + workerObj = &m_ownedWorkers.at(workerId).jsObj; + } catch (std::out_of_range& e) { + // Worker was already terminated + return; + } + + Value onmessageValue = workerObj->getProperty("onmessage"); if (onmessageValue.isUndefined()) { return; } - JSValueRef args[] = { JSCWebWorker::createMessageObject(m_context, json) }; + JSValueRef args[] = { createMessageObject(json) }; onmessageValue.asObject().callAsFunction(1, args); flush(); } -int JSCExecutor::addWebWorker(const std::string& script, JSValueRef workerRef) { - static std::atomic_int nextWorkerId(0); - int workerId = nextWorkerId++; +void JSCExecutor::receiveMessageFromOwner(const std::string& msgString) { + CHECK(m_owner) << "Received message in a Executor that doesn't have an owner!"; - m_webWorkers.emplace(std::piecewise_construct, std::forward_as_tuple(workerId), std::forward_as_tuple(workerId, this, script)); - Object workerObj = Value(m_context, workerRef).asObject(); - workerObj.makeProtected(); - m_webWorkerJSObjs.emplace(workerId, std::move(workerObj)); - return workerId; + JSValueRef args[] = { createMessageObject(msgString) }; + Value onmessageValue = Object::getGlobalObject(m_context).getProperty("onmessage"); + onmessageValue.asObject().callAsFunction(1, args); } -void JSCExecutor::postMessageToWebWorker(int workerId, JSValueRef message, JSValueRef *exn) { - JSCWebWorker& worker = m_webWorkers.at(workerId); - worker.postMessage(message); +void JSCExecutor::terminateOwnedWebWorker(int workerId) { + auto worker = m_ownedWorkers.at(workerId).getExecutor(); + std::shared_ptr workerMQT = worker->m_messageQueueThread; + m_ownedWorkers.erase(workerId); + workerMQT->quitSynchronous(); } -void JSCExecutor::terminateWebWorker(int workerId) { - JSCWebWorker& worker = m_webWorkers.at(workerId); - - worker.terminate(); - - m_webWorkers.erase(workerId); - m_webWorkerJSObjs.erase(workerId); +Object JSCExecutor::createMessageObject(const std::string& msgJson) { + Value rebornJSMsg = Value::fromJSON(m_context, String(msgJson.c_str())); + Object messageObject = Object::create(m_context); + messageObject.setProperty("data", rebornJSMsg); + return messageObject; } // Native JS hooks +JSValueRef JSCExecutor::nativePostMessage( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception) { + if (argumentCount != 1) { + *exception = makeJSCException(ctx, "postMessage got wrong number of arguments"); + return JSValueMakeUndefined(ctx); + } + JSValueRef msg = arguments[0]; + JSCExecutor *webWorker = s_globalContextRefToJSCExecutor.at(JSContextGetGlobalContext(ctx)); + + webWorker->postMessageToOwner(msg); + + return JSValueMakeUndefined(ctx); +} static JSValueRef makeInvalidModuleIdJSCException( JSContextRef ctx, @@ -433,7 +535,7 @@ JSValueRef JSCExecutor::nativePostMessageToWorker( return JSValueMakeUndefined(ctx); } - executor->postMessageToWebWorker((int) workerDouble, arguments[1], exception); + executor->postMessageToOwnedWebWorker((int) workerDouble, arguments[1], exception); return JSValueMakeUndefined(ctx); } @@ -464,7 +566,7 @@ JSValueRef JSCExecutor::nativeTerminateWorker( return JSValueMakeUndefined(ctx); } - executor->terminateWebWorker((int) workerDouble); + executor->terminateOwnedWebWorker((int) workerDouble); return JSValueMakeUndefined(ctx); } diff --git a/ReactAndroid/src/main/jni/react/JSCExecutor.h b/ReactAndroid/src/main/jni/react/JSCExecutor.h index dbba889ab..c60f33422 100644 --- a/ReactAndroid/src/main/jni/react/JSCExecutor.h +++ b/ReactAndroid/src/main/jni/react/JSCExecutor.h @@ -6,9 +6,10 @@ #include #include #include + #include "Executor.h" #include "JSCHelpers.h" -#include "JSCWebWorker.h" +#include "Value.h" namespace facebook { namespace react { @@ -23,10 +24,26 @@ private: std::string cacheDir_; }; -class JSCExecutor : public JSExecutor, public JSCWebWorkerOwner { +class JSCExecutor; +class WorkerRegistration : public noncopyable { +public: + explicit WorkerRegistration(std::unique_ptr executor, Object jsObj) : + jsObj(std::move(jsObj)), + executor(std::move(executor)) {} + + JSCExecutor* getExecutor() { + return executor.get(); + } + + Object jsObj; +private: + std::unique_ptr executor; +}; + +class JSCExecutor : public JSExecutor { public: /** - * Should be invoked from the JS thread. + * Must be invoked from thread this Executor will run on. */ explicit JSCExecutor(Bridge *bridge, const std::string& cacheDir); ~JSCExecutor() override; @@ -56,25 +73,40 @@ public: virtual void handleMemoryPressureCritical() override; void installNativeHook(const char *name, JSObjectCallAsFunctionCallback callback); - virtual void onMessageReceived(int workerId, const std::string& message) override; - virtual JSGlobalContextRef getContext() override; - virtual std::shared_ptr getMessageQueueThread() override; private: JSGlobalContextRef m_context; - std::unordered_map m_webWorkers; - std::unordered_map m_webWorkerJSObjs; Bridge *m_bridge; + int m_workerId = 0; // if this is a worker executor, this is non-zero + JSCExecutor *m_owner = nullptr; // if this is a worker executor, this is non-null + std::shared_ptr m_isDestroyed = std::shared_ptr(new bool(false)); + std::unordered_map m_ownedWorkers; std::string m_deviceCacheDir; std::shared_ptr m_messageQueueThread; std::unique_ptr m_unbundle; - int addWebWorker(const std::string& script, JSValueRef workerRef); - void postMessageToWebWorker(int worker, JSValueRef message, JSValueRef *exn); + /** + * WebWorker constructor. Must be invoked from thread this Executor will run on. + */ + explicit JSCExecutor( + Bridge *bridge, + int workerId, + JSCExecutor *owner, + const std::string& script); + + void initOnJSVMThread(); + void terminateOnJSVMThread(); void flush(); - void terminateWebWorker(int worker); - void loadModule(uint32_t moduleId); void flushQueueImmediate(std::string queueJSON); + void loadModule(uint32_t moduleId); + + int addWebWorker(const std::string& script, JSValueRef workerRef); + void postMessageToOwnedWebWorker(int worker, JSValueRef message, JSValueRef *exn); + void postMessageToOwner(JSValueRef result); + void receiveMessageFromOwnedWebWorker(int workerId, const std::string& message); + void receiveMessageFromOwner(const std::string &msgString); + void terminateOwnedWebWorker(int worker); + Object createMessageObject(const std::string& msgData); static JSValueRef nativeStartWorker( JSContextRef ctx, @@ -97,6 +129,13 @@ private: size_t argumentCount, const JSValueRef arguments[], JSValueRef *exception); + static JSValueRef nativePostMessage( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception); static JSValueRef nativeRequire( JSContextRef ctx, JSObjectRef function, diff --git a/ReactAndroid/src/main/jni/react/JSCWebWorker.cpp b/ReactAndroid/src/main/jni/react/JSCWebWorker.cpp deleted file mode 100644 index e8e710bdf..000000000 --- a/ReactAndroid/src/main/jni/react/JSCWebWorker.cpp +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2004-present Facebook. All Rights Reserved. - -#include "JSCWebWorker.h" - -#include -#include -#include -#include - -#include -#include - -#include "JSCHelpers.h" -#include "MessageQueueThread.h" -#include "Platform.h" -#include "Value.h" - -#include - -namespace facebook { -namespace react { - -// TODO(9604425): thread safety -static std::unordered_map s_globalContextRefToJSCWebWorker; - -JSCWebWorker::JSCWebWorker(int id, JSCWebWorkerOwner *owner, std::string scriptSrc) : - id_(id), - scriptName_(std::move(scriptSrc)), - owner_(owner) { - ownerMessageQueueThread_ = owner->getMessageQueueThread(); - CHECK(ownerMessageQueueThread_) << "Owner MessageQueue must not be null"; - workerMessageQueueThread_ = WebWorkerUtil::createWebWorkerThread(id, ownerMessageQueueThread_.get()); - CHECK(workerMessageQueueThread_) << "Failed to create worker thread"; - - workerMessageQueueThread_->runOnQueue([this] () { - initJSVMAndLoadScript(); - }); -} - - -JSCWebWorker::~JSCWebWorker() { - CHECK(isTerminated()) << "Didn't terminate the web worker before releasing it!";; -} - -void JSCWebWorker::postMessage(JSValueRef msg) { - std::string msgString = Value(owner_->getContext(), msg).toJSONString(); - - workerMessageQueueThread_->runOnQueue([this, msgString] () { - if (isTerminated()) { - return; - } - - JSValueRef args[] = { createMessageObject(context_, msgString) }; - Value onmessageValue = Object::getGlobalObject(context_).getProperty("onmessage"); - onmessageValue.asObject().callAsFunction(1, args); - }); -} - -void JSCWebWorker::terminate() { - if (isTerminated()) { - return; - } - isTerminated_.store(true, std::memory_order_release); - - if (workerMessageQueueThread_->isOnThread()) { - terminateOnWorkerThread(); - } else { - std::mutex signalMutex; - std::condition_variable signalCv; - bool terminationComplete = false; - - workerMessageQueueThread_->runOnQueue([&] () mutable { - std::lock_guard lock(signalMutex); - - terminateOnWorkerThread(); - terminationComplete = true; - - signalCv.notify_one(); - }); - - std::unique_lock lock(signalMutex); - signalCv.wait(lock, [&terminationComplete] { return terminationComplete; }); - } -} - -void JSCWebWorker::terminateOnWorkerThread() { - s_globalContextRefToJSCWebWorker.erase(context_); - JSGlobalContextRelease(context_); - context_ = nullptr; - workerMessageQueueThread_->quitSynchronous(); -} - -bool JSCWebWorker::isTerminated() { - return isTerminated_.load(std::memory_order_acquire); -} - -void JSCWebWorker::initJSVMAndLoadScript() { - CHECK(!isTerminated()) << "Worker was already finished!"; - CHECK(!context_) << "Worker JS VM was already created!"; - - context_ = JSGlobalContextCreateInGroup( - NULL, // use default JS 'global' object - NULL // create new group (i.e. new VM) - ); - s_globalContextRefToJSCWebWorker[context_] = this; - - // TODO(9604438): Protect against script does not exist - std::string script = WebWorkerUtil::loadScriptFromAssets(scriptName_); - evaluateScript(context_, String(script.c_str()), String(scriptName_.c_str())); - - installGlobalFunction(context_, "postMessage", nativePostMessage); -} - -void JSCWebWorker::postMessageToOwner(JSValueRef msg) { - std::string msgString = Value(context_, msg).toJSONString(); - ownerMessageQueueThread_->runOnQueue([this, msgString] () { - owner_->onMessageReceived(id_, msgString); - }); -} - -JSValueRef JSCWebWorker::nativePostMessage( - JSContextRef ctx, - JSObjectRef function, - JSObjectRef thisObject, - size_t argumentCount, - const JSValueRef arguments[], - JSValueRef *exception) { - if (argumentCount != 1) { - *exception = makeJSCException(ctx, "postMessage got wrong number of arguments"); - return JSValueMakeUndefined(ctx); - } - JSValueRef msg = arguments[0]; - JSCWebWorker *webWorker = s_globalContextRefToJSCWebWorker.at(JSContextGetGlobalContext(ctx)); - - if (webWorker->isTerminated()) { - return JSValueMakeUndefined(ctx); - } - - webWorker->postMessageToOwner(msg); - - return JSValueMakeUndefined(ctx); -} - -/*static*/ -Object JSCWebWorker::createMessageObject(JSContextRef context, const std::string& msgJson) { - Value rebornJSMsg = Value::fromJSON(context, String(msgJson.c_str())); - Object messageObject = Object::create(context); - messageObject.setProperty("data", rebornJSMsg); - return std::move(messageObject); -} - -} -} diff --git a/ReactAndroid/src/main/jni/react/JSCWebWorker.h b/ReactAndroid/src/main/jni/react/JSCWebWorker.h deleted file mode 100644 index 73d983cdf..000000000 --- a/ReactAndroid/src/main/jni/react/JSCWebWorker.h +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2004-present Facebook. All Rights Reserved. - -#include -#include -#include -#include -#include -#include - -#include - -#include "Value.h" - -namespace facebook { -namespace react { - -class MessageQueueThread; - -/** - * A class that can own the lifecycle, receive messages from, and dispatch messages - * to JSCWebWorkers. - */ -class JSCWebWorkerOwner { -public: - /** - * Called when a worker has posted a message with `postMessage`. - */ - virtual void onMessageReceived(int workerId, const std::string& message) = 0; - virtual JSGlobalContextRef getContext() = 0; - - /** - * Should return the owner's MessageQueueThread. Calls to onMessageReceived will be enqueued - * on this thread. - */ - virtual std::shared_ptr getMessageQueueThread() = 0; -}; - -/** - * Implementation of a web worker for JSC. The web worker should be created from the owner's - * (e.g., owning JSCExecutor instance) JS MessageQueueThread. The worker is responsible for - * creating its own MessageQueueThread. - * - * During operation, the JSCExecutor should call postMessage **from its own MessageQueueThread** - * to send messages to the worker. The worker will handle enqueueing those messages on its own - * MessageQueueThread as appropriate. When the worker has a message to post to the owner, it will - * enqueue a call to owner->onMessageReceived on the owner's MessageQueueThread. - */ -class JSCWebWorker { -public: - explicit JSCWebWorker(int id, JSCWebWorkerOwner *owner, std::string script); - ~JSCWebWorker(); - - /** - * Post a message to be received by the worker on its thread. This must be called from - * ownerMessageQueueThread_. - */ - void postMessage(JSValueRef msg); - - /** - * Synchronously quits the current worker and cleans up its VM. - */ - void terminate(); - - /** - * Whether terminate() has been called on this worker. - */ - bool isTerminated(); - - static Object createMessageObject(JSContextRef context, const std::string& msgData); -private: - void initJSVMAndLoadScript(); - void postRunnableToEventLoop(std::function&& runnable); - void postMessageToOwner(JSValueRef result); - void terminateOnWorkerThread(); - - int id_; - std::atomic_bool isTerminated_ = ATOMIC_VAR_INIT(false); - std::string scriptName_; - JSCWebWorkerOwner *owner_ = nullptr; - std::shared_ptr ownerMessageQueueThread_; - std::unique_ptr workerMessageQueueThread_; - JSGlobalContextRef context_ = nullptr; - - static JSValueRef nativePostMessage( - JSContextRef ctx, - JSObjectRef function, - JSObjectRef thisObject, - size_t argumentCount, - const JSValueRef arguments[], - JSValueRef *exception); -}; - -} -} diff --git a/ReactAndroid/src/main/jni/react/MessageQueueThread.h b/ReactAndroid/src/main/jni/react/MessageQueueThread.h index 544b91142..f6352ab0d 100644 --- a/ReactAndroid/src/main/jni/react/MessageQueueThread.h +++ b/ReactAndroid/src/main/jni/react/MessageQueueThread.h @@ -2,7 +2,9 @@ #pragma once +#include #include +#include namespace facebook { namespace react { @@ -14,6 +16,24 @@ class MessageQueueThread { virtual bool isOnThread() = 0; // quitSynchronous() should synchronously ensure that no further tasks will run on the queue. virtual void quitSynchronous() = 0; + + void runOnQueueSync(std::function&& runnable) { + std::mutex signalMutex; + std::condition_variable signalCv; + bool runnableComplete = false; + + runOnQueue([&] () mutable { + std::lock_guard lock(signalMutex); + + runnable(); + runnableComplete = true; + + signalCv.notify_one(); + }); + + std::unique_lock lock(signalMutex); + signalCv.wait(lock, [&runnableComplete] { return runnableComplete; }); + } }; }}