diff --git a/adaptive/runner.py b/adaptive/runner.py
index f3b84b3467e409a18f6784b16936a0a3c2d9e66a..0660e884609fb9f83ae7b1d4bccc2a54d086130e 100644
--- a/adaptive/runner.py
+++ b/adaptive/runner.py
@@ -8,6 +8,7 @@ import os
 import time
 import traceback
 import warnings
+import abc
 
 from .notebook_integration import live_plot, live_info, in_ipynb
 from .utils import timed
@@ -53,7 +54,7 @@ else:
     _default_executor_kwargs = {}
 
 
-class BaseRunner:
+class BaseRunner(metaclass=abc.ABCMeta):
     """Base class for runners that use `concurrent.futures.Executors`.
 
     Parameters
@@ -241,6 +242,20 @@ class BaseRunner:
     def failed(self):
         """Set of points that failed ``runner.retries`` times."""
         return set(self.tracebacks) - set(self.to_retry)
+    
+    @abc.abstractmethod
+    def elapsed_time(self):
+        """Return the total time elapsed since the runner
+        was started.
+        
+        Is called in `overhead`.
+        """
+        pass
+    
+    @abc.abstractmethod
+    def _submit(self, x):
+        """Is called in `_get_futures`."""
+        pass
 
 
 class BlockingRunner(BaseRunner):
@@ -444,11 +459,6 @@ class AsyncRunner(BaseRunner):
                 raise RuntimeError('Cannot use an executor when learning an '
                                    'async function.')
             self.executor.shutdown()  # Make sure we don't shoot ourselves later
-            self._submit = lambda x: self.ioloop.create_task(self.function(x))
-        else:
-            self._submit = functools.partial(self.ioloop.run_in_executor,
-                                             self.executor,
-                                             self.function)
 
         self.task = self.ioloop.create_task(self._run())
         self.saving_task = None
@@ -458,6 +468,13 @@ class AsyncRunner(BaseRunner):
                           "in a Jupyter notebook, remember to run "
                           "'adaptive.notebook_extension()'")
 
+    def _submit(self, x):
+        ioloop = self.ioloop
+        if inspect.iscoroutinefunction(self.learner.function):
+            return ioloop.create_task(self.function(x))
+        else:
+            return ioloop.run_in_executor(self.executor, self.function, x)
+
     def status(self):
         """Return the runner status as a string.