diff --git a/rts/IOManager.c b/rts/IOManager.c
index 4e3f3b06f4f41e3869299150e522e94a6f0b203a..3e6761b219a9657e9a578a5f37518a67fef3003a 100644
--- a/rts/IOManager.c
+++ b/rts/IOManager.c
@@ -458,7 +458,44 @@ setIOManagerControlFd(uint32_t cap_no USED_IF_THREADS, int fd USED_IF_THREADS) {
 }
 #endif
 
-#if !defined(THREADED_RTS)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wsuggest-attribute=noreturn"
+
+void syncIOWaitReady(Capability   *cap USED_IF_NOT_THREADS,
+                     StgTSO       *tso USED_IF_NOT_THREADS,
+                     IOReadOrWrite rw  USED_IF_NOT_THREADS,
+                     HsInt         fd  USED_IF_NOT_THREADS)
+{
+    ASSERT(tso->why_blocked == NotBlocked);
+    switch (iomgr_type) {
+#if defined(IOMGR_ENABLED_SELECT)
+        case IO_MANAGER_SELECT:
+        {
+            StgWord why_blocked = rw == IORead ? BlockedOnRead : BlockedOnWrite;
+            tso->block_info.fd = fd;
+            RELEASE_STORE(&tso->why_blocked, why_blocked);
+            appendToIOBlockedQueue(cap, tso);
+            break;
+        }
+#endif
+#if defined(IOMGR_ENABLED_WIN32_LEGACY)
+        case IO_MANAGER_WIN32_LEGACY:
+        {
+            StgWord why_blocked = rw == IORead ? BlockedOnRead : BlockedOnWrite;
+            tso->block_info.fd = fd;
+            RELEASE_STORE(&tso->why_blocked, why_blocked);
+            appendToIOBlockedQueue(cap, tso);
+            break;
+        }
+#endif
+        default:
+            barf("waitRead# / waitWrite# not available for current I/O manager");
+    }
+}
+#pragma GCC diagnostic pop
+
+
+#if defined(IOMGR_ENABLED_SELECT) || defined(IOMGR_ENABLED_WIN32_LEGACY)
 void appendToIOBlockedQueue(Capability *cap, StgTSO *tso)
 {
     CapIOManager *iomgr = cap->iomgr;
@@ -470,7 +507,9 @@ void appendToIOBlockedQueue(Capability *cap, StgTSO *tso)
     }
     iomgr->blocked_queue_tl = tso;
 }
+#endif
 
+#if defined(IOMGR_ENABLED_SELECT)
 void insertIntoSleepingQueue(Capability *cap, StgTSO *tso, LowResTime target)
 {
     CapIOManager *iomgr = cap->iomgr;
diff --git a/rts/IOManager.h b/rts/IOManager.h
index fb25d1f7e40a2572ece294f9b88d735cee6f8f5f..df9d1b1d6f827d6480884f0afd1bc129c8854efd 100644
--- a/rts/IOManager.h
+++ b/rts/IOManager.h
@@ -272,6 +272,13 @@ void wakeupIOManager(void);
 void markCapabilityIOManager(evac_fn evac, void *user, CapIOManager *iomgr);
 
 
+/* Several code paths are almost identical between read and write paths. In
+ * such cases we use a shared code path with an enum to say which we're doing.
+ */
+typedef enum { IORead, IOWrite } IOReadOrWrite;
+
+void syncIOWaitReady(Capability *cap, StgTSO *tso, IOReadOrWrite rw, HsInt fd);
+
 #if !defined(THREADED_RTS)
 /* Add a thread to the end of the queue of threads blocked on I/O.
  *
diff --git a/rts/PrimOps.cmm b/rts/PrimOps.cmm
index 9f9bfdaf7a1302a88e705219baec1086d7322047..46c6c926b1e4d6c514024e6d7b31eac6df47cea0 100644
--- a/rts/PrimOps.cmm
+++ b/rts/PrimOps.cmm
@@ -2548,34 +2548,16 @@ stg_whereFromzh (P_ clos, W_ buf)
 
 stg_waitReadzh ( W_ fd )
 {
-#if defined(THREADED_RTS)
-    ccall barf("waitRead# on threaded RTS") never returns;
-#else
-
-    ASSERT(StgTSO_why_blocked(CurrentTSO) == NotBlocked::I32);
-    StgTSO_block_info(CurrentTSO) = fd;
-    %release StgTSO_why_blocked(CurrentTSO) = BlockedOnRead::I32;
-    // No locking - we're not going to use this interface in the
-    // threaded RTS anyway.
-    ccall appendToIOBlockedQueue(MyCapability() "ptr", CurrentTSO "ptr");
+    ccall syncIOWaitReady(MyCapability() "ptr", CurrentTSO "ptr",
+                          /* IORead */ 0::I32, fd);
     jump stg_block_noregs();
-#endif
 }
 
 stg_waitWritezh ( W_ fd )
 {
-#if defined(THREADED_RTS)
-    ccall barf("waitWrite# on threaded RTS") never returns;
-#else
-
-    ASSERT(StgTSO_why_blocked(CurrentTSO) == NotBlocked::I32);
-    StgTSO_block_info(CurrentTSO) = fd;
-    %release StgTSO_why_blocked(CurrentTSO) = BlockedOnWrite::I32;
-    // No locking - we're not going to use this interface in the
-    // threaded RTS anyway.
-    ccall appendToIOBlockedQueue(MyCapability() "ptr", CurrentTSO "ptr");
+    ccall syncIOWaitReady(MyCapability() "ptr", CurrentTSO "ptr",
+                          /* IOWrite */ 1::I32, fd);
     jump stg_block_noregs();
-#endif
 }
 
 stg_delayzh ( W_ us_delay )