Replace copy coroutine with pthread implementation.

This commit is contained in:
Samuel Williams 2021-06-26 10:17:26 +12:00
parent 9c9531950c
commit 42130a64f0
No known key found for this signature in database
GPG key ID: A0765423A44728FB
30 changed files with 492 additions and 449 deletions

View file

@ -1,21 +0,0 @@
#ifndef COROUTINE_STACK_H
#define COROUTINE_STACK_H 1
/*
* This file is part of the "Coroutine" project and released under the MIT License.
*
* Created by Samuel Williams on 10/11/2020.
* Copyright, 2020, by Samuel Williams.
*/
#include COROUTINE_H
#ifdef COROUTINE_PRIVATE_STACK
#define COROUTINE_STACK_LOCAL(type, name) type *name = ruby_xmalloc(sizeof(type))
#define COROUTINE_STACK_FREE(name) ruby_xfree(name)
#else
#define COROUTINE_STACK_LOCAL(type, name) type name##_local; type * name = &name##_local
#define COROUTINE_STACK_FREE(name)
#endif
#endif /* COROUTINE_STACK_H */

View file

@ -22,6 +22,7 @@ enum {COROUTINE_REGISTERS = 6};
struct coroutine_context
{
void **stack_pointer;
void *argument;
};
typedef COROUTINE(* coroutine_start)(struct coroutine_context *from, struct coroutine_context *self);

View file

@ -23,6 +23,7 @@ enum {COROUTINE_REGISTERS = 8};
struct coroutine_context
{
void **stack_pointer;
void *argument;
};
typedef COROUTINE(* coroutine_start)(struct coroutine_context *from, struct coroutine_context *self);

View file

@ -22,6 +22,7 @@ enum {COROUTINE_REGISTERS = 0xb0 / 8};
struct coroutine_context
{
void **stack_pointer;
void *argument;
};
typedef COROUTINE(* coroutine_start)(struct coroutine_context *from, struct coroutine_context *self);

View file

