diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b9c7200f491911d63973488067bb545a238daebe..3a03bf281f4856f6ae217f6a4bb9c86841a4159a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,6 +1,8 @@
 image: "flowdalic/debian-dev:1.13"
 
 before_script:
+  - apt update && apt install -y pkg-config
+  - apt install -y -t testing liburcu-dev
   - |
     readarray TOOLS <<EOF
     c++
diff --git a/emper/Emper.hpp b/emper/Emper.hpp
index 76fa35e31ccb4ed0defe0e1511cba81f1b05e5d1..d5c3dc57b40a468a3361fb40c1e6f0fdcc48395a 100644
--- a/emper/Emper.hpp
+++ b/emper/Emper.hpp
@@ -22,4 +22,12 @@ static const bool WORKER_SLEEP =
 #endif
 		;
 
+static const bool LIBURCU =
+#ifdef EMPER_LIBURCU
+		true
+#else
+		false
+#endif
+		;
+
 }	 // namespace emper
diff --git a/emper/Runtime.cpp b/emper/Runtime.cpp
index 041bcc4bc7c52b6f634a0ed39ae513d119aa6889..2cc9e480b524f777e94f152fdda963cef656b601 100644
--- a/emper/Runtime.cpp
+++ b/emper/Runtime.cpp
@@ -8,6 +8,7 @@
 // Non portable.
 #include <sched.h>				// for cpu_set_t, CPU_SET, CPU_ZERO
 #include <sys/sysinfo.h>	// for get_nprocs
+#include <urcu.h>					// for rcu_register_thread
 
 #include <cstdlib>	// for rand, srand, abort
 #include <cstring>
