[pypy-commit] pypy fast-gil: Move the GIL logic to a new thread_gil.c. Use simple names for the

arigo noreply at buildbot.pypy.org
Tue Jun 24 19:09:36 CEST 2014


Author: Armin Rigo <arigo at tunes.org>
Branch: fast-gil
Changeset: r72198:50a199f1b79d
Date: 2014-06-24 19:09 +0200
http://bitbucket.org/pypy/pypy/changeset/50a199f1b79d/

Log:	Move the GIL logic to a new thread_gil.c. Use simple names for the
	basic operations and define these in thread_pthread and thread_nt as
	appropriate.

diff --git a/rpython/translator/c/src/thread_gil.c b/rpython/translator/c/src/thread_gil.c
new file mode 100644
--- /dev/null
+++ b/rpython/translator/c/src/thread_gil.c
@@ -0,0 +1,160 @@
+
+/* Idea:
+
+   - "The GIL" is a composite concept.  There are two locks, and "the
+     GIL is locked" when both are locked.
+
+   - The first lock is a simple global variable 'rpy_fastgil'.  With
+     shadowstack, we use the most portable definition: 0 means unlocked
+     and != 0 means locked.  With asmgcc, 0 means unlocked but only 1
+     means locked.  A different value means unlocked too, but the value
+     is used by the JIT to contain the stack top for stack root scanning.
+
+   - The second lock is a regular mutex.  In the fast path, it is never
+     unlocked.  Remember that "the GIL is unlocked" means that either
+     the first or the second lock is unlocked.  It should never be the
+     case that both are unlocked at the same time.
+
+   - Let's call "thread 1" the thread with the GIL.  Whenever it does an
+     external function call, it sets 'rpy_fastgil' to 0 (unlocked).
+     This is the cheapest way to release the GIL.  When it returns from
+     the function call, this thread attempts to atomically change
+     'rpy_fastgil' to 1.  In the common case where it works, thread 1
+     has got the GIL back and so continues to run.
+
+   - Say "thread 2" is eagerly waiting for thread 1 to become blocked in
+     some long-running call.  Regularly, it checks if 'rpy_fastgil' is 0
+     and tries to atomically change it to 1.  If it succeeds, it means
+     that the GIL was not previously locked.  Thread 2 has now got the GIL.
+
+   - If there are more than 2 threads, the rest is really sleeping by
+     waiting on the 'mutex_gil_stealer' held by thread 2.
+
+   - An additional mechanism is used for when thread 1 wants to
+     explicitly yield the GIL to thread 2: it does so by releasing
+     'mutex_gil' (which is otherwise not released) but keeping the
+     value of 'rpy_fastgil' to 1.
+*/
+
+long rpy_fastgil = 1;
+long rpy_waiting_threads = -1;
+static mutex_t mutex_gil_stealer;
+static mutex_t mutex_gil;
+
+void RPyGilAllocate(void)
+{
+    assert(RPY_FASTGIL_LOCKED(rpy_fastgil));
+    mutex_init(&mutex_gil_stealer);
+    mutex_init(&mutex_gil);
+    mutex_lock(&mutex_gil);
+    rpy_waiting_threads = 0;
+}
+
+void RPyGilAcquire(void)
+{
+    /* Acquires the GIL.  Note: this function saves and restores 'errno'.
+     */
+    long old_fastgil = lock_test_and_set(&rpy_fastgil, 1);
+
+    if (!RPY_FASTGIL_LOCKED(old_fastgil)) {
+        /* The fastgil was not previously locked: success.
+           'mutex_gil' should still be locked at this point.
+        */
+    }
+    else {
+        /* Otherwise, another thread is busy with the GIL. */
+        int old_errno = errno;
+
+        /* Register me as one of the threads that is actively waiting
+           for the GIL.  The number of such threads is found in
+           rpy_lock_ready. */
+        assert(rpy_waiting_threads >= 0);
+        atomic_increment(&rpy_waiting_threads);
+
+        /* Enter the waiting queue from the end.  Assuming a roughly
+           first-in-first-out order, this will nicely give the threads
+           a round-robin chance.
+        */
+        mutex_lock(&mutex_gil_stealer);
+
+        /* We are now the stealer thread.  Steals! */
+        while (1) {
+            /* Sleep for one interval of time.  We may be woken up earlier
+               if 'mutex_gil' is released.
+            */
+            if (mutex_lock_timeout(&mutex_gil, 0.001)) {   /* 1 ms... */
+                /* We arrive here if 'mutex_gil' was recently released
+                   and we just relocked it.
+                 */
+                old_fastgil = 0;
+                break;
+            }
+
+            /* Busy-looping here.  Try to look again if 'rpy_fastgil' is
+               released.
+            */
+            if (!RPY_FASTGIL_LOCKED(rpy_fastgil)) {
+                old_fastgil = lock_test_and_set(&rpy_fastgil, 1);
+                if (!RPY_FASTGIL_LOCKED(old_fastgil))
+                    /* yes, got a non-held value!  Now we hold it. */
+                    break;
+            }
+            /* Otherwise, loop back. */
+        }
+        atomic_decrement(&rpy_waiting_threads);
+        mutex_unlock(&mutex_gil_stealer);
+
+        errno = old_errno;
+    }
+
+#ifdef PYPY_USE_ASMGCC
+    if (old_fastgil != 0) {
+        /* this case only occurs from the JIT compiler */
+        struct pypy_ASM_FRAMEDATA_HEAD0 *new =
+            (struct pypy_ASM_FRAMEDATA_HEAD0 *)old_fastgil;
+        struct pypy_ASM_FRAMEDATA_HEAD0 *root = &pypy_g_ASM_FRAMEDATA_HEAD;
+        struct pypy_ASM_FRAMEDATA_HEAD0 *next = root->as_next;
+        new->as_next = next;
+        new->as_prev = root;
+        root->as_next = new;
+        next->as_prev = new;
+    }
+#else
+    assert(old_fastgil == 0);
+#endif
+    assert(RPY_FASTGIL_LOCKED(rpy_fastgil));
+    return;
+}
+
+/*
+void RPyGilRelease(void)
+{
+    Releases the GIL in order to do an external function call.
+    We assume that the common case is that the function call is
+    actually very short, and optimize accordingly.
+
+    Note: this function is defined as a 'static inline' in thread.h.
+}
+*/
+
+long RPyGilYieldThread(void)
+{
+    /* can be called even before RPyGilAllocate(), but in this case,
+       'rpy_waiting_threads' will be -1. */
+    assert(RPY_FASTGIL_LOCKED(rpy_fastgil));
+    if (rpy_waiting_threads <= 0)
+        return 0;
+
+    /* Explicitly release the 'mutex_gil'.
+     */
+    mutex_unlock(&mutex_gil);
+
+    /* Now nobody has got the GIL, because 'mutex_gil' is released (but
+       rpy_fastgil is still locked).  Call RPyGilAcquire().  It will
+       enqueue ourselves at the end of the 'mutex_gil_stealer' queue.
+       If there is no other waiting thread, it will fall through both
+       its pthread_mutex_lock() and pthread_mutex_timedlock() now.
+     */
+    RPyGilAcquire();
+    return 1;
+}
diff --git a/rpython/translator/c/src/thread_nt.c b/rpython/translator/c/src/thread_nt.c
--- a/rpython/translator/c/src/thread_nt.c
+++ b/rpython/translator/c/src/thread_nt.c
@@ -196,52 +196,35 @@
 /* GIL code                                                 */
 /************************************************************/
 