@ -1,162 +0,0 @@
/*
* This file is part of the "Coroutine" project and released under the MIT License.
*
* Created by Samuel Williams on 24/6/2019.
* Copyright, 2019, by Samuel Williams.
*/
#include "Context.h"
#include <stdint.h>
// http://gcc.gnu.org/onlinedocs/gcc/Alternate-Keywords.html
#ifndef __GNUC__
#define __asm__ asm
#endif
#if defined(__sparc)
__attribute__((noinline))
// https://marc.info/?l=linux-sparc&m=131914569320660&w=2
static void coroutine_flush_register_windows(void) {
__asm__
#ifdef __GNUC__
__volatile__
#endif
#if defined(__sparcv9) || defined(__sparc_v9__) || defined(__arch64__)
#ifdef __GNUC__
("flushw" : : : "%o7")
#else
("flushw")
#endif
#else
("ta 0x03")
#endif
;
}
#else
static void coroutine_flush_register_windows(void) {}
#endif
__attribute__((noinline))
void *coroutine_stack_pointer(void) {
return (void*)(
(char*)__builtin_frame_address(0)
);
}
// Save the current stack to a private area. It is likely that when restoring the stack, this stack frame will be incomplete. But that is acceptable since the previous stack frame which called `setjmp` should be correctly restored.
__attribute__((noinline))
int coroutine_save_stack_1(struct coroutine_context * context) {
assert(context->stack);
assert(context->base);
void *stack_pointer = coroutine_stack_pointer();
// At this point, you may need to ensure on architectures that use register windows, that all registers are flushed to the stack, otherwise the copy of the stack will not contain the valid registers:
coroutine_flush_register_windows();
// Save stack to private area:
if (stack_pointer < context->base) {
size_t size = (char*)context->base - (char*)stack_pointer;
assert(size <= context->size);
memcpy(context->stack, stack_pointer, size);
context->used = size;
} else {
size_t size = (char*)stack_pointer - (char*)context->base;
assert(size <= context->size);
memcpy(context->stack, context->base, size);
context->used = size;
}
// Initialized:
return 0;
}
// Copy the current stack to a private memory buffer.
int coroutine_save_stack(struct coroutine_context * context) {
if (_setjmp(context->state)) {
// Restored.
return 1;
}
// We need to invoke the memory copy from one stack frame deeper than the one that calls setjmp. That is because if you don't do this, the setjmp might be restored into an invalid stack frame (truncated, etc):
return coroutine_save_stack_1(context);
}
__attribute__((noreturn, noinline))
void coroutine_restore_stack_padded(struct coroutine_context *context, void * buffer) {
void *stack_pointer = coroutine_stack_pointer();
assert(context->base);
// At this point, you may need to ensure on architectures that use register windows, that all registers are flushed to the stack, otherwise when we copy in the new stack, the registers would not be updated:
coroutine_flush_register_windows();
// Restore stack from private area:
if (stack_pointer < context->base) {
void * bottom = (char*)context->base - context->used;
assert(bottom > stack_pointer);
memcpy(bottom, context->stack, context->used);
} else {
void * top = (char*)context->base + context->used;
assert(top < stack_pointer);
memcpy(context->base, context->stack, context->used);
}
// Restore registers. The `buffer` is to force the compiler NOT to elide he buffer and `alloca`:
_longjmp(context->state, (int)(1 | (intptr_t)buffer));
}
// In order to swap between coroutines, we need to swap the stack and registers.
// `setjmp` and `longjmp` are able to swap registers, but what about swapping stacks? You can use `memcpy` to copy the current stack to a private area and `memcpy` to copy the private stack of the next coroutine to the main stack.
// But if the stack yop are copying in to the main stack is bigger than the currently executing stack, the `memcpy` will clobber the current stack frame (including the context argument). So we use `alloca` to push the current stack frame *beyond* the stack we are about to copy in. This ensures the current stack frame in `coroutine_restore_stack_padded` remains valid for calling `longjmp`.
__attribute__((noreturn))
void coroutine_restore_stack(struct coroutine_context *context) {
void *stack_pointer = coroutine_stack_pointer();
void *buffer = NULL;
// We must ensure that the next stack frame is BEYOND the stack we are restoring:
if (stack_pointer < context->base) {
intptr_t offset = (intptr_t)stack_pointer - ((intptr_t)context->base - context->used);
if (offset > 0) buffer = alloca(offset);
} else {
intptr_t offset = ((intptr_t)context->base + context->used) - (intptr_t)stack_pointer;
if (offset > 0) buffer = alloca(offset);
}
assert(context->used > 0);
coroutine_restore_stack_padded(context, buffer);
}
struct coroutine_context *coroutine_transfer(struct coroutine_context *current, struct coroutine_context *target)
{
struct coroutine_context *previous = target->from;
// In theory, either this condition holds true, or we should assign the base address to target:
assert(current->base == target->base);
// If you are trying to copy the coroutine to a different thread
// target->base = current->base
target->from = current;
assert(current != target);
// It's possible to come here, even thought the current fiber has been terminated. We are never going to return so we don't bother saving the stack.
if (current->stack) {
if (coroutine_save_stack(current) == 0) {
coroutine_restore_stack(target);
}
} else {
coroutine_restore_stack(target);
}
target->from = previous;
return target;
}

View file

