diff --git a/targettopaz.py b/targettopaz.py index e20e2c07c..dbd62b9ca 100644 --- a/targettopaz.py +++ b/targettopaz.py @@ -21,3 +21,4 @@ def handle_config(config, translateconfig): elif host_factory.name in ('linux', 'darwin'): host_factory.cflags += ('-DMAX_STACK_SIZE=%d' % max_stack_size,) config.translation.suggest(check_str_without_nul=True) + config.translating = True diff --git a/topaz/executioncontext.py b/topaz/executioncontext.py index e750e4ec7..7a957e6d5 100644 --- a/topaz/executioncontext.py +++ b/topaz/executioncontext.py @@ -1,23 +1,13 @@ -from rpython.rlib import jit +import sys + +from rpython.rlib import jit, objectmodel +from rpython.rlib.unroll import unrolling_iterable from topaz.error import RubyError from topaz.frame import Frame from topaz.objects.fiberobject import W_FiberObject - -class ExecutionContextHolder(object): - # TODO: convert to be a threadlocal store once we have threads. - def __init__(self): - self._ec = None - - def get(self): - return self._ec - - def set(self, ec): - self._ec = ec - - def clear(self): - self._ec = None +TICK_COUNTER_STEP = 100 class ExecutionContext(object): @@ -34,6 +24,16 @@ def __init__(self): self.fiber_thread = None self.w_main_fiber = None + @staticmethod + def _mark_thread_disappeared(space): + # Called in the child process after os.fork() by interp_posix.py. + # Marks all ExecutionContexts except the current one + # with 'thread_disappeared = True'. + me = space.getexecutioncontext() + for ec in space.threadlocals.getallvalues().values(): + if ec is not me: + ec.thread_disappeared = True + def getmainfiber(self, space): if self.w_main_fiber is None: self.w_main_fiber = W_FiberObject.build_main_fiber(space, self) @@ -48,7 +48,8 @@ def gettraceproc(self): def hastraceproc(self): return self.w_trace_proc is not None and not self.in_trace_proc - def invoke_trace_proc(self, space, event, scope_id, classname, frame=None): + def invoke_only_trace_proc(self, space, event, scope_id, classname, + frame=None): if self.hastraceproc(): self.in_trace_proc = True try: @@ -68,6 +69,13 @@ def invoke_trace_proc(self, space, event, scope_id, classname, frame=None): finally: self.in_trace_proc = False + def invoke_trace_proc(self, space, event, scope_id, classname, frame=None, + decr_by=TICK_COUNTER_STEP): + self.invoke_only_trace_proc(space, event, scope_id, classname, frame) + actionflag = space.actionflag + if actionflag.decrement_ticker(decr_by) < 0: + actionflag.action_dispatcher(self, frame) + def enter(self, frame): frame.backref = self.topframeref if self.last_instr != -1: @@ -174,3 +182,142 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, tb): if self.added: del self.ec.catch_names[self.catch_name] + + +class AbstractActionFlag(object): + """This holds in an integer the 'ticker'. If threads are enabled, + it is decremented at each bytecode; when it reaches zero, we release + the GIL. And whether we have threads or not, it is forced to zero + whenever we fire any of the asynchronous actions. + """ + + _immutable_fields_ = ["checkinterval_scaled?"] + + def __init__(self): + self._periodic_actions = [] + self._nonperiodic_actions = [] + self.has_bytecode_counter = False + self.fired_actions = None + # the default value is not 100, unlike CPython 2.7, but a much + # larger value, because we use a technique that not only allows + # but actually *forces* another thread to run whenever the counter + # reaches zero. + self.checkinterval_scaled = 10000 * TICK_COUNTER_STEP + self._rebuild_action_dispatcher() + + def fire(self, action): + """Request for the action to be run before the next opcode.""" + if not action._fired: + action._fired = True + if self.fired_actions is None: + self.fired_actions = [] + self.fired_actions.append(action) + # set the ticker to -1 in order to force action_dispatcher() + # to run at the next possible bytecode + self.reset_ticker(-1) + + def register_periodic_action(self, action, use_bytecode_counter): + """NOT_RPYTHON: + Register the PeriodicAsyncAction action to be called whenever the + tick counter becomes smaller than 0. If 'use_bytecode_counter' is + True, make sure that we decrease the tick counter at every bytecode. + This is needed for threads. Note that 'use_bytecode_counter' can be + False for signal handling, because whenever the process receives a + signal, the tick counter is set to -1 by C code in signals.h. + """ + assert isinstance(action, PeriodicAsyncAction) + # hack to put the release-the-GIL one at the end of the list, + # and the report-the-signals one at the start of the list. + if use_bytecode_counter: + self._periodic_actions.append(action) + self.has_bytecode_counter = True + else: + self._periodic_actions.insert(0, action) + self._rebuild_action_dispatcher() + + def getcheckinterval(self): + return self.checkinterval_scaled // TICK_COUNTER_STEP + + def setcheckinterval(self, interval): + MAX = sys.maxint // TICK_COUNTER_STEP + if interval < 1: + interval = 1 + elif interval > MAX: + interval = MAX + self.checkinterval_scaled = interval * TICK_COUNTER_STEP + self.reset_ticker(-1) + + def _rebuild_action_dispatcher(self): + periodic_actions = unrolling_iterable(self._periodic_actions) + + @jit.unroll_safe + @objectmodel.dont_inline + def action_dispatcher(ec, frame): + # periodic actions (first reset the bytecode counter) + self.reset_ticker(self.checkinterval_scaled) + for action in periodic_actions: + action.perform(ec, frame) + + # nonperiodic actions + actions = self.fired_actions + if actions is not None: + self.fired_actions = None + # NB. in case there are several actions, we reset each + # 'action._fired' to false only when we're about to call + # 'action.perform()'. This means that if + # 'action.fire()' happens to be called any time before + # the corresponding perform(), the fire() has no + # effect---which is the effect we want, because + # perform() will be called anyway. + for action in actions: + action._fired = False + action.perform(ec, frame) + + self.action_dispatcher = action_dispatcher + + +class ActionFlag(AbstractActionFlag): + """The normal class for space.actionflag. The signal module provides + a different one.""" + _ticker = 0 + + def get_ticker(self): + return self._ticker + + def reset_ticker(self, value): + self._ticker = value + + def decrement_ticker(self, by): + value = self._ticker + if self.has_bytecode_counter: # this 'if' is constant-folded + if jit.isconstant(by) and by == 0: + pass # normally constant-folded too + else: + value -= by + self._ticker = value + return value + + +class AsyncAction(object): + """Abstract base class for actions that must be performed + asynchronously with regular bytecode execution, but that still need + to occur between two opcodes, not at a completely random time. + """ + _fired = False + + def __init__(self, space): + self.space = space + + def fire(self): + """Request for the action to be run before the next opcode. + The action must have been registered at space initalization time.""" + self.space.actionflag.fire(self) + + def perform(self, executioncontext, frame): + """To be overridden.""" + + +class PeriodicAsyncAction(AsyncAction): + """Abstract base class for actions that occur automatically + every TICK_COUNTER_STEP bytecodes. + """ diff --git a/topaz/interpreter.py b/topaz/interpreter.py index d4b2a3e34..24fa1b3eb 100644 --- a/topaz/interpreter.py +++ b/topaz/interpreter.py @@ -69,8 +69,12 @@ def _interpret(self, space, pc, frame, bytecode): frame.last_instr = pc if (space.getexecutioncontext().hastraceproc() and bytecode.lineno_table[pc] != bytecode.lineno_table[prev_pc]): - space.getexecutioncontext().invoke_trace_proc( - space, "line", None, None, frame=frame) + if jit.we_are_jitted(): + space.getexecutioncontext().invoke_only_trace_proc( + space, "line", None, None, frame=frame) + else: + space.getexecutioncontext().invoke_trace_proc( + space, "line", None, None, frame=frame) try: pc = self.handle_bytecode(space, pc, frame, bytecode) except RubyError as e: diff --git a/topaz/main.py b/topaz/main.py index 0b1d44ec8..d062c48ae 100644 --- a/topaz/main.py +++ b/topaz/main.py @@ -1,7 +1,6 @@ from __future__ import absolute_import import os -import subprocess from rpython.rlib import jit from rpython.rlib.objectmodel import specialize @@ -10,7 +9,7 @@ from topaz.error import RubyError, print_traceback from topaz.objects.exceptionobject import W_SystemExit from topaz.objspace import ObjectSpace -from topaz.system import IS_WINDOWS, IS_64BIT +from topaz.system import IS_WINDOWS, RUBY_DESCRIPTION USAGE = "\n".join([ @@ -42,11 +41,6 @@ "" ]) COPYRIGHT = "topaz - Copyright (c) Alex Gaynor and individual contributors\n" -RUBY_REVISION = subprocess.check_output([ - "git", - "--git-dir", os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, ".git"), - "rev-parse", "--short", "HEAD" -]).rstrip() if IS_WINDOWS: def WinStdinStream(): @@ -72,6 +66,7 @@ def get_topaz_config_options(): return { "translation.continuation": True, "translation.jit_opencoder_model": "big", + "translation.rweakref": True, } @@ -128,13 +123,7 @@ def _parse_argv(space, argv): elif arg == "--copyright": raise ShortCircuitError(COPYRIGHT) elif arg == "--version": - raise ShortCircuitError("%s\n" % space.str_w( - space.send( - space.w_object, - "const_get", - [space.newstr_fromstr("RUBY_DESCRIPTION")] - ) - )) + raise ShortCircuitError("%s\n" % RUBY_DESCRIPTION) elif arg == "-v": flag_globals_w["$-v"] = space.w_true flag_globals_w["$VERBOSE"] = space.w_true @@ -221,23 +210,6 @@ def _parse_argv(space, argv): def _entry_point(space, argv): - if IS_WINDOWS: - system = "Windows" - cpu = "x86_64" if IS_64BIT else "i686" - else: - system, _, _, _, cpu = os.uname() - platform = "%s-%s" % (cpu, system.lower()) - engine = "topaz" - version = "2.4.0" - patchlevel = 0 - description = "%s (ruby-%sp%d) (git rev %s) [%s]" % (engine, version, patchlevel, RUBY_REVISION, platform) - space.set_const(space.w_object, "RUBY_ENGINE", space.newstr_fromstr(engine)) - space.set_const(space.w_object, "RUBY_VERSION", space.newstr_fromstr(version)) - space.set_const(space.w_object, "RUBY_PATCHLEVEL", space.newint(patchlevel)) - space.set_const(space.w_object, "RUBY_PLATFORM", space.newstr_fromstr(platform)) - space.set_const(space.w_object, "RUBY_DESCRIPTION", space.newstr_fromstr(description)) - space.set_const(space.w_object, "RUBY_REVISION", space.newstr_fromstr(RUBY_REVISION)) - try: ( flag_globals_w, @@ -275,7 +247,7 @@ def _entry_point(space, argv): space.set_const(space.w_object, "ARGV", space.newarray(argv_w)) explicitly_verbose = space.is_true(flag_globals_w["$-v"]) if explicitly_verbose: - os.write(1, "%s\n" % description) + os.write(1, "%s\n" % RUBY_DESCRIPTION) for varname, w_value in flag_globals_w.iteritems(): space.globals.set(space, varname, w_value) diff --git a/topaz/modules/ffi/type.py b/topaz/modules/ffi/type.py index 81dd3112f..8e3754cee 100644 --- a/topaz/modules/ffi/type.py +++ b/topaz/modules/ffi/type.py @@ -1,13 +1,13 @@ -from topaz.objects.objectobject import W_Object -from topaz.module import ClassDef - -from rpython.rlib.jit_libffi import FFI_TYPE_P from rpython.rlib import clibffi -from rpython.rtyper.lltypesystem import rffi, lltype +from rpython.rlib.jit_libffi import FFI_TYPE_P +from rpython.rlib.objectmodel import not_rpython from rpython.rlib.rarithmetic import intmask -from topaz.coerce import Coerce +from rpython.rtyper.lltypesystem import rffi, lltype +from topaz.coerce import Coerce +from topaz.module import ClassDef from topaz.modules.ffi import misc +from topaz.objects.objectobject import W_Object _native_types = [ ('VOID', clibffi.ffi_type_void, lltype.Void, []), @@ -66,14 +66,14 @@ del _native_types +@not_rpython def lltype_for_name(name): - """NOT_RPYTHON""" # XXX maybe use a dictionary return lltypes[type_names.index(name)] +@not_rpython def size_for_name(name): - """NOT_RPYTHON""" # XXX maybe use a dictionary return lltype_sizes[type_names.index(name)] diff --git a/topaz/modules/process.py b/topaz/modules/process.py index fdac8d8ee..449953f67 100644 --- a/topaz/modules/process.py +++ b/topaz/modules/process.py @@ -12,6 +12,8 @@ from topaz.modules.signal import SIGNALS from topaz.system import IS_WINDOWS from topaz.error import error_for_oserror +from topaz.executioncontext import ExecutionContext +from topaz.utils import threadlocals if IS_WINDOWS: @@ -92,6 +94,9 @@ def method_exit_bang(self, space, status=0): def method_fork(self, space, block): pid = fork() if pid == 0: + ExecutionContext._mark_thread_disappeared(space) + threadlocals.reinit_threads(space) + if block is not None: space.invoke_block(block, []) space.send(self, "exit") diff --git a/topaz/objects/threadobject.py b/topaz/objects/threadobject.py index 6d9cafaa6..cfc919aad 100644 --- a/topaz/objects/threadobject.py +++ b/topaz/objects/threadobject.py @@ -1,20 +1,33 @@ import copy +import weakref + +from rpython.rlib import jit +from rpython.rlib.rshrinklist import AbstractShrinkList from topaz.module import ClassDef from topaz.objects.objectobject import W_Object +class WRefShrinkList(AbstractShrinkList): + def must_keep(self, wref): + return wref() is not None + + class W_ThreadObject(W_Object): + _attrs_ = ["space"] classdef = ClassDef("Thread", W_Object.classdef) def __init__(self, space): W_Object.__init__(self, space) - # TODO: This should be a map dict. - self.local_storage = {} + self.space = space + + def local_storage(self): + return storage.get_or_create_local_storage(self.space) def __deepcopy__(self, memo): obj = super(W_ThreadObject, self).__deepcopy__(memo) - obj.local_storage = copy.deepcopy(self.local_storage, memo) + local_storage_copy = copy.deepcopy(self.local_storage(), memo) + storage.get_or_create_local_storage(self.space, local_storage_copy) return obj @classdef.singleton_method("current") @@ -23,11 +36,11 @@ def method_current(self, space): @classdef.method("[]", key="str") def method_subscript(self, space, key): - return self.local_storage.get(key, space.w_nil) + return self.local_storage().get(key, space.w_nil) @classdef.method("[]=", key="str") def method_subscript_assign(self, space, key, w_value): - self.local_storage[key] = w_value + self.local_storage()[key] = w_value return w_value @classdef.method("recursion_guard") @@ -50,3 +63,29 @@ def method_in_recursion_guardp(self, space, w_identifier): if identifier in ec.recursive_calls: return space.w_true return space.w_false + + +class Local(): + """Thread-local data""" + + @jit.dont_look_inside + def __init__(self): + self.dicts = {} # mapping ExecutionContexts to storage dicts + + @jit.dont_look_inside + def get_or_create_local_storage(self, space, initdata=None): + ec = space.getexecutioncontext() + if ec not in self.dicts or initdata is not None: + self.dicts[ec] = initdata or {} + self._register_in_ec(space, ec) + return self.dicts[ec] + + def _register_in_ec(self, space, ec): + if not space.config.translation.rweakref: + return # without weakrefs, works but 'dicts' is never cleared + if ec._thread_local_objs is None: + ec._thread_local_objs = WRefShrinkList() + ec._thread_local_objs.append(weakref.ref(self)) + + +storage = Local() diff --git a/topaz/objspace.py b/topaz/objspace.py index b9af002b8..fbcb76d5b 100644 --- a/topaz/objspace.py +++ b/topaz/objspace.py @@ -7,7 +7,8 @@ from rpython.rlib import jit, rpath, types from rpython.rlib.cache import Cache -from rpython.rlib.objectmodel import specialize, compute_unique_id +from rpython.rlib.objectmodel import ( + specialize, compute_unique_id, we_are_translated) from rpython.rlib.signature import signature from rpython.rlib.rarithmetic import intmask from rpython.rlib.rbigint import rbigint @@ -20,7 +21,7 @@ from topaz.celldict import GlobalsDict from topaz.closure import ClosureCell from topaz.error import RubyError, print_traceback -from topaz.executioncontext import ExecutionContext, ExecutionContextHolder +from topaz.executioncontext import ActionFlag, ExecutionContext from topaz.frame import Frame from topaz.interpreter import Interpreter from topaz.lexer import LexerError, Lexer @@ -76,6 +77,7 @@ from topaz.objects.timeobject import W_TimeObject from topaz.parser import Parser from topaz.utils.ll_file import isdir +from topaz.utils.threadlocals import ThreadLocals class SpaceCache(Cache): @@ -93,7 +95,8 @@ def __init__(self, config): self.cache = SpaceCache(self) self.symbol_cache = {} - self._executioncontexts = ExecutionContextHolder() + self.actionflag = ActionFlag() + self.threadlocals = ThreadLocals(self) self.globals = GlobalsDict() self.bootstrap = True self.exit_handlers_w = [] @@ -248,7 +251,6 @@ def __init__(self, config): os.path.dirname(__file__), os.path.pardir), "lib-ruby")) def _freeze_(self): - self._executioncontexts.clear() return True def find_executable(self, executable): @@ -267,6 +269,11 @@ def setup(self, executable): """ Performs runtime setup. """ + + # To be called before using the space + self.threadlocals.setup_threads(self) + self.threadlocals.enter_thread() + path = rpath.rabspath(self.find_executable(executable)) # Fallback to a path relative to the compiled location. lib_path = self.base_lib_path @@ -284,6 +291,25 @@ def setup(self, executable): self.send(self.w_load_path, "unshift", [self.newstr_fromstr(lib_path)]) self.load_kernel(kernel_path) + self.set_const( + self.w_object, + "RUBY_ENGINE", self.newstr_fromstr(system.RUBY_ENGINE)) + self.set_const( + self.w_object, + "RUBY_VERSION", self.newstr_fromstr(system.RUBY_VERSION)) + self.set_const( + self.w_object, + "RUBY_PATCHLEVEL", self.newint(system.RUBY_PATCHLEVEL)) + self.set_const( + self.w_object, + "RUBY_PLATFORM", self.newstr_fromstr(system.RUBY_PLATFORM)) + self.set_const( + self.w_object, + "RUBY_DESCRIPTION", self.newstr_fromstr(system.RUBY_DESCRIPTION)) + self.set_const( + self.w_object, + "RUBY_REVISION", self.newstr_fromstr(system.RUBY_REVISION)) + def load_kernel(self, kernel_path): self.send( self.w_kernel, @@ -339,11 +365,29 @@ def execute(self, source, w_self=None, lexical_scope=None, filepath="-e", @jit.loop_invariant def getexecutioncontext(self): - ec = self._executioncontexts.get() - if ec is None: - ec = ExecutionContext() - self._executioncontexts.set(ec) - return ec + if not we_are_translated(): + if self.config.translating: + assert self.threadlocals.get_ec() is None, ( + "threadlocals got an ExecutionContext during translation!") + try: + return self._ec_during_translation + except AttributeError: + ec = ExecutionContext() + self._ec_during_translation = ec + return ec + else: + ec = self.threadlocals.get_ec() + if ec is None: + self.threadlocals.enter_thread() + ec = self.threadlocals.get_ec() + return ec + else: + # translated case follows. + # the result is assumed to be non-null: enter_thread() was called + # by space.setup(). + ec = self.threadlocals.get_ec() + assert ec is not None + return ec def create_frame(self, bc, w_self=None, lexical_scope=None, block=None, parent_interp=None, top_parent_interp=None, diff --git a/topaz/system.py b/topaz/system.py index 625ccad39..0c9562e2f 100755 --- a/topaz/system.py +++ b/topaz/system.py @@ -1,9 +1,33 @@ import sys import os import platform +import subprocess IS_POSIX = os.name == "posix" IS_WINDOWS = os.name == "nt" IS_LINUX = "linux" in sys.platform IS_64BIT = "64bit" in platform.architecture()[0] IS_CYGWIN = "cygwin" == sys.platform + +try: + RUBY_REVISION = subprocess.check_output([ + "git", + "--git-dir", os.path.join(os.path.dirname( + os.path.abspath(__file__)), os.pardir, ".git"), + "rev-parse", "--short", "HEAD" + ]).rstrip() +except subprocess.CalledProcessError: + RUBY_REVISION = "unknown" + +if IS_WINDOWS: + os_name = "Windows" + cpu = "x86_64" if IS_64BIT else "i686" +else: + os_name, _, _, _, cpu = os.uname() + +RUBY_PLATFORM = "%s-%s" % (cpu, os_name.lower()) +RUBY_ENGINE = "topaz" +RUBY_VERSION = "2.4.0" +RUBY_PATCHLEVEL = 0 +RUBY_DESCRIPTION = "%s (ruby-%sp%d) (git rev %s) [%s]" % ( + RUBY_ENGINE, RUBY_VERSION, RUBY_PATCHLEVEL, RUBY_REVISION, RUBY_PLATFORM) diff --git a/topaz/utils/threadlocals.py b/topaz/utils/threadlocals.py new file mode 100644 index 000000000..a447db2ca --- /dev/null +++ b/topaz/utils/threadlocals.py @@ -0,0 +1,187 @@ +import weakref + +from rpython.rlib import rshrinklist, rthread +from rpython.rlib import rgil +from rpython.rlib.objectmodel import not_rpython, we_are_translated + +from topaz.executioncontext import ExecutionContext, PeriodicAsyncAction + +ExecutionContext._thread_local_objs = None + + +class ThreadLocals(object): + # Inspired by Pypy's GILThreadLocals/OSThreadLocals + gil_ready = False + _immutable_fields_ = ['gil_ready?'] + + @not_rpython + def __init__(self, space): + self.space = space + self._valuedict = {} + self._cleanup_() + self.raw_thread_local = rthread.ThreadLocalReference( + ExecutionContext, loop_invariant=True) + # add the GIL-releasing callback as an action on the space + space.actionflag.register_periodic_action(GILReleaseAction(space), + use_bytecode_counter=True) + + def can_optimize_with_weaklist(self): + config = self.space.config + return (config.translation.rweakref and + rthread.ThreadLocalReference.automatic_keepalive(config)) + + def get_ec(self): + ec = self.raw_thread_local.get() + if not we_are_translated(): + assert ec is self._valuedict.get(rthread.get_ident(), None) + return ec + + def _cleanup_(self): + self._valuedict.clear() + self._weaklist = None + self._mainthreadident = 0 + + def enter_thread(self): + "Notification that the current thread is about to start running." + self._set_ec(ExecutionContext()) + + def try_enter_thread(self): + # common case: the thread-local has already got a value + if self.raw_thread_local.get() is not None: + return False + + # Else, make and attach a new ExecutionContext + ec = ExecutionContext() + if not self.can_optimize_with_weaklist(): + self._set_ec(ec) + return True + + # If can_optimize_with_weaklist(), then 'rthread' keeps the + # thread-local values alive until the end of the thread. Use + # AutoFreeECWrapper as an object with a __del__; when this + # __del__ is called, it means the thread was really finished. + # In this case we don't want leave_thread() to be called + # explicitly, so we return False. + if self._weaklist is None: + self._weaklist = ListECWrappers() + self._weaklist.append(weakref.ref(AutoFreeECWrapper(ec))) + self._set_ec(ec, register_in_valuedict=False) + return False + + def _set_ec(self, ec, register_in_valuedict=True): + ident = rthread.get_ident() + if self._mainthreadident == 0 or self._mainthreadident == ident: + self._mainthreadident = ident + if register_in_valuedict: + self._valuedict[ident] = ec + self.raw_thread_local.set(ec) + + def leave_thread(self): + "Notification that the current thread is about to stop." + ec = self.get_ec() + if ec is not None: + try: + thread_is_stopping(ec) + finally: + self.raw_thread_local.set(None) + ident = rthread.get_ident() + try: + del self._valuedict[ident] + except KeyError: + pass + + def setup_threads(self, space): + """Enable threads in the object space, if they haven't already been.""" + if not self.gil_ready: + # Note: this is a quasi-immutable read by module/pypyjit/interp_jit + # It must be changed (to True) only if it was really False before + rgil.allocate() + self.gil_ready = True + result = True + else: + result = False # already set up + return result + + def threads_initialized(self): + return self.gil_ready + + def getallvalues(self): + if self._weaklist is None: + return self._valuedict + # This logic walks the 'self._weaklist' list and adds the + # ExecutionContexts to 'result'. We are careful in case there + # are two AutoFreeECWrappers in the list which have the same + # 'ident'; in this case we must keep the most recent one (the + # older one should be deleted soon). Moreover, entries in + # self._valuedict have priority because they are never + # outdated. + result = {} + for h in self._weaklist.items(): + wrapper = h() + if wrapper is not None and not wrapper.deleted: + result[wrapper.ident] = wrapper.ec + # ^^ this possibly overwrites an older ec + result.update(self._valuedict) + return result + + def reinit_threads(self, space): + "Called in the child process after a fork()" + ident = rthread.get_ident() + ec = self.get_ec() + assert ec is not None + self._cleanup_() # clears self._valuedict + self._mainthreadident = ident + self._set_ec(ec) + + +def reinit_threads(space): + "Called in the child process after a fork()" + space.threadlocals.reinit_threads(space) + # bootstrapper.reinit() + rthread.thread_after_fork() + + +def thread_is_stopping(ec): + tlobjs = ec._thread_local_objs + if tlobjs is None: + return + ec._thread_local_objs = None + for wref in tlobjs.items(): + local = wref() + if local is not None: + del local.dicts[ec] + local.last_dict = None + local.last_ec = None + + +class AutoFreeECWrapper(object): + deleted = False + + def __init__(self, ec): + # this makes a loop between 'self' and 'ec'. It should not prevent + # the __del__ method here from being called. + self.ec = ec + ec._threadlocals_auto_free = self + self.ident = rthread.get_ident() + + def __del__(self): + # this is always called in another thread: the thread + # referenced by 'self.ec' has finished at that point, and + # we're just after the GC which finds no more references to + # 'ec' (and thus to 'self'). + self.deleted = True + thread_is_stopping(self.ec) + + +class ListECWrappers(rshrinklist.AbstractShrinkList): + def must_keep(self, wref): + return wref() is not None + + +class GILReleaseAction(PeriodicAsyncAction): + """An action called every TICK_COUNTER_STEP bytecodes. It releases the GIL + to give some other thread a chance to run. + """ + + def perform(self, executioncontext, frame): + rgil.yield_thread()