-static volatile LONG pending_acquires = -1;
-static CRITICAL_SECTION mutex_gil;
-static HANDLE cond_gil;
+typedef HANDLE mutex_t;
 
-long RPyGilAllocate(void)
-{
-    pending_acquires = 0;
-    InitializeCriticalSection(&mutex_gil);
-    EnterCriticalSection(&mutex_gil);
-    cond_gil = CreateEvent (NULL, FALSE, FALSE, NULL);
-    return 1;
+static void gil_fatal(const char *msg) {
+    fprintf(stderr, "Fatal error in the GIL: %s\n", msg);
+    abort();
 }
 
-long RPyGilYieldThread(void)
-{
-    /* can be called even before RPyGilAllocate(), but in this case,
-       pending_acquires will be -1 */
-    if (pending_acquires <= 0)
-        return 0;
-    InterlockedIncrement(&pending_acquires);
-    PulseEvent(cond_gil);
-
-    /* hack: the three following lines do a pthread_cond_wait(), and
-       normally specifying a timeout of INFINITE would be fine.  But the
-       first and second operations are not done atomically, so there is a
-       (small) risk that PulseEvent misses the WaitForSingleObject().
-       In this case the process will just sleep a few milliseconds. */
-    LeaveCriticalSection(&mutex_gil);
-    WaitForSingleObject(cond_gil, 15);
-    EnterCriticalSection(&mutex_gil);
-
-    InterlockedDecrement(&pending_acquires);
-    return 1;
+static inline void mutex_init(mutex_t *mutex) {
+    *mutex = CreateMutex(NULL, 0, NULL);
+    if (*mutex == NULL)
+        gil_fatal("CreateMutex failed");
 }
 
-void RPyGilRelease(void)
-{
-    LeaveCriticalSection(&mutex_gil);
-    PulseEvent(cond_gil);
+static inline void mutex_lock(mutex_t *mutex) {
+    WaitForSingleObject(*mutex, INFINITE);
 }
 
-void RPyGilAcquire(void)
-{
-    InterlockedIncrement(&pending_acquires);
-    EnterCriticalSection(&mutex_gil);
-    InterlockedDecrement(&pending_acquires);
+static inline void mutex_unlock(mutex_t *mutex) {
+    ReleaseMutex(*mutex);
 }
 
-# error "XXX implement me"
+static inline int mutex_lock_timeout(mutex_t *mutex, double delay)
+{
+    DWORD result = WaitForSingleObject(*mutex, (DWORD)(delay * 1000.0 + 0.9));
+    return (result != WAIT_TIMEOUT);
+}
+
+#define lock_test_and_set(ptr, value)  InterlockedExchangeAcquire(ptr, value)
+#define atomic_increment(ptr)          InterlockedIncrement(ptr)
+#define atomic_decrement(ptr)          InterlockedDecrement(ptr)
+
+#include "src/thread_gil.c"
diff --git a/rpython/translator/c/src/thread_pthread.c b/rpython/translator/c/src/thread_pthread.c
--- a/rpython/translator/c/src/thread_pthread.c
+++ b/rpython/translator/c/src/thread_pthread.c
@@ -472,71 +472,18 @@
 /* GIL code                                                 */
 /************************************************************/
 
-
 #include <time.h>
 
-
 #define ASSERT_STATUS(call)                             \
     if (call != 0) {                                    \
         fprintf(stderr, "Fatal error: " #call "\n");    \
         abort();                                        \
     }
 
-/* Idea:
-
-   - "The GIL" is a composite concept.  There are two locks, and "the
-     GIL is locked" when both are locked.
-
-   - The first lock is a simple global variable 'rpy_fastgil'.  With
-     shadowstack, we use the most portable definition: 0 means unlocked
-     and != 0 means locked.  With asmgcc, 0 means unlocked but only 1
-     means locked.  A different value means unlocked too, but the value
-     is used by the JIT to contain the stack top for stack root scanning.
-
-   - The second lock is a regular mutex.  In the fast path, it is never
-     unlocked.  Remember that "the GIL is unlocked" means that either
-     the first or the second lock is unlocked.  It should never be the
-     case that both are unlocked at the same time.
-
-   - Let's call "thread 1" the thread with the GIL.  Whenever it does an
-     external function call, it sets 'rpy_fastgil' to 0 (unlocked).
-     This is the cheapest way to release the GIL.  When it returns from
-     the function call, this thread attempts to atomically change
-     'rpy_fastgil' to 1.  In the common case where it works, thread 1
-     has got the GIL back and so continues to run.
-
-   - Say "thread 2" is eagerly waiting for thread 1 to become blocked in
-     some long-running call.  Regularly, it checks if 'rpy_fastgil' is 0
-     and tries to atomically change it to 1.  If it succeeds, it means
-     that the GIL was not previously locked.  Thread 2 has now got the GIL.
-
-   - If there are more than 2 threads, the rest is really sleeping by
-     waiting on the 'mutex_gil_stealer' held by thread 2.
-
-   - An additional mechanism is used for when thread 1 wants to
-     explicitly yield the GIL to thread 2: it does so by releasing
-     'mutex_gil' (which is otherwise not released) but keeping the
-     value of 'rpy_fastgil' to 1.
-*/
-
-long rpy_fastgil = 1;
-static pthread_mutex_t mutex_gil_stealer;
-static pthread_mutex_t mutex_gil;
-long rpy_lock_ready = 0;
-
-void RPyGilAllocate(void)
+static inline void timespec_add(struct timespec *t, double incr)
 {
-    assert(RPY_FASTGIL_LOCKED(rpy_fastgil));
-    ASSERT_STATUS(pthread_mutex_init(&mutex_gil_stealer,
-                                     pthread_mutexattr_default));
-    ASSERT_STATUS(pthread_mutex_init(&mutex_gil, pthread_mutexattr_default));
-    ASSERT_STATUS(pthread_mutex_lock(&mutex_gil));
-    rpy_lock_ready = 1;
-}
-
-static inline void timespec_add(struct timespec *t, long incr)
-{
-    long nsec = t->tv_nsec + incr;
+    /* assumes that "incr" is not too large, less than 1 second */
+    long nsec = t->tv_nsec + (long)(incr * 1000000000.0);
     if (nsec >= 1000000000) {
         t->tv_sec += 1;
         nsec -= 1000000000;
@@ -545,112 +492,29 @@
     t->tv_nsec = nsec;
 }
 
-void RPyGilAcquire(void)
-{
-    /* Acquires the GIL.  Note: this function saves and restores 'errno'.
-     */
-    long old_fastgil = __sync_lock_test_and_set(&rpy_fastgil, 1);
+typedef pthread_mutex_t mutex_t;
 
-    if (!RPY_FASTGIL_LOCKED(old_fastgil)) {
-        /* The fastgil was not previously locked: success.
-           'mutex_gil' should still be locked at this point.
-        */
-    }
-    else {
-        /* Otherwise, another thread is busy with the GIL. */
-        int old_errno = errno;
-
-        /* Enter the waiting queue from the end.  Assuming a roughly
-           first-in-first-out order, this will nicely give the threads
-           a round-robin chance.
-        */
-        assert(rpy_lock_ready);
-        ASSERT_STATUS(pthread_mutex_lock(&mutex_gil_stealer));
-
-        /* We are now the stealer thread.  Steals! */
-        while (1) {
-            int delay = 1000000;   /* 1 ms... */
-            struct timespec t;
-
-            /* Sleep for one interval of time.  We may be woken up earlier
-               if 'mutex_gil' is released.
-            */
-            clock_gettime(CLOCK_REALTIME, &t);
-            timespec_add(&t, delay);
-            int error_from_timedlock = pthread_mutex_timedlock(&mutex_gil, &t);
-
-            if (error_from_timedlock != ETIMEDOUT) {
-                ASSERT_STATUS(error_from_timedlock);
-
-                /* We arrive here if 'mutex_gil' was recently released
-                   and we just relocked it.
-                 */
-                old_fastgil = 0;
-                break;
-            }
-
-            /* Busy-looping here.  Try to look again if 'rpy_fastgil' is
-               released.
-            */
-            if (!RPY_FASTGIL_LOCKED(rpy_fastgil)) {
-                old_fastgil = __sync_lock_test_and_set(&rpy_fastgil, 1);
-                if (!RPY_FASTGIL_LOCKED(old_fastgil))
-                    /* yes, got a non-held value!  Now we hold it. */
-                    break;
-            }
-            /* Otherwise, loop back. */
-        }
-        ASSERT_STATUS(pthread_mutex_unlock(&mutex_gil_stealer));
-
-        errno = old_errno;
-    }
-
-#ifdef PYPY_USE_ASMGCC
-    if (old_fastgil != 0) {
-        /* this case only occurs from the JIT compiler */
-        struct pypy_ASM_FRAMEDATA_HEAD0 *new =
-            (struct pypy_ASM_FRAMEDATA_HEAD0 *)old_fastgil;
-        struct pypy_ASM_FRAMEDATA_HEAD0 *root = &pypy_g_ASM_FRAMEDATA_HEAD;
-        struct pypy_ASM_FRAMEDATA_HEAD0 *next = root->as_next;
-        new->as_next = next;
-        new->as_prev = root;
-        root->as_next = new;
-        next->as_prev = new;
-    }
-#else
-    assert(old_fastgil == 0);
-#endif
-    assert(RPY_FASTGIL_LOCKED(rpy_fastgil));
-    return;
+static inline void mutex_init(mutex_t *mutex) {
+    ASSERT_STATUS(pthread_mutex_init(mutex, pthread_mutexattr_default));
 }
-
-/*
-void RPyGilRelease(void)
-{
-    Releases the GIL in order to do an external function call.
-    We assume that the common case is that the function call is
-    actually very short, and optimize accordingly.
-
-    Note: this function is defined as a 'static inline' in thread.h.
+static inline void mutex_lock(mutex_t *mutex) {
+    ASSERT_STATUS(pthread_mutex_lock(mutex));
 }
-*/
-
-long RPyGilYieldThread(void)
-{
-    assert(RPY_FASTGIL_LOCKED(rpy_fastgil));
-    if (!rpy_lock_ready)
+static inline void mutex_unlock(mutex_t *mutex) {
+    ASSERT_STATUS(pthread_mutex_unlock(mutex));
+}
+static inline int mutex_lock_timeout(mutex_t *mutex, double delay) {
+    struct timespec t;
+    clock_gettime(CLOCK_REALTIME, &t);
+    timespec_add(&t, delay);
+    int error_from_timedlock = pthread_mutex_timedlock(mutex, &t);
+    if (error_from_timedlock == ETIMEDOUT)
         return 0;
-
-    /* Explicitly release the 'mutex_gil'.
-     */
-    ASSERT_STATUS(pthread_mutex_unlock(&mutex_gil));
-
-    /* Now nobody has got the GIL, because 'mutex_gil' is released (but
-       rpy_fastgil is still locked).  Call RPyGilAcquire().  It will
-       enqueue ourselves at the end of the 'mutex_gil_stealer' queue.
-       If there is no other waiting thread, it will fall through both
-       its pthread_mutex_lock() and pthread_mutex_timedlock() now.
-     */
-    RPyGilAcquire();
+    ASSERT_STATUS(error_from_timedlock);
     return 1;
 }
+#define lock_test_and_set(ptr, value)  __sync_lock_test_and_set(ptr, value)
+#define atomic_increment(ptr)          __sync_fetch_and_add(ptr, 1)
+#define atomic_decrement(ptr)          __sync_fetch_and_sub(ptr, 1)
+
+#include "src/thread_gil.c"


More information about the pypy-commit mailing list