diff --git a/py/modthread.c b/py/modthread.c
index 930ca45bbb7261f18ca0cbdd62534c2613711c80..7efad78c9a3951faf08da4ddecf729997224cd48 100644
--- a/py/modthread.c
+++ b/py/modthread.c
@@ -146,6 +146,8 @@ typedef struct _thread_entry_args_t {
 } thread_entry_args_t;
 
 STATIC void *thread_entry(void *args_in) {
+    // Execution begins here for a new thread.  We do not have the GIL.
+
     thread_entry_args_t *args = (thread_entry_args_t*)args_in;
 
     mp_state_thread_t ts;
@@ -154,6 +156,8 @@ STATIC void *thread_entry(void *args_in) {
     mp_stack_set_top(&ts + 1); // need to include ts in root-pointer scan
     mp_stack_set_limit(16 * 1024); // fixed stack limit for now
 
+    MP_THREAD_GIL_ENTER();
+
     // signal that we are set up and running
     mp_thread_start();
 
@@ -188,6 +192,8 @@ STATIC void *thread_entry(void *args_in) {
     // signal that we are finished
     mp_thread_finish();
 
+    MP_THREAD_GIL_EXIT();
+
     return NULL;
 }
 
diff --git a/py/mpconfig.h b/py/mpconfig.h
index 998d1b6924cfcf95b8934d751714b38b50798599..b79ddac1961abde13abbceb37bfecc9f4f757d47 100644
--- a/py/mpconfig.h
+++ b/py/mpconfig.h
@@ -829,6 +829,12 @@ typedef double mp_float_t;
 #define MICROPY_PY_THREAD (0)
 #endif
 
+// Whether to make the VM/runtime thread-safe using a global lock
+// If not enabled then thread safety must be provided at the Python level
+#ifndef MICROPY_PY_THREAD_GIL
+#define MICROPY_PY_THREAD_GIL (MICROPY_PY_THREAD)
+#endif
+
 // Extended modules
 
 #ifndef MICROPY_PY_UCTYPES
diff --git a/py/mpstate.h b/py/mpstate.h
index 3ee243dffa78187749e93d3a5c82555d88fae55c..b79fd7c5c73b2054e17b788264c4400d9ebad26b 100644
--- a/py/mpstate.h
+++ b/py/mpstate.h
@@ -175,6 +175,11 @@ typedef struct _mp_state_vm_t {
     #if MICROPY_ENABLE_EMERGENCY_EXCEPTION_BUF && MICROPY_EMERGENCY_EXCEPTION_BUF_SIZE == 0
     mp_int_t mp_emergency_exception_buf_size;
     #endif
+
+    #if MICROPY_PY_THREAD_GIL
+    // This is a global mutex used to make the VM/runtime thread-safe.
+    mp_thread_mutex_t gil_mutex;
+    #endif
 } mp_state_vm_t;
 
 // This structure holds state that is specific to a given thread.
diff --git a/py/mpthread.h b/py/mpthread.h
index d0164ea29e85e385d480d2bc475d43761295e8df..747de60fefbf2bd54ccce01220378aeae62870a5 100644
--- a/py/mpthread.h
+++ b/py/mpthread.h
@@ -38,6 +38,14 @@
 
 struct _mp_state_thread_t;
 
+#if MICROPY_PY_THREAD_GIL
+#define MP_THREAD_GIL_ENTER() mp_thread_mutex_lock(&MP_STATE_VM(gil_mutex), 1)
+#define MP_THREAD_GIL_EXIT() mp_thread_mutex_unlock(&MP_STATE_VM(gil_mutex))
+#else
+#define MP_THREAD_GIL_ENTER()
+#define MP_THREAD_GIL_EXIT()
+#endif
+
 struct _mp_state_thread_t *mp_thread_get_state(void);
 void mp_thread_set_state(void *state);
 void mp_thread_create(void *(*entry)(void*), void *arg, size_t stack_size);
diff --git a/py/runtime.c b/py/runtime.c
index 7f28abbf4f9436a6d4c66200cc53d6f80bbad2df..f88c92be63c539866ed7197ee3f0ccf0064b8502 100644
--- a/py/runtime.c
+++ b/py/runtime.c
@@ -91,6 +91,12 @@ void mp_init(void) {
     // start with no extensions to builtins
     MP_STATE_VM(mp_module_builtins_override_dict) = NULL;
     #endif
+
+    #if MICROPY_PY_THREAD_GIL
+    mp_thread_mutex_init(&MP_STATE_VM(gil_mutex));
+    #endif
+
+    MP_THREAD_GIL_ENTER();
 }
 
 void mp_deinit(void) {
diff --git a/py/vm.c b/py/vm.c
index bd5bae115e6abb5936b6d49ac0ad69dc7c362b45..65801401d144ac64f5caaae6bfbe67d641f2305c 100644
--- a/py/vm.c
+++ b/py/vm.c
@@ -1263,6 +1263,10 @@ pending_exception_check:
                     RAISE(obj);
                 }
 
+                // TODO make GIL release more efficient
+                MP_THREAD_GIL_EXIT();
+                MP_THREAD_GIL_ENTER();
+
             } // for loop
 
         } else {