@ -1,98 +0,0 @@
#ifndef COROUTINE_COPY_CONTEXT_H
#define COROUTINE_COPY_CONTEXT_H 1
/*
* This file is part of the "Coroutine" project and released under the MIT License.
*
* Created by Samuel Williams on 27/6/2019.
* Copyright, 2019, by Samuel Williams.
*/
#pragma once
#include <assert.h>
#include <stddef.h>
#include <setjmp.h>
#include <string.h>
#include <stdlib.h>
/* OpenBSD supports alloca, but does not include alloca.h */
#ifndef __OpenBSD__
#include <alloca.h>
#endif
#define COROUTINE __attribute__((noreturn)) void
#ifdef HAVE_STDINT_H
#include <stdint.h>
#if INTPTR_MAX <= INT32_MAX
#define COROUTINE_LIMITED_ADDRESS_SPACE
#endif
#endif
// This stack copying implementation which uses a private stack for each coroutine, including the main one.
#define COROUTINE_PRIVATE_STACK
struct coroutine_context
{
// Private stack:
void *stack;
size_t size, used;
// The top (or bottom) of the currently executing stack:
void *base;
jmp_buf state;
struct coroutine_context *from;
};
typedef COROUTINE(*coroutine_start)(struct coroutine_context *from, struct coroutine_context *self);
int coroutine_save_stack(struct coroutine_context * context);
COROUTINE coroutine_restore_stack(struct coroutine_context *context);
// @param stack The private stack area memory allocation (pointer to lowest address).
// @param size The size of the private stack area.
// @param base A stack pointer to the base of the main stack. On x86 hardware, this is the upper extent of the region that will be copied to the private stack.
static inline void coroutine_initialize_main(struct coroutine_context *context, void *stack, size_t size, void *base) {
assert(stack);
assert(size >= 1024);
context->stack = stack;
context->size = size;
context->used = 0;
assert(base);
context->base = base;
context->from = NULL;
}
// @param start The start function to invoke.
static inline void coroutine_initialize(
struct coroutine_context *context,
coroutine_start start,
void *stack,
size_t size,
void *base
) {
assert(start);
coroutine_initialize_main(context, stack, size, base);
if (coroutine_save_stack(context)) {
start(context->from, context);
}
}
struct coroutine_context *coroutine_transfer(struct coroutine_context *current, register struct coroutine_context *target);
static inline void coroutine_destroy(struct coroutine_context *context)
{
context->stack = NULL;
context->size = 0;
context->from = NULL;
}
#endif /* COROUTINE_COPY_CONTEXT_H */

View file

@ -26,6 +26,7 @@ struct coroutine_context
emscripten_fiber_t state;
coroutine_start entry_func;
struct coroutine_context * from;
void *argument;
};
COROUTINE coroutine_trampoline(void * _context);

View file

