diff --git a/Doxyfile b/Doxyfile
index 0389113ce2aae6fae062e626e2f6a5cb96b53b84..e3cff65eeb429bbabdd634f6daa68d8f44bc240d 100644
--- a/Doxyfile
+++ b/Doxyfile
@@ -1493,7 +1493,7 @@ INCLUDE_FILE_PATTERNS  =
 # undefined via #undef or recursively expanded use the := operator
 # instead of the = operator.
 
-PREDEFINED             =
+PREDEFINED             = __cplusplus
 
 # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then
 # this tag can be used to specify a list of macro names that should be expanded.
diff --git a/include/osv/condvar.h b/include/osv/condvar.h
index be4a629d1ac749a1381b2c92ddbaa2c08ee2ccb6..03a3dfb33eb572a65447a68944477f0a5c043998 100644
--- a/include/osv/condvar.h
+++ b/include/osv/condvar.h
@@ -5,7 +5,7 @@
  * BSD license as described in the LICENSE file in the top-level directory.
  */
 
-// condvar is OSv's implemenation of the classic "condition variable"
+// condvar is OSv's implementation of the classic "condition variable"
 // synchronization primitive. It is similar to condition variables in POSIX
 // Threads, but makes one additional guarantees that POSIX Threads do not:
 // Our condvar_wait() is guaranteed not to have "spurious wakeups", i.e.,
@@ -58,6 +58,27 @@
 
 struct wait_record;
 
+/**
+ * Condition Variable
+ *
+ * condvar implements the classic "condition variable" synchronization
+ * primitive, which together with a mutex is used to allow threads to wait
+ * until a certain condition occurs.
+ *
+ * condvar is similar to condition variables in POSIX Threads, but makes one
+ * additional guarantees that POSIX Threads do not: Our wait() is
+ * guaranteed not to have "spurious wakeups", i.e., condvar_wait() will only
+ * return if someone called condvar_wake_one()/all().
+ *
+ * This difference is subtle. Usually you still need to check the condition
+ * after condvar_wait() returns because other threads may race us to change
+ * the condition. But in some cases we use this guarantee. One example is our
+ * semaphore implementation (semaphore.cc): In the traditional implementation
+ * when spurious wait() wakeups are possible, the wakee decrements the
+ * semaphore's counter() this can cause a "thundering herd" problem). Using
+ * our condvar without spurious wakeups, it is possible to decrement the
+ * counter in the waker, and decide exactly who to wake.
+ */
 typedef struct condvar {
     mutex_t m;
     struct {
@@ -79,9 +100,60 @@ typedef struct condvar {
 #ifdef __cplusplus
     // In C++, for convenience also provide methods.
     condvar() { memset(this, 0, sizeof *this); }
+    /**
+     * Wait on the condition variable, or timer to expire
+     *
+     * Wait to be woken (with wake_one() or wake_all()), or the given timer
+     * to expire, whichever occurs first. If the timer is nullptr, or omitted,
+     * wait indefinitely until woken.
+     *
+     * It is assumed that wait() is called with the given mutex locked.
+     * This mutex is unlocked during the wait, and re-locked before wait()
+     * returns.
+     *
+     * The current implementation assumes (as do Posix Threads) that when
+     * multiple threads wait on the same condition variable concurrently,
+     * they all do it with the same mutex. If two threads concurrently use
+     * two different mutexes to wait on the same condition variable, the
+     * results are undefined (currently, it causes an assertion failure).
+     *
+     * \return 0 if woken by a wake_one() or wake_all(), or ETIMEOUT (!=0)
+     * if was not woken, but the timer expired. Note if a wakeup and the
+     * timeout race, by the time wait() returns the timer may have already
+     * expired even if 0 is returned. What a return of 0 means is not that
+     * a timeout hasn't yet occurred, but rather that one wake_one()/wake_all()
+     * was consumed to wake us.
+     */
     inline int wait(mutex_t *user_mutex, sched::timer *tmr = nullptr);
+    /**
+     * \overload
+     */
     inline int wait(mutex_t &user_mutex, sched::timer *tmr = nullptr);
+    /**
+     * Wake one thread waiting on the condition variable
+     *
+     * Wake one of the threads currently waiting on the condition variable,
+     * or do nothing if there is no thread waiting.
+     *
+     * wake_one() may be called with the mutex either locked, or unlocked.
+     * If the mutex is locked, the target thread will not be actually woken
+     * until the waking thread unlocks it and the target thread can have it.
+     * This optimization is known as "wait morphing".
+     */
     inline void wake_one();
+    /**
+     * Wake all threads waiting on the condition variable
+     *
+     * Wake all of the threads currently waiting on the condition variable,
+     * or do nothing if there is no thread waiting.
+     *
+     * If more than one thread is waiting, they will not all be woken
+     * concurrently as all will need the same mutex and most will need to
+     * go back to sleep (this is known as the "thundering herd problem").
+     * Rather, only one thread is woken, and the rest are moved to the
+     * waiting list of the mutex, to be woken up one by one as the mutex
+     * becomes available. This optimization is known as "wait morphing".
+     */
     inline void wake_all();
     template <class Pred>
     void wait_until(mutex& mtx, Pred pred);