diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt
index 98e1c00ce30dae4013da2cba8b4d7d51cfc1ea2e..55c44446b476efacbbe7410f1ac4d3e696ac0d07 100644
--- a/indra/llcommon/CMakeLists.txt
+++ b/indra/llcommon/CMakeLists.txt
@@ -189,6 +189,7 @@ set(llcommon_HEADER_FILES
     lllistenerwrapper.h
     llliveappconfig.h
     lllivefile.h
+    llmainthreadtask.h
     llmd5.h
     llmemory.h
     llmemorystream.h
diff --git a/indra/llcommon/llmainthreadtask.cpp b/indra/llcommon/llmainthreadtask.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e0d70cacd86600cbd964f70f1065bdfd349dd4bf
--- /dev/null
+++ b/indra/llcommon/llmainthreadtask.cpp
@@ -0,0 +1,22 @@
+/**
+ * @file   llmainthreadtask.cpp
+ * @author Nat Goodspeed
+ * @date   2019-12-05
+ * @brief  Implementation for llmainthreadtask.
+ * 
+ * $LicenseInfo:firstyear=2019&license=viewerlgpl$
+ * Copyright (c) 2019, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "llmainthreadtask.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+
+// This file is required by our CMake integration-test machinery. It
+// contributes no code to the viewer executable.
diff --git a/indra/llcommon/llmainthreadtask.h b/indra/llcommon/llmainthreadtask.h
new file mode 100644
index 0000000000000000000000000000000000000000..526374981ace2850799a7deb572c6e14e10485e5
--- /dev/null
+++ b/indra/llcommon/llmainthreadtask.h
@@ -0,0 +1,102 @@
+/**
+ * @file   llmainthreadtask.h
+ * @author Nat Goodspeed
+ * @date   2019-12-04
+ * @brief  LLMainThreadTask dispatches work to the main thread. When invoked on
+ *         the main thread, it performs the work inline.
+ * 
+ * $LicenseInfo:firstyear=2019&license=viewerlgpl$
+ * Copyright (c) 2019, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LLMAINTHREADTASK_H)
+#define LL_LLMAINTHREADTASK_H
+
+#include "lleventtimer.h"
+#include "llthread.h"
+#include "lockstatic.h"
+#include "llmake.h"
+#include <future>
+#include <type_traits>              // std::result_of
+#include <boost/signals2/dummy_mutex.hpp>
+
+class LLMainThreadTask
+{
+private:
+    // Don't instantiate this class -- use dispatch() instead.
+    LLMainThreadTask() {}
+    // If our caller doesn't explicitly pass a LockStatic<something>, make a
+    // fake one.
+    struct Static
+    {
+        boost::signals2::dummy_mutex mMutex;
+    };
+    typedef llthread::LockStatic<Static> LockStatic;
+
+public:
+    /// dispatch() is the only way to invoke this functionality.
+    /// If you call it with a LockStatic<something>, dispatch() unlocks it
+    /// before blocking for the result.
+    template <typename Static, typename CALLABLE>
+    static auto dispatch(llthread::LockStatic<Static>& lk, CALLABLE&& callable)
+        -> decltype(callable())
+    {
+        if (on_main_thread())
+        {
+            // we're already running on the main thread, perfect
+            return callable();
+        }
+        else
+        {
+            // It's essential to construct LLEventTimer subclass instances on
+            // the heap because, on completion, LLEventTimer deletes them.
+            // Once we enable C++17, we can use Class Template Argument
+            // Deduction. Until then, use llmake_heap().
+            auto* task = llmake_heap<Task>(std::forward<CALLABLE>(callable));
+            // The moment we construct a new LLEventTimer subclass object, its
+            // tick() method might get called. However, its tick() method
+            // might depend on something locked by the passed LockStatic.
+            // Unlock it so tick() can proceed.
+            lk.unlock();
+            auto future = task->mTask.get_future();
+            // Now simply block on the future.
+            return future.get();
+        }
+    }
+
+    /// You can call dispatch() without a LockStatic<something>.
+    template <typename CALLABLE>
+    static auto dispatch(CALLABLE&& callable) -> decltype(callable())
+    {
+        LockStatic lk;
+        return dispatch(lk, std::forward<CALLABLE>(callable));
+    }
+
+private:
+    template <typename CALLABLE>
+    struct Task: public LLEventTimer
+    {
+        Task(CALLABLE&& callable):
+            // no wait time: call tick() next chance we get
+            LLEventTimer(0),
+            mTask(std::forward<CALLABLE>(callable))
+        {}
+        BOOL tick() override
+        {
+            // run the task on the main thread, will populate the future
+            // obtained by get_future()
+            mTask();
+            // tell LLEventTimer we're done (one shot)
+            return TRUE;
+        }
+        // Given arbitrary CALLABLE, which might be a lambda, how are we
+        // supposed to obtain its signature for std::packaged_task? It seems
+        // redundant to have to add an argument list to engage result_of, then
+        // add the argument list again to complete the signature. At least we
+        // only support a nullary CALLABLE.
+        std::packaged_task<typename std::result_of<CALLABLE()>::type()> mTask;
+    };
+};
+
+#endif /* ! defined(LL_LLMAINTHREADTASK_H) */