@ -19,6 +19,7 @@ enum {
struct coroutine_context
{
void **stack_pointer;
void *argument;
};
typedef COROUTINE(* coroutine_start)(struct coroutine_context *from, struct coroutine_context *self);

268
coroutine/pthread/Context.c Normal file
View file

@ -0,0 +1,268 @@
/*
* This file is part of the "Coroutine" project and released under the MIT License.
*
* Created by Samuel Williams on 24/6/2021.
* Copyright, 2021, by Samuel Williams.
*/
#include "Context.h"
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
static const int DEBUG = 0;
static
int check(const char * message, int result) {
if (result) {
switch (result) {
case EDEADLK:
if (DEBUG) fprintf(stderr, "deadlock detected result=%d errno=%d\n", result, errno);
break;
default:
if (DEBUG) fprintf(stderr, "error detected result=%d errno=%d\n", result, errno);
perror(message);
}
}
assert(result == 0);
return result;
}
void coroutine_initialize_main(struct coroutine_context * context) {
context->id = pthread_self();
check("coroutine_initialize_main:pthread_cond_init",
pthread_cond_init(&context->schedule, NULL)
);
context->shared = (struct coroutine_shared*)malloc(sizeof(struct coroutine_shared));
assert(context->shared);
context->shared->main = context;
context->shared->count = 1;
if (DEBUG) {
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
check("coroutine_initialize_main:pthread_mutex_init",
pthread_mutex_init(&context->shared->guard, &attr)
);
} else {
check("coroutine_initialize_main:pthread_mutex_init",
pthread_mutex_init(&context->shared->guard, NULL)
);
}
}
static
void coroutine_release(struct coroutine_context *context) {
if (context->shared) {
size_t count = (context->shared->count -= 1);
if (count == 0) {
if (DEBUG) fprintf(stderr, "coroutine_release:pthread_mutex_destroy(%p)\n", &context->shared->guard);
pthread_mutex_destroy(&context->shared->guard);
free(context->shared);
}
context->shared = NULL;
if (DEBUG) fprintf(stderr, "coroutine_release:pthread_cond_destroy(%p)\n", &context->schedule);
pthread_cond_destroy(&context->schedule);
}
}
void coroutine_initialize(
struct coroutine_context *context,
coroutine_start start,
void *stack,
size_t size
) {
assert(start && stack && size >= 1024);
// We will create the thread when we first transfer, but save the details now:
context->shared = NULL;
context->start = start;
context->stack = stack;
context->size = size;
}
static
int is_locked(pthread_mutex_t * mutex) {
int result = pthread_mutex_trylock(mutex);
// If we could successfully lock the mutex:
if (result == 0) {
pthread_mutex_unlock(mutex);
// We could lock the mutex, so it wasn't locked:
return 0;
} else {
// Otherwise we couldn't lock it because it's already locked:
return 1;
}
}
static
void coroutine_guard_unlock(void * _context)
{
struct coroutine_context * context = _context;
if (DEBUG) fprintf(stderr, "coroutine_guard_unlock:pthread_mutex_unlock\n");
check("coroutine_guard_unlock:pthread_mutex_unlock",
pthread_mutex_unlock(&context->shared->guard)
);
}
static
void coroutine_wait(struct coroutine_context *context)
{
if (DEBUG) fprintf(stderr, "coroutine_wait:pthread_mutex_lock(guard=%p is_locked=%d)\n", &context->shared->guard, is_locked(&context->shared->guard));
check("coroutine_wait:pthread_mutex_lock",
pthread_mutex_lock(&context->shared->guard)
);
if (DEBUG) fprintf(stderr, "coroutine_wait:pthread_mutex_unlock(guard)\n");
pthread_mutex_unlock(&context->shared->guard);
}
static
void coroutine_trampoline_cleanup(void *_context) {
struct coroutine_context * context = _context;
coroutine_release(context);
}
void * coroutine_trampoline(void * _context)
{
struct coroutine_context * context = _context;
assert(context->shared);
pthread_cleanup_push(coroutine_trampoline_cleanup, context);
coroutine_wait(context);
context->start(context->from, context);
pthread_cleanup_pop(1);
return NULL;
}
static
int coroutine_create_thread(struct coroutine_context *context)
{
int result;
pthread_attr_t attr;
result = pthread_attr_init(&attr);
if (result != 0) {
return result;
}
result = pthread_attr_setstack(&attr, context->stack, (size_t)context->size);
if (result != 0) {
pthread_attr_destroy(&attr);
return result;
}
result = pthread_cond_init(&context->schedule, NULL);
if (result != 0) {
pthread_attr_destroy(&attr);
return result;
}
result = pthread_create(&context->id, &attr, coroutine_trampoline, context);
if (result != 0) {
pthread_attr_destroy(&attr);
if (DEBUG) fprintf(stderr, "coroutine_create_thread:pthread_cond_destroy(%p)\n", &context->schedule);
pthread_cond_destroy(&context->schedule);
return result;
}
context->shared->count += 1;
return result;
}
struct coroutine_context * coroutine_transfer(struct coroutine_context * current, struct coroutine_context * target)
{
assert(current->shared);
struct coroutine_context * previous = target->from;
target->from = current;
if (DEBUG) fprintf(stderr, "coroutine_transfer:pthread_mutex_lock(guard=%p is_locked=%d)\n", &current->shared->guard, is_locked(&current->shared->guard));
pthread_mutex_lock(&current->shared->guard);
pthread_cleanup_push(coroutine_guard_unlock, current);
// First transfer:
if (target->shared == NULL) {
target->shared = current->shared;
if (DEBUG) fprintf(stderr, "coroutine_transfer:coroutine_create_thread...\n");
if (coroutine_create_thread(target)) {
if (DEBUG) fprintf(stderr, "coroutine_transfer:coroutine_create_thread failed\n");
target->shared = NULL;
target->from = previous;
return NULL;
}
} else {
if (DEBUG) fprintf(stderr, "coroutine_transfer:pthread_cond_signal(target)\n");
pthread_cond_signal(&target->schedule);
}
// A side effect of acting upon a cancellation request while in a condition wait is that the mutex is (in effect) re-acquired before calling the first cancellation cleanup handler. If cancelled, pthread_cond_wait immediately invokes cleanup handlers.
if (DEBUG) fprintf(stderr, "coroutine_transfer:pthread_cond_wait(schedule=%p, guard=%p, is_locked=%d)\n", &current->schedule, &current->shared->guard, is_locked(&current->shared->guard));
check("coroutine_transfer:pthread_cond_wait",
pthread_cond_wait(&current->schedule, &current->shared->guard)
);
if (DEBUG) fprintf(stderr, "coroutine_transfer:pthread_cleanup_pop\n");
pthread_cleanup_pop(1);
#ifdef __FreeBSD__
// Apparently required for FreeBSD:
pthread_testcancel();
#endif
target->from = previous;
return target;
}
static
void coroutine_join(struct coroutine_context * context) {
if (DEBUG) fprintf(stderr, "coroutine_join:pthread_cancel\n");
check("coroutine_join:pthread_cancel",
pthread_cancel(context->id)
);
if (DEBUG) fprintf(stderr, "coroutine_join:pthread_join\n");
check("coroutine_join:pthread_join",
pthread_join(context->id, NULL)
);
if (DEBUG) fprintf(stderr, "coroutine_join:pthread_join done\n");
}
void coroutine_destroy(struct coroutine_context * context)
{
if (DEBUG) fprintf(stderr, "coroutine_destroy\n");
assert(context);
// We are already destroyed or never created:
if (context->shared == NULL) return;
if (context == context->shared->main) {
context->shared->main = NULL;
coroutine_release(context);
} else {
coroutine_join(context);
assert(context->shared == NULL);
}
}

View file

@ -0,0 +1,63 @@
/*
* This file is part of the "Coroutine" project and released under the MIT License.
*
* Created by Samuel Williams on 24/6/2021.
* Copyright, 2021, by Samuel Williams.
*/
#pragma once
#include <assert.h>
#include <stddef.h>
#include <pthread.h>
#define COROUTINE void
#define COROUTINE_PTHREAD_CONTEXT
#ifdef HAVE_STDINT_H
#include <stdint.h>
#if INTPTR_MAX <= INT32_MAX
#define COROUTINE_LIMITED_ADDRESS_SPACE
#endif
#endif
struct coroutine_context;
struct coroutine_shared
{
pthread_mutex_t guard;
struct coroutine_context * main;
size_t count;
};
typedef COROUTINE(* coroutine_start)(struct coroutine_context *from, struct coroutine_context *self);
struct coroutine_context
{
struct coroutine_shared * shared;
coroutine_start start;
void *argument;
void *stack;
size_t size;
pthread_t id;
pthread_cond_t schedule;
struct coroutine_context * from;
};
void coroutine_initialize_main(struct coroutine_context * context);
void coroutine_initialize(
struct coroutine_context *context,
coroutine_start start,
void *stack,
size_t size
);
struct coroutine_context * coroutine_transfer(struct coroutine_context * current, struct coroutine_context * target);
void coroutine_destroy(struct coroutine_context * context);

View file

@ -12,6 +12,7 @@ enum {COROUTINE_REGISTERS = 0xd0 / 8};
struct coroutine_context
{
void **stack_pointer;
void *argument;
};
typedef COROUTINE(* coroutine_start)(struct coroutine_context *from, struct coroutine_context *self);

View file

@ -11,6 +11,7 @@
#if defined(__sun) && !defined(__EXTENSIONS__)
#define __EXTENSIONS__
#endif
#include "Context.h"
void coroutine_trampoline(void * _start, void * _context)

View file

@ -27,6 +27,7 @@ struct coroutine_context
{
ucontext_t state;
struct coroutine_context * from;
void *argument;
};
typedef COROUTINE(* coroutine_start)(struct coroutine_context *from, struct coroutine_context *self);

View file

@ -24,6 +24,7 @@ enum {COROUTINE_REGISTERS = 4};
struct coroutine_context
{
void **stack_pointer;
void *argument;
};
typedef void(__fastcall * coroutine_start)(struct coroutine_context *from, struct coroutine_context *self);

View file

@ -25,6 +25,7 @@ enum {
struct coroutine_context
{
void **stack_pointer;
void *argument;
};
typedef void(* coroutine_start)(struct coroutine_context *from, struct coroutine_context *self);

View file

@ -23,6 +23,7 @@ enum {COROUTINE_REGISTERS = 4};
struct coroutine_context
{
void **stack_pointer;
void *argument;
};
typedef COROUTINE(* coroutine_start)(struct coroutine_context *from, struct coroutine_context *self) __attribute__((fastcall));