@@ -78,6 +79,10 @@ Runtime::Runtime(workerid_t workerCount, RuntimeStrategy& strategy, unsigned int
 		workerIds[i] = i;
 
 		auto thread_function = [](void* voidWorkerId) -> void* {
+			if constexpr (emper::LIBURCU) {
+				rcu_register_thread();
+			}
+
 			return currentRuntime->workerLoop(voidWorkerId);
 		};
 		errno = pthread_create(&threads[i], &attr, thread_function, &workerIds[i]);
diff --git a/emper/meson.build b/emper/meson.build
index eeee83e0fcdc45d4072b09519c49684b85ddba25..f73891a7543d0266bdeea336542a7a009957264d 100644
--- a/emper/meson.build
+++ b/emper/meson.build
@@ -45,7 +45,7 @@ emper = library(
   [emper_cpp_sources, emper_generated_files],
   emper_asm_objects,
   include_directories: emper_all_include,
-  dependencies: thread_dep,
+  dependencies: emper_dependencies,
   install: true,
 )
 
diff --git a/iwyu-mappings.imp b/iwyu-mappings.imp
index 8a64523b20e91b6a76971ec7398b9dbbcd5bfa9f..3b0d99bef9cad915b5f885595e236530608d3c27 100644
--- a/iwyu-mappings.imp
+++ b/iwyu-mappings.imp
@@ -1,4 +1,5 @@
 [
 	{ include: ["<bits/getopt_core.h>", "private", "<unistd.h>", "public"] },
 	{ include: ["@<gtest/.*>", "private", "<gtest/gtest.h>", "public"] },
+	{ include: ["<urcu/map/urcu-memb.h>", "private", "<urcu.h>", "public"] },
 ]
diff --git a/meson.build b/meson.build
index 325105c849c06f727a5fa8c03dfff3561e7ee435..6c2c21e593783963abaa0561fc907a93b5a9502d 100644
--- a/meson.build
+++ b/meson.build
@@ -12,12 +12,14 @@ project('EMPER', 'c', 'cpp',
 add_project_arguments('-Wno-non-virtual-dtor', language: 'cpp')
 
 thread_dep = dependency('threads')
-emper_dependencies = [thread_dep]
+liburcu_dep = dependency('liburcu')
+emper_dependencies = [thread_dep, liburcu_dep]
 
 run_target('iwyu',
 		   command: 'tools/check-iwyu')
 
 conf_data = configuration_data()
+conf_data.set('EMPER_LIBURCU', get_option('userspace-rcu'))
 conf_data.set('EMPER_WORKER_SLEEP', get_option('worker_sleep'))
 conf_data.set('EMPER_LOCKED_WS_QUEUE', get_option('locked_ws_queue'))
 conf_data.set('EMPER_OVERFLOW_QUEUE', get_option('overflow_queue'))
diff --git a/meson_options.txt b/meson_options.txt
index 98a9ca1502ba1b7aa5fe3a03bd7d85a587771147..8032e30c80192ddd7463a3d70e288570472e3bbd 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,3 +1,9 @@
+option(
+  'userspace-rcu',
+  type: 'boolean',
+  value: true,
+  description: 'Allow EMPER fibers to use userspace RCU',
+)
 option(
   'worker_sleep',
   type: 'boolean',
diff --git a/tests/SimpleURCUTest.cpp b/tests/SimpleURCUTest.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..dfd9f813b93e3060b37c8e3d66ce4a802dbca721
--- /dev/null
+++ b/tests/SimpleURCUTest.cpp
@@ -0,0 +1,93 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// Copyright © 2020 Florian Fischer
+#include <urcu.h>						 // for rcu_read_lock, rcu_read_unlock
+#include <urcu/rculfhash.h>	 // for RCU lock-free hash table
+
+#include <algorithm>	 // for find
+#include <cstdlib>		 // for exit, EXIT_FAILURE, EXIT_SUC...
+#include <functional>	 // for hash
+#include <iostream>		 // for hash
+#include <vector>			 // for vector
+
+#include "Common.hpp"										 // for die
+#include "CountingPrivateSemaphore.hpp"	 // for CPS
+#include "Fiber.hpp"										 // for Fiber
+#include "Runtime.hpp"									 // for Runtime
+#include "emper.hpp"										 // for spawn
+
+struct node {
+	int value;
+	struct cds_lfht_node node; /* Chaining in hash table */
+};
+
+using node_t = struct node;
+
+auto main() -> int {
+	Runtime runtime;
+	struct cds_lfht* ht;
+	std::vector<int> values = {
+			-5, 42, 42, 36, 24,
+	}; /* 42 is duplicated */
+
+	// Allocate new hash table.
+	ht = cds_lfht_new(1, 1, 0, CDS_LFHT_AUTO_RESIZE | CDS_LFHT_ACCOUNTING, nullptr);
+	if (!ht) {
+		die("Error allocating hash table", false);
+	}
+
+	Fiber* verifier = Fiber::from([&]() {
+		CPS cps;
+		for (auto& value : values) {
+			// add each value to the hash table
+			spawn(
+					[&ht, value] {
+						auto* node = reinterpret_cast<node_t*>(malloc(sizeof(node_t)));
+						if (!node) {
+							die("allocating node failed", true);
+						}
+
+						cds_lfht_node_init(&node->node);
+						node->value = value;
+						size_t hash = std::hash<int>{}(value);
+
+						rcu_read_lock();
+						cds_lfht_add(ht, hash, &node->node);
+						rcu_read_unlock();
+					},
+					cps);
+		}
+
+		// Wait for the adders to finish
+		cps.wait();
+
+		// Verify the content of the hash table.
+		// Iterate over each hash table node.
+		// Iteration needs to be performed within RCU read-side critical section.
+		struct cds_lfht_iter iter;
+		node_t* node;
+		rcu_read_lock();
+		size_t i = 0;
+		cds_lfht_for_each_entry(ht, &iter, node, node) {
+			++i;
+			auto it = std::find(values.begin(), values.end(), node->value);
+			if (it == values.end()) {
+				std::cerr << "value: " << node->value << " not found in cds_lfht" << std::endl;
+				exit(EXIT_FAILURE);
+			}
+		}
+		rcu_read_unlock();
+
+		if (i != values.size()) {
+			std::cerr << "number of values in cds_lfht: " << i
+								<< " differ number of added ones: " << values.size() << std::endl;
+			exit(EXIT_FAILURE);
+		}
+
+		exit(EXIT_SUCCESS);
+	});
+
+	runtime.schedule(*verifier);
+	runtime.waitUntilFinished();
+
+	return EXIT_FAILURE;
+}
diff --git a/tests/meson.build b/tests/meson.build
index 2ac875c0ee7d3dc4c9f4b46f8437b9a815ec26b3..e1c2f709cb7c25948cb176c905825b400a8ca4d2 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -1,3 +1,7 @@
+cc = meson.get_compiler('c')
+liburcu_memb = cc.find_library('urcu-memb')
+liburcu_cds = cc.find_library('urcu-cds')
+
 tests = {
 		  'SimpleFibTest.cpp':
 		  {
@@ -29,10 +33,14 @@ tests = {
 		  {
 			'description': 'Simple LAWS scheduling strategy test',
 		  },
+		  'SimpleURCUTest.cpp':
+		  {
+			'description': 'Simple userspace-rcu hash table test',
+			'dependencies': [liburcu_memb, liburcu_cds]
+		  },
 		}
 
 undef_ndebug = '-UNDEBUG'
-test_dep = [thread_dep]
 test_env = environment(
   {
 	# Set glibc's MALLOC_PERTURB to 1. This means that newly allocated
@@ -50,12 +58,17 @@ foreach source, test_dict : tests
   # The test_name is the name of the source file without the file suffix.
   test_name = source.split('.')[0]
 
+  test_dep = [thread_dep]
+  if test_dict.has_key('dependencies')
+  	test_dep += test_dict['dependencies']
+  endif
+
   test_exe = executable(test_name,
 						source,
 						include_directories: emper_all_include,
 						c_args: undef_ndebug,
 						cpp_args: undef_ndebug,
-						dependencies: test_dep,
+						dependencies: emper_dependencies + test_dep,
 						link_with: [emper, emper_c],
 					   )