diff --git a/utils/v4l2-compliance/v4l2-test-buffers.cpp b/utils/v4l2-compliance/v4l2-test-buffers.cpp
index d1224438..d37ee4de 100644
--- a/utils/v4l2-compliance/v4l2-test-buffers.cpp
+++ b/utils/v4l2-compliance/v4l2-test-buffers.cpp
@@ -32,8 +32,11 @@
 #include <ctype.h>
 #include <errno.h>
 #include <poll.h>
+#include <pthread.h>
+#include <signal.h>
 #include <sys/ioctl.h>
 #include <netinet/in.h>
+#include <atomic>
 #include <map>
 #include <vector>
 #include "v4l2-compliance.h"
@@ -2306,65 +2309,159 @@ int testRequests(struct node *node, bool test_streaming)
 	return 0;
 }
 
-static int testBlockingDQBuf(struct node *node, cv4l_queue &q)
+
+/*
+ * This class wraps a pthread in such a way that it simplifies passing
+ * parameters, checking completion, gracious halting, and aggressive
+ * termination (an empty signal handler, as installed by testBlockingDQBuf,
+ * is necessary). This alleviates the need for spaghetti error paths when
+ * multiple potentially blocking threads are involved.
+ */
+class BlockingThread
 {
-	int pid_dqbuf;
-	int pid_streamoff;
-	int pid;
+public:
+	BlockingThread() : done(false), running(false) {}
 
-	fail_on_test(q.reqbufs(node, 2));
-	fail_on_test(node->streamon(q.g_type()));
+	virtual ~BlockingThread()
+	{
+		stop();
+	}
 
-	/*
-	 * This test checks if a blocking wait in VIDIOC_DQBUF doesn't block
-	 * other ioctls.
-	 */
-	fflush(stdout);
-	pid_dqbuf = fork();
-	fail_on_test(pid_dqbuf == -1);
+	int start()
+	{
+		int ret = pthread_create(&thread, NULL, startRoutine, this);
+		if (ret < 0)
+			return ret;
+
+		running = true;
+		return 0;
+	}
+
+	void stop()
+	{
+		if (!running)
+			return;
+
+		/*
+		 * If the thread is blocked on an ioctl, try to wake it up with
+		 * a signal.
+		 */
+		if (!done) {
+			pthread_kill(thread, SIGUSR1);
+			usleep(100000);
+		}
 
-	if (pid_dqbuf == 0) { // Child
 		/*
-		 * In the child process we call VIDIOC_DQBUF and wait
-		 * indefinitely since no buffers are queued.
+		 * If the signal failed to interrupt the ioctl, use the heavy
+		 * artillery and cancel the thread.
 		 */
-		cv4l_buffer buf(q.g_type(), V4L2_MEMORY_MMAP);
+		if (!done) {
+			pthread_cancel(thread);
+			usleep(100000);
+		}
+
+		pthread_join(thread, NULL);
+		running = false;
+	}
+
+	std::atomic<bool> done;
+
+private:
+	static void *startRoutine(void *arg)
+	{
+		BlockingThread *self = static_cast<BlockingThread *>(arg);
+
+		pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
+		pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
+
+		self->run();
+
+		self->done = true;
+		return NULL;
+	}
+
+	virtual void run() = 0;
 
+	pthread_t thread;
+	std::atomic<bool> running;
+};
+
+class DqbufThread : public BlockingThread
+{
+public:
+	DqbufThread(cv4l_queue *q, struct node *n) : queue(q), node(n) {}
+
+private:
+	void run() override
+	{
+		/*
+		 * In this thread we call VIDIOC_DQBUF and wait indefinitely
+		 * since no buffers are queued.
+		 */
+		cv4l_buffer buf(queue->g_type(), V4L2_MEMORY_MMAP);
 		node->dqbuf(buf);
-		std::exit(EXIT_SUCCESS);
 	}
 
-	/* Wait for the child process to start and block */
-	usleep(100000);
-	pid = waitpid(pid_dqbuf, NULL, WNOHANG);
-	/* Check that it is really blocking */
-	fail_on_test(pid);
+	cv4l_queue *queue;
+	struct node *node;
+};
 
-	fflush(stdout);
-	pid_streamoff = fork();
-	fail_on_test(pid_streamoff == -1);
+class StreamoffThread : public BlockingThread
+{
+public:
+	StreamoffThread(cv4l_queue *q, struct node *n) : queue(q), node(n) {}
 
-	if (pid_streamoff == 0) { // Child
+private:
+	void run() override
+	{
 		/*
-		 * In the second child call STREAMOFF: this shouldn't
+		 * In this thread call STREAMOFF; this shouldn't
 		 * be blocked by the DQBUF!
 		 */
-		node->streamoff(q.g_type());
-		std::exit(EXIT_SUCCESS);
+		node->streamoff(queue->g_type());
 	}
 
-	int wstatus_streamoff = 0;
+	cv4l_queue *queue;
+	struct node *node;
+};
+
+static void pthread_sighandler(int sig)
+{
+}
+
+static int testBlockingDQBuf(struct node *node, cv4l_queue &q)
+{
+	DqbufThread thread_dqbuf(&q, node);
+	StreamoffThread thread_streamoff(&q, node);
+
+	/*
+	 * SIGUSR1 is ignored by default, so install an empty signal handler
+	 * so that we can use SIGUSR1 to wake up threads potentially blocked
+	 * on ioctls.
+	 */
+	signal(SIGUSR1, pthread_sighandler);
+
+	fail_on_test(q.reqbufs(node, 2));
+	fail_on_test(node->streamon(q.g_type()));
+
+	/*
+	 * This test checks if a blocking wait in VIDIOC_DQBUF doesn't block
+	 * other ioctls.
+	 */
+	fflush(stdout);
+	thread_dqbuf.start();
+
+	/* Wait for the child thread to start and block */
+	usleep(100000);
+	/* Check that it is really blocking */
+	fail_on_test(thread_dqbuf.done);
+
+	fflush(stdout);
+	thread_streamoff.start();
 
 	/* Wait for the second child to start and exit */
 	usleep(250000);
-	pid = waitpid(pid_streamoff, &wstatus_streamoff, WNOHANG);
-	kill(pid_dqbuf, SIGKILL);
-	fail_on_test(pid != pid_streamoff);
-	/* Test that the second child exited properly */
-	if (!pid || !WIFEXITED(wstatus_streamoff)) {
-		kill(pid_streamoff, SIGKILL);
-		fail_on_test(!pid || !WIFEXITED(wstatus_streamoff));
-	}
+	fail_on_test(!thread_streamoff.done);
 
 	fail_on_test(node->streamoff(q.g_type()));
 	fail_on_test(q.reqbufs(node, 0));
