w TurtleWare: Dynamic Vars - The Empire Strikes Back By turtleware.eu Published On :: Mon, 28 Oct 2024 00:00:00 GMT Table of Contents Thread Local storage exhausted The layer of indirection I can fix her Let's write some tests! Summary Thread Local storage exhausted In the last post I've described a technique to use dynamic variables by value instead of the name by utilizing the operator PROGV. Apparently it works fine on all Common Lisp implementations I've tried except from SBCL, where the number of thread local variables is by default limited to something below 4000. To add salt to the injury, these variables are not garbage collected. Try the following code to crash into LDB: (defun foo () (loop for i from 0 below 4096 do (when (zerop (mod i 100)) (print i)) (progv (list (gensym)) (list 42) (values)))) (foo) This renders our new technique not very practical given SBCL popularity. We need to either abandon the idea or come up with a workaround. The layer of indirection Luckily for us we've already introduced a layer of indirection. Operators to access dynamic variables are called DLET, DSET and DREF. This means, that it is enough to provide a kludge implementation for SBCL with minimal changes to the remaining code. The old code works the same as previously except that instead of SYMBOL-VALUE we use the accessor DYNAMIC-VARIABLE-VALUE, and the old call to PROGV is now DYNAMIC-VARIABLE-PROGV. Moreover DYNAMIC-EFFECTIVE-SLOT used functions BOUNDP and MAKUNBOUND, so we replace these with DYNAMIC-VARIABLE-BOUND-P and DYNAMIC-VARIABLE-MAKUNBOUND. To abstract away things further we also introduce the constructor MAKE-DYNAMIC-VARIABLE (defpackage "EU.TURTLEWARE.BLOG/DLET" (:local-nicknames ("MOP" #+closer-mop "C2MOP" #+(and (not closer-mop) ecl) "MOP" #+(and (not closer-mop) ccl) "CCL" #+(and (not closer-mop) sbcl) "SB-MOP")) (:use "CL")) (in-package "EU.TURTLEWARE.BLOG/DLET") (eval-when (:compile-toplevel :execute :load-toplevel) (unless (member :bordeaux-threads *features*) (error "Please load BORDEAUX-THREADS.")) (when (member :sbcl *features*) (unless (member :fake-progv-kludge *features*) (format t "~&;; Using FAKE-PROGV-KLUDGE for SBCL.~%") (push :fake-progv-kludge *features*)))) (defmacro dlet (bindings &body body) (flet ((pred (binding) (and (listp binding) (= 2 (length binding))))) (unless (every #'pred bindings) (error "DLET: bindings must be lists of two values.~%~ Invalid bindings:~%~{ ~s~%~}" (remove-if #'pred bindings)))) (loop for (var val) in bindings collect var into vars collect val into vals finally (return `(dynamic-variable-progv (list ,@vars) (list ,@vals) ,@body)))) (defmacro dset (&rest pairs) `(setf ,@(loop for (var val) on pairs by #'cddr collect `(dref ,var) collect val))) (defmacro dref (variable) `(dynamic-variable-value ,variable)) ;;; ... (defmethod mop:slot-boundp-using-class ((class standard-class) object (slotd dynamic-effective-slot)) (dynamic-variable-bound-p (slot-dvar object slotd))) (defmethod mop:slot-makunbound-using-class ((class standard-class) object (slotd dynamic-effective-slot)) (dynamic-variable-makunbound (slot-dvar object slotd))) With these in place we can change the portable implementation to conform. #-fake-progv-kludge (progn (defun make-dynamic-variable () (gensym)) (defun dynamic-variable-value (variable) (symbol-value variable)) (defun (setf dynamic-variable-value) (value variable) (setf (symbol-value variable) value)) (defun dynamic-variable-bound-p (variable) (boundp variable)) (defun dynamic-variable-makunbound (variable) (makunbound variable)) (defmacro dynamic-variable-progv (vars vals &body body) `(progv ,vars ,vals ,@body))) I can fix her The implementation for SBCL will mediate access to the dynamic variable value with a synchronized hash table with weak keys. The current process is the key of the hash table and the list of bindings is the value of the hash table. For compatibility between implementations the top level value of the symbol will be shared. The variable +FAKE-UNBOUND+ is the marker that signifies, that the variable has no value. When the list of bindings is EQ to +CELL-UNBOUND+, then it means that we should use the global value. We add new bindings by pushing to it. #+fake-progv-kludge (progn (defvar +fake-unbound+ 'unbound) (defvar +cell-unbound+ '(no-binding)) (defclass dynamic-variable () ((tls-table :initform (make-hash-table :synchronized t :weakness :key) :reader dynamic-variable-tls-table) (top-value :initform +fake-unbound+ :accessor dynamic-variable-top-value))) (defun make-dynamic-variable () (make-instance 'dynamic-variable)) (defun dynamic-variable-bindings (dvar) (let ((process (bt:current-thread)) (tls-table (dynamic-variable-tls-table dvar))) (gethash process tls-table +cell-unbound+))) (defun (setf dynamic-variable-bindings) (value dvar) (let ((process (bt:current-thread)) (tls-table (dynamic-variable-tls-table dvar))) (setf (gethash process tls-table +cell-unbound+) value)))) We define two readers for the variable value - one that simply reads the value, and the other that signals an error if the variable is unbound. Writer for its value either replaces the current binding, or if the value cell is unbound, then we modify the top-level symbol value. We use the value +FAKE-UNBOUND+ to check whether the variable is bound and to make it unbound. #+fake-progv-kludge (progn (defun %dynamic-variable-value (dvar) (let ((tls-binds (dynamic-variable-bindings dvar))) (if (eq tls-binds +cell-unbound+) (dynamic-variable-top-value dvar) (car tls-binds)))) (defun dynamic-variable-value (dvar) (let ((tls-value (%dynamic-variable-value dvar))) (when (eq tls-value +fake-unbound+) (error 'unbound-variable :name "(unnamed)")) tls-value)) (defun (setf dynamic-variable-value) (value dvar) (let ((tls-binds (dynamic-variable-bindings dvar))) (if (eq tls-binds +cell-unbound+) (setf (dynamic-variable-top-value dvar) value) (setf (car tls-binds) value)))) (defun dynamic-variable-bound-p (dvar) (not (eq +fake-unbound+ (%dynamic-variable-value dvar)))) (defun dynamic-variable-makunbound (dvar) (setf (dynamic-variable-value dvar) +fake-unbound+))) Finally we define the operator to dynamically bind variables that behaves similar to PROGV. Note that we PUSH and POP from the thread-local hash table DYNAMIC-VARIABLE-BINDINGS, so no synchronization is necessary. #+fake-progv-kludge (defmacro dynamic-variable-progv (vars vals &body body) (let ((svars (gensym)) (svals (gensym)) (var (gensym)) (val (gensym))) `(let ((,svars ,vars)) (loop for ,svals = ,vals then (rest ,svals) for ,var in ,svars for ,val = (if ,svals (car ,svals) +fake-unbound+) do (push ,val (dynamic-variable-bindings ,var))) (unwind-protect (progn ,@body) (loop for ,var in ,svars do (pop (dynamic-variable-bindings ,var))))))) Let's write some tests! But of course, we are going to also write a test framework. It's short, I promise. As a bonus point the API is compatibile with fiveam, so it is possible to drop tests as is in the appropriate test suite. (defvar *all-tests* '()) (defun run-tests () (dolist (test (reverse *all-tests*)) (format *debug-io* "Test ~a... " test) (handler-case (funcall test) (serious-condition (c) (format *debug-io* "Failed: ~a~%" c)) (:no-error (&rest args) (declare (ignore args)) (format *debug-io* "Passed.~%"))))) (defmacro test (name &body body) `(progn (pushnew ',name *all-tests*) (defun ,name () ,@body))) (defmacro is (form) `(assert ,form)) (defmacro pass ()) (defmacro signals (condition form) `(is (block nil (handler-case ,form (,condition () (return t))) nil))) (defmacro finishes (form) `(is (handler-case ,form (serious-condition (c) (declare (ignore c)) nil) (:no-error (&rest args) (declare (ignore args)) t)))) Now let's get to tests. First we'll test our metaclass: (defclass dynamic-let.test-class () ((slot1 :initarg :slot1 :dynamic nil :accessor slot1) (slot2 :initarg :slot2 :dynamic t :accessor slot2) (slot3 :initarg :slot3 :accessor slot3)) (:metaclass class-with-dynamic-slots)) (defparameter *dynamic-let.test-instance-1* (make-instance 'dynamic-let.test-class :slot1 :a :slot2 :b :slot3 :c)) (defparameter *dynamic-let.test-instance-2* (make-instance 'dynamic-let.test-class :slot1 :x :slot2 :y :slot3 :z)) (test dynamic-let.1 (let ((o1 *dynamic-let.test-instance-1*) (o2 *dynamic-let.test-instance-2*)) (with-slots (slot1 slot2 slot3) o1 (is (eq :a slot1)) (is (eq :b slot2)) (is (eq :c slot3))) (with-slots (slot1 slot2 slot3) o2 (is (eq :x slot1)) (is (eq :y slot2)) (is (eq :z slot3))))) (test dynamic-let.2 (let ((o1 *dynamic-let.test-instance-1*) (o2 *dynamic-let.test-instance-2*)) (signals error (slot-dlet (((o1 'slot1) 1)) nil)) (slot-dlet (((o1 'slot2) :k)) (is (eq :k (slot-value o1 'slot2))) (is (eq :y (slot-value o2 'slot2)))))) (test dynamic-let.3 (let ((o1 *dynamic-let.test-instance-1*) (exit nil) (fail nil)) (flet ((make-runner (values) (lambda () (slot-dlet (((o1 'slot2) :start)) (let ((value (slot2 o1))) (unless (eq value :start) (setf fail value))) (loop until (eq exit t) do (setf (slot2 o1) (elt values (random (length values)))) (let ((value (slot2 o1))) (unless (member value values) (setf fail value) (setf exit t)))))))) (let ((r1 (bt:make-thread (make-runner '(:k1 :k2)))) (r2 (bt:make-thread (make-runner '(:k3 :k4)))) (r3 (bt:make-thread (make-runner '(:k5 :k6))))) (sleep .1) (setf exit t) (map nil #'bt:join-thread (list r1 r2 r3)) (is (eq (slot2 o1) :b)) (is (null fail)))))) Then let's test the dynamic variable itself: (test dynamic-let.4 "Test basic dvar operators." (let ((dvar (make-dynamic-variable))) (is (eql 42 (dset dvar 42))) (is (eql 42 (dref dvar))) (ignore-errors (dlet ((dvar :x)) (is (eql :x (dref dvar))) (error "foo"))) (is (eql 42 (dref dvar))))) (test dynamic-let.5 "Test bound-p operator." (let ((dvar (make-dynamic-variable))) (is (not (dynamic-variable-bound-p dvar))) (dset dvar 15) (is (dynamic-variable-bound-p dvar)) (dynamic-variable-makunbound dvar) (is (not (dynamic-variable-bound-p dvar))))) (test dynamic-let.6 "Test makunbound operator." (let ((dvar (make-dynamic-variable))) (dset dvar t) (is (dynamic-variable-bound-p dvar)) (finishes (dynamic-variable-makunbound dvar)) (is (not (dynamic-variable-bound-p dvar))))) (test dynamic-let.7 "Test locally bound-p operator." (let ((dvar (make-dynamic-variable))) (is (not (dynamic-variable-bound-p dvar))) (dlet ((dvar 15)) (is (dynamic-variable-bound-p dvar))) (is (not (dynamic-variable-bound-p dvar))))) (test dynamic-let.8 "Test locally unbound-p operator." (let ((dvar (make-dynamic-variable))) (dset dvar t) (is (dynamic-variable-bound-p dvar)) (dlet ((dvar nil)) (is (dynamic-variable-bound-p dvar)) (finishes (dynamic-variable-makunbound dvar)) (is (not (dynamic-variable-bound-p dvar)))) (is (dynamic-variable-bound-p dvar)))) (test dynamic-let.9 "Stress test the implementation (see :FAKE-PROGV-KLUDGE)." (finishes ; at the same time (let ((dvars (loop repeat 4096 collect (make-dynamic-variable)))) ;; ensure tls variable (loop for v in dvars do (dlet ((v 1)))) (loop for i from 0 below 4096 for r = (random 4096) for v1 in dvars for v2 = (elt dvars r) do (when (zerop (mod i 64)) (pass)) (dlet ((v1 42) (v2 43)) (values)))))) (test dynamic-let.0 "Stress test the implementation (see :FAKE-PROGV-KLUDGE)." (finishes ; can be gc-ed (loop for i from 0 below 4096 do (when (zerop (mod i 64)) (pass)) (dlet (((make-dynamic-variable) 42)) (values))))) All that is left is to test both dynamic variable implementations: BLOG/DLET> (lisp-implementation-type) "ECL" BLOG/DLET> (run-tests) Test DYNAMIC-LET.1... Passed. Test DYNAMIC-LET.2... Passed. Test DYNAMIC-LET.3... Passed. Test DYNAMIC-LET.4... Passed. Test DYNAMIC-LET.5... Passed. Test DYNAMIC-LET.6... Passed. Test DYNAMIC-LET.7... Passed. Test DYNAMIC-LET.8... Passed. Test DYNAMIC-LET.9... Passed. Test DYNAMIC-LET.0... Passed. NIL And with the kludge: BLOG/DLET> (lisp-implementation-type) "SBCL" BLOG/DLET> (run-tests) Test DYNAMIC-LET.1... Passed. Test DYNAMIC-LET.2... Passed. Test DYNAMIC-LET.3... Passed. Test DYNAMIC-LET.4... Passed. Test DYNAMIC-LET.5... Passed. Test DYNAMIC-LET.6... Passed. Test DYNAMIC-LET.7... Passed. Test DYNAMIC-LET.8... Passed. Test DYNAMIC-LET.9... Passed. Test DYNAMIC-LET.0... Passed. NIL Summary In this post we've made our implementation to work on SBCL even when there are more than a few thousand dynamic variables. We've also added a simple test suite that checks the basic behavior. As it often happens, after achieving some goal we get greedy and achieve more. That's the case here as well. In the next (and the last) post in this series I'll explore the idea of adding truly thread-local variables without a shared global value. This will be useful for lazily creating context on threads that are outside of our control. We'll also generalize the implementation so it is possible to subclass and implement ones own flavor of a dynamic variable. Full Article
w TurtleWare: Dynamic Vars - Return of the Jedi By turtleware.eu Published On :: Mon, 04 Nov 2024 00:00:00 GMT Table of Contents The protocol Control operators Synchronized hash tables with weakness First-class dynamic variables STANDARD-DYNAMIC-VARIABLE SURROGATE-DYNAMIC-VARIABLE Thread-local variables The protocol The implementation Thread-local slots What can we use it for? In the previous two posts I've presented an implementation of first-class dynamic variables using PROGV and a surrogate implementation for SBCL. Now we will double down on this idea and make the protocol extensible. Finally we'll implement a specialized version of dynamic variables where even the top level value of the variable is thread-local. The protocol Previously we've defined operators as either macros or functions. Different implementations were protected by the feature flag and symbols collided. Now we will introduce the protocol composed of a common superclass and functions that are specialized by particular implementations. Most notably we will introduce a new operator CALL-WITH-DYNAMIC-VARIABLE that is responsible for establishing a single binding. Thanks to that it will be possible to mix dynamic variables of different types within a single DLET statement. (defclass dynamic-variable () ()) (defgeneric dynamic-variable-bindings (dvar)) (defgeneric dynamic-variable-value (dvar)) (defgeneric (setf dynamic-variable-value) (value dvar)) (defgeneric dynamic-variable-bound-p (dvar)) (defgeneric dynamic-variable-makunbound (dvar)) (defgeneric call-with-dynamic-variable (cont dvar &optional value)) Moreover we'll define a constructor that is specializable by a key. This design will allow us to refer to the dynamic variable class by using a shorter name. We will also define the standard class to be used and an matching constructor. (defparameter *default-dynamic-variable-class* #-fake-progv-kludge 'standard-dynamic-variable #+fake-progv-kludge 'surrogate-dynamic-variable) (defgeneric make-dynamic-variable-using-key (key &rest initargs) (:method (class &rest initargs) (apply #'make-instance class initargs)) (:method ((class (eql t)) &rest initargs) (apply #'make-instance *default-dynamic-variable-class* initargs)) (:method ((class null) &rest initargs) (declare (ignore class initargs)) (error "Making a dynamic variable that is not, huh?"))) (defun make-dynamic-variable (&rest initargs) (apply #'make-dynamic-variable-using-key t initargs)) Control operators Control operators are the same as previously, that is a set of four macros that consume the protocol specified above. Note that DYNAMIC-VARIABLE-PROGV expands to a recursive call where each binding is processed separately. (defmacro dlet (bindings &body body) (flet ((pred (binding) (and (listp binding) (= 2 (length binding))))) (unless (every #'pred bindings) (error "DLET: bindings must be lists of two values.~%~ Invalid bindings:~%~{ ~s~%~}" (remove-if #'pred bindings)))) (loop for (var val) in bindings collect var into vars collect val into vals finally (return `(dynamic-variable-progv (list ,@vars) (list ,@vals) ,@body)))) (defmacro dset (&rest pairs) `(setf ,@(loop for (var val) on pairs by #'cddr collect `(dref ,var) collect val))) (defmacro dref (variable) `(dynamic-variable-value ,variable)) (defun call-with-dynamic-variable-progv (cont vars vals) (flet ((thunk () (if vals (call-with-dynamic-variable cont (car vars) (car vals)) (call-with-dynamic-variable cont (car vars))))) (if vars (call-with-dynamic-variable-progv #'thunk (cdr vars) (cdr vals)) (funcall cont)))) (defmacro dynamic-variable-progv (vars vals &body body) (let ((cont (gensym))) `(flet ((,cont () ,@body)) (call-with-dynamic-variable-progv (function ,cont) ,vars ,vals)))) Synchronized hash tables with weakness Previously we've used SBCL-specific options to define a synchronized hash table with weak keys. This won't do anymore, because we will need a similar object to implement the thread-local storage for top level values. trivial-garbage is a portability layer that allows to define hash tables with a specified weakness, but it does not provide an argument that would abstract away synchronization. We will ensure thread-safety with locks instead. (defclass tls-table () ((table :initform (trivial-garbage:make-weak-hash-table :test #'eq :weakness :key)) (lock :initform (bt:make-lock)))) (defun make-tls-table () (make-instance 'tls-table)) (defmacro with-tls-table ((var self) &body body) (let ((obj (gensym))) `(let* ((,obj ,self) (,var (slot-value ,obj 'table))) (bt:with-lock-held ((slot-value ,obj 'lock)) ,@body)))) First-class dynamic variables STANDARD-DYNAMIC-VARIABLE Previously in the default implementation we've represented dynamic variables with a symbol. The new implementation is similar except that the symbol is read from a STANDARD-OBJECT that represents the variable. This also enables us to specialize the function CALL-WITH-DYNAMIC-VARIABLE: (defclass standard-dynamic-variable (dynamic-variable) ((symbol :initform (gensym) :accessor dynamic-variable-bindings))) (defmethod dynamic-variable-value ((dvar standard-dynamic-variable)) (symbol-value (dynamic-variable-bindings dvar))) (defmethod (setf dynamic-variable-value) (value (dvar standard-dynamic-variable)) (setf (symbol-value (dynamic-variable-bindings dvar)) value)) (defmethod dynamic-variable-bound-p ((dvar standard-dynamic-variable)) (boundp (dynamic-variable-bindings dvar))) (defmethod dynamic-variable-makunbound ((dvar standard-dynamic-variable)) (makunbound (dynamic-variable-bindings dvar))) (defmethod call-with-dynamic-variable (cont (dvar standard-dynamic-variable) &optional (val nil val-p)) (progv (list (dynamic-variable-bindings dvar)) (if val-p (list val) ()) (funcall cont))) SURROGATE-DYNAMIC-VARIABLE The implementation of the SURROGATE-DYNAMIC-VARIABLE is almost the same as previously. The only difference is that we use the previously defined indirection to safely work with hash tables. Also note, that we are not add the feature condition - both classes is always created. (defvar +fake-unbound+ 'unbound) (defvar +cell-unbound+ '(no-binding)) (defclass surrogate-dynamic-variable (dynamic-variable) ((tls-table :initform (make-tls-table) :reader dynamic-variable-tls-table) (top-value :initform +fake-unbound+ :accessor dynamic-variable-top-value))) (defmethod dynamic-variable-bindings ((dvar surrogate-dynamic-variable)) (let ((process (bt:current-thread))) (with-tls-table (tls-table (dynamic-variable-tls-table dvar)) (gethash process tls-table +cell-unbound+)))) (defmethod (setf dynamic-variable-bindings) (value (dvar surrogate-dynamic-variable)) (let ((process (bt:current-thread))) (with-tls-table (tls-table (dynamic-variable-tls-table dvar)) (setf (gethash process tls-table) value)))) (defun %dynamic-variable-value (dvar) (let ((tls-binds (dynamic-variable-bindings dvar))) (if (eq tls-binds +cell-unbound+) (dynamic-variable-top-value dvar) (car tls-binds)))) (defmethod dynamic-variable-value ((dvar surrogate-dynamic-variable)) (let ((tls-value (%dynamic-variable-value dvar))) (when (eq tls-value +fake-unbound+) (error 'unbound-variable :name "(unnamed)")) tls-value)) (defmethod (setf dynamic-variable-value) (value (dvar surrogate-dynamic-variable)) (let ((tls-binds (dynamic-variable-bindings dvar))) (if (eq tls-binds +cell-unbound+) (setf (dynamic-variable-top-value dvar) value) (setf (car tls-binds) value)))) (defmethod dynamic-variable-bound-p ((dvar surrogate-dynamic-variable)) (not (eq +fake-unbound+ (%dynamic-variable-value dvar)))) (defmethod dynamic-variable-makunbound ((dvar surrogate-dynamic-variable)) (setf (dynamic-variable-value dvar) +fake-unbound+)) ;;; Apparently CCL likes to drop^Helide some writes and that corrupts bindings ;;; table. Let's ensure that the value is volatile. #+ccl (defvar *ccl-ensure-volatile* nil) (defmethod call-with-dynamic-variable (cont (dvar surrogate-dynamic-variable) &optional (val +fake-unbound+)) (push val (dynamic-variable-bindings dvar)) (let (#+ccl (*ccl-ensure-volatile* (dynamic-variable-bindings dvar))) (unwind-protect (funcall cont) (pop (dynamic-variable-bindings dvar))))) Thread-local variables We've refactored the previous code to be extensible. Now we can use metaobjects from the previous post without change. We can also test both implementations in the same process interchangeably by customizing the default class parameter. It is the time now to have some fun and extend dynamic variables into variables with top value not shared between different threads. This will enable ultimate thread safety. With our new protocol the implementation is trivial! The protocol First we will define the protocol class. THREAD-LOCAL-VARIABLE is a variant of a DYNAMIC-VARIABLE with thread-local top values. We specify initialization arguments :INITVAL and :INITFUN that will be used to assign the top value of a binding. The difference is that INITVAL specifies a single value, while INITFUN can produce an unique object on each invocation. INITARG takes a precedence over INTIFUN, and if neither is supplied, then a variable is unbound. We include the constructor that builds on MAKE-DYNAMIC-VARIABLE-USING-KEY, and macros corresponding to DEFVAR and DEFPARAMETER. Note that they expand to :INITFUN - this assures that the initialization form is re-evaluated for each new thread where the variable is used. (defclass thread-local-variable (dynamic-variable) ()) (defmethod initialize-instance :after ((self thread-local-variable) &key initfun initval) (declare (ignore self initfun initval))) (defparameter *default-thread-local-variable-class* #-fake-progv-kludge 'standard-thread-local-variable #+fake-progv-kludge 'surrogate-thread-local-variable) (defun make-thread-local-variable (&rest initargs) (apply #'make-dynamic-variable-using-key *default-thread-local-variable-class* initargs)) (defmacro create-tls-variable (&optional (form nil fp) &rest initargs) `(make-thread-local-variable ,@(when fp `(:initfun (lambda () ,form))) ,@initargs)) (defmacro define-tls-variable (name &rest initform-and-initargs) `(defvar ,name (create-tls-variable ,@initform-and-initargs))) (defmacro define-tls-parameter (name &rest initform-and-initargs) `(defparameter ,name (create-tls-variable ,@initform-and-initargs))) Perhaps it is a good time to introduce a new convention for tls variable names. I think that surrounding names with the minus sign is a nice idea, because it signifies, that it is something less than a global value. For example: DYNAMIC-VARS> (define-tls-variable -context- (progn (print "Initializing context!") (list :context))) -CONTEXT- DYNAMIC-VARS> -context- #<a EU.TURTLEWARE.DYNAMIC-VARS::STANDARD-THREAD-LOCAL-VARIABLE 0x7f7636c08640> DYNAMIC-VARS> (dref -context-) "Initializing context!" (:CONTEXT) DYNAMIC-VARS> (dref -context-) (:CONTEXT) DYNAMIC-VARS> (dset -context- :the-new-value) :THE-NEW-VALUE DYNAMIC-VARS> (dref -context-) :THE-NEW-VALUE DYNAMIC-VARS> (bt:make-thread (lambda () (print "Let's read it!") (print (dref -context-)))) #<process "Anonymous thread" 0x7f7637a26cc0> "Let's read it!" "Initializing context!" (:CONTEXT) DYNAMIC-VARS> (dref -context-) :THE-NEW-VALUE The implementation You might have noticed the inconspicuous operator DYNAMIC-VARIABLE-BINDINGS that is part of the protocol. It returns an opaque object that represents values of the dynamic variable in the current context: for STANDARD-DYNAMIC-VARIABLE it is a symbol for SURROGATE-DYNAMIC-VARIABLE it is a thread-local list of bindings In any case all other operators first take this object and then use it to read, write or bind the value. The gist of the tls variables implementation is to always return an object that is local to the thread. To store these objects we will use the tls-table we've defined earlier. (defclass thread-local-variable-mixin (dynamic-variable) ((tls-table :initform (make-tls-table) :reader dynamic-variable-tls-table) (tls-initfun :initarg :initfun :initform nil :accessor thread-local-variable-initfun) (tls-initval :initarg :initval :initform +fake-unbound+ :accessor thread-local-variable-initval))) For the class STANDARD-THREAD-LOCAL-VARIABLE we will simply return a different symbol depending on the thread: (defclass standard-thread-local-variable (thread-local-variable-mixin thread-local-variable standard-dynamic-variable) ()) (defmethod dynamic-variable-bindings ((tvar standard-thread-local-variable)) (flet ((make-new-tls-bindings () (let ((symbol (gensym)) (initval (thread-local-variable-initval tvar)) (initfun (thread-local-variable-initfun tvar))) (cond ((not (eq +fake-unbound+ initval)) (setf (symbol-value symbol) initval)) ((not (null initfun)) (setf (symbol-value symbol) (funcall initfun)))) symbol))) (let ((key (bt:current-thread))) (with-tls-table (tls-table (dynamic-variable-tls-table tvar)) (or (gethash key tls-table) (setf (gethash key tls-table) (make-new-tls-bindings))))))) And for the class SURROGATE-THREAD-LOCAL-VARIABLE the only difference from the SURROGATE-DYNAMIC-VARIABLE implementation is to cons a new list as the initial value (even when it is unbound) to ensure it is not EQ to +CELL-UNBOUND+. (defclass surrogate-thread-local-variable (thread-local-variable-mixin thread-local-variable surrogate-dynamic-variable) ()) (defmethod dynamic-variable-bindings ((tvar surrogate-thread-local-variable)) (flet ((make-new-tls-bindings () (let ((initval (thread-local-variable-initval tvar)) (initfun (thread-local-variable-initfun tvar))) (cond ((not (eq +fake-unbound+ initval)) (list initval)) ((not (null initfun)) (list (funcall initfun))) (t (list +fake-unbound+)))))) (let ((key (bt:current-thread))) (with-tls-table (tls-table (dynamic-variable-tls-table tvar)) (or (gethash key tls-table) (setf (gethash key tls-table) (make-new-tls-bindings))))))) That's all, now we have two implementations of thread-local variables. Ramifications are similar as with "ordinary" dynamic variables - the standard implementation is not advised for SBCL, because it will crash in LDB. Thread-local slots First we are going to allow to defined dynamic variable types with an abbreviated names. This will enable us to specify in the slot definition that type, for example (MY-SLOT :DYNAMIC :TLS :INITFORM 34) ;;; Examples how to add shorthand type names for the dynamic slots: (defmethod make-dynamic-variable-using-key ((key (eql :tls)) &rest initargs) (apply #'make-dynamic-variable-using-key *default-thread-local-variable-class* initargs)) (defmethod make-dynamic-variable-using-key ((key (eql :normal-tls)) &rest initargs) (apply #'make-dynamic-variable-using-key 'standard-thread-local-variable initargs)) (defmethod make-dynamic-variable-using-key ((key (eql :kludge-tls)) &rest initargs) (apply #'make-dynamic-variable-using-key 'surrogate-thread-local-variable initargs)) ;;; For *DEFAULT-DYNAMIC-VARIABLE* specify :DYNAMIC T. (defmethod make-dynamic-variable-using-key ((key (eql :normal-dyn)) &rest initargs) (apply #'make-dynamic-variable-using-key 'standard-dynamic-variable initargs)) (defmethod make-dynamic-variable-using-key ((key (eql :kludge-dyn)) &rest initargs) (apply #'make-dynamic-variable-using-key 'surrogate-dynamic-variable initargs)) In order to do that, we need to remember he value of the argument :DYNAMIC. We will read it with DYNAMIC-VARIABLE-TYPE and that value will be available in both direct and the effective slot: ;;; Slot definitions ;;; There is a considerable boilerplate involving customizing slots. ;;; ;;; - direct slot definition: local to a single defclass form ;;; ;;; - effective slot definition: combination of all direct slots with the same ;;; name in the class and its superclasses ;;; (defclass dynamic-direct-slot (mop:standard-direct-slot-definition) ((dynamic :initform nil :initarg :dynamic :reader dynamic-variable-type))) ;;; The metaobject protocol did not specify an elegant way to communicate ;;; between the direct slot definition and the effective slot definition. ;;; Luckily we have dynamic bindings! :-) (defvar *kludge/mop-deficiency/dynamic-variable-type* nil) ;;; DYNAMIC-EFFECTIVE-SLOT is implemented to return as slot-value values of the ;;; dynamic variable that is stored with the instance. ;;; ;;; It would be nice if we could specify :ALLOCATION :DYNAMIC for the slot, but ;;; then STANDARD-INSTANCE-ACCESS would go belly up. We could make a clever ;;; workaround, but who cares? (defclass dynamic-effective-slot (mop:standard-effective-slot-definition) ((dynamic :initform *kludge/mop-deficiency/dynamic-variable-type* :reader dynamic-variable-type))) Moreover we specialize the function MAKE-DYNAMIC-VARIABLE-USING-KEY to the effective slot class. The initargs in this method are meant for the instance. When the dynamic variable is created, we check whether it is a thread-local variable and initialize its INITVAL and INITFUN to values derived from INITARGS, MOP:SLOT-DEFINITION-INITARGS and MOP:SLOT-DEFINITION-INITFUN: (defmethod make-dynamic-variable-using-key ((key dynamic-effective-slot) &rest initargs) (let* ((dvar-type (dynamic-variable-type key)) (dvar (make-dynamic-variable-using-key dvar-type))) (when (typep dvar 'thread-local-variable) (loop with slot-initargs = (mop:slot-definition-initargs key) for (key val) on initargs by #'cddr when (member key slot-initargs) do (setf (thread-local-variable-initval dvar) val)) (setf (thread-local-variable-initfun dvar) (mop:slot-definition-initfunction key))) dvar)) The rest of the implementation of DYNAMIC-EFFECTIVE-SLOT is unchanged: (defmethod mop:slot-value-using-class ((class standard-class) object (slotd dynamic-effective-slot)) (dref (slot-dvar object slotd))) (defmethod (setf mop:slot-value-using-class) (new-value (class standard-class) object (slotd dynamic-effective-slot)) (dset (slot-dvar object slotd) new-value)) (defmethod mop:slot-boundp-using-class ((class standard-class) object (slotd dynamic-effective-slot)) (dynamic-variable-bound-p (slot-dvar object slotd))) (defmethod mop:slot-makunbound-using-class ((class standard-class) object (slotd dynamic-effective-slot)) (dynamic-variable-makunbound (slot-dvar object slotd))) The implementation of CLASS-WITH-DYNAMIC-SLOTS is also very similar. The first difference in that ALLOCATE-INSTANCE calls MAKE-DYNAMIC-VARIABLE-USING-KEY instead of MAKE-DYNAMIC-VARIABLE and supplies the effective slot definition as the key, and the instance initargs as the remaining arguments. Note that at this point initargs are already validated by MAKE-INSTANCE. The second difference is that MOP:COMPUTE-EFFECTIVE-SLOT-DEFINITION binds the flag *KLUDGE/MOP-DEFICIENCY/DYNAMIC-VARIABLE-TYPE* to DYNAMIC-VARIABLE-TYPE. ;;; This is a metaclass that allows defining dynamic slots that are bound with ;;; the operator SLOT-DLET, and, depending on the type, may have thread-local ;;; top value. ;;; ;;; The metaclass CLASS-WITH-DYNAMIC-SLOTS specifies alternative effective slot ;;; definitions for slots with an initarg :dynamic. (defclass class-with-dynamic-slots (standard-class) ()) ;;; Class with dynamic slots may be subclasses of the standard class. (defmethod mop:validate-superclass ((class class-with-dynamic-slots) (super standard-class)) t) ;;; When allocating the instance we initialize all slots to a fresh symbol that ;;; represents the dynamic variable. (defmethod allocate-instance ((class class-with-dynamic-slots) &rest initargs) (let ((object (call-next-method))) (loop for slotd in (mop:class-slots class) when (typep slotd 'dynamic-effective-slot) do (setf (mop:standard-instance-access object (mop:slot-definition-location slotd)) (apply #'make-dynamic-variable-using-key slotd initargs))) object)) ;;; To improve potential composability of CLASS-WITH-DYNAMIC-SLOTS with other ;;; metaclasses we treat specially only slots that has :DYNAMIC in initargs, ;;; otherwise we call the next method. (defmethod mop:direct-slot-definition-class ((class class-with-dynamic-slots) &rest initargs) (loop for (key) on initargs by #'cddr when (eq key :dynamic) do (return-from mop:direct-slot-definition-class (find-class 'dynamic-direct-slot))) (call-next-method)) (defmethod mop:compute-effective-slot-definition ((class class-with-dynamic-slots) name direct-slotds) (declare (ignore name)) (let ((latest-slotd (first direct-slotds))) (if (typep latest-slotd 'dynamic-direct-slot) (let ((*kludge/mop-deficiency/dynamic-variable-type* (dynamic-variable-type latest-slotd))) (call-next-method)) (call-next-method)))) (defmethod mop:effective-slot-definition-class ((class class-with-dynamic-slots) &rest initargs) (declare (ignore initargs)) (if *kludge/mop-deficiency/dynamic-variable-type* (find-class 'dynamic-effective-slot) (call-next-method))) Finally the implementation of SLOT-DLET does not change: ;;; Accessing and binding symbols behind the slot. We don't use SLOT-VALUE, ;;; because it will return the _value_ of the dynamic variable, and not the ;;; variable itself. (defun slot-dvar (object slotd) (check-type slotd dynamic-effective-slot) (mop:standard-instance-access object (mop:slot-definition-location slotd))) (defun slot-dvar* (object slot-name) (let* ((class (class-of object)) (slotd (find slot-name (mop:class-slots class) :key #'mop:slot-definition-name))) (slot-dvar object slotd))) (defmacro slot-dlet (bindings &body body) `(dlet ,(loop for ((object slot-name) val) in bindings collect `((slot-dvar* ,object ,slot-name) ,val)) ,@body)) Finally we can define a class with slots that do not share the top value: DYNAMIC-VARS> (defclass c1 () ((slot1 :initarg :slot1 :dynamic nil :accessor slot1) (slot2 :initarg :slot2 :dynamic t :accessor slot2) (slot3 :initarg :slot3 :dynamic :tls :accessor slot3)) (:metaclass class-with-dynamic-slots)) #<The EU.TURTLEWARE.DYNAMIC-VARS::CLASS-WITH-DYNAMIC-SLOTS EU.TURTLEWARE.DYNAMIC-VARS::C1> DYNAMIC-VARS> (with-slots (slot1 slot2 slot3) *object* (setf slot1 :x slot2 :y slot3 :z) (list slot1 slot2 slot3)) (:X :Y :Z) DYNAMIC-VARS> (bt:make-thread (lambda () (with-slots (slot1 slot2 slot3) *object* (setf slot1 :i slot2 :j slot3 :k) (print (list slot1 slot2 slot3))))) #<process "Anonymous thread" 0x7f76424c0240> (:I :J :K) DYNAMIC-VARS> (with-slots (slot1 slot2 slot3) *object* (list slot1 slot2 slot3)) (:I :J :Z) What can we use it for? Now that we know how to define thread-local variables, we are left with a question what can we use it for. Consider having a line-buffering stream. One possible implementation could be sketched as: (defclass line-buffering-stream (fancy-stream) ((current-line :initform (make-adjustable-string) :accessor current-line) (current-ink :initform +black+ :accessor current-ink))) (defmethod stream-write-char ((stream line-buffering-stream) char) (if (char= char # ewline) (terpri stream) (vector-push-extend char (current-line stream)))) (defmethod stream-terpri ((stream line-buffering-stream)) (%put-line-on-screen (current-line stream) (current-ink stream)) (setf (fill-pointer (current-line stream)) 0)) If this stream is shared between multiple threads, then even if individual operations and %PUT-LINE-ON-SCREEN are thread-safe , we have a problem. For example FORMAT writes are not usually atomic and individual lines are easily corrupted. If we use custom colors, these are also a subject of race conditions. The solution is as easy as making both slots thread-local. In that case the buffered line is private to each thread and it is put on the screen atomically: (defclass line-buffering-stream (fancy-stream) ((current-line :initform (make-adjustable-string) :accessor current-line :dynamic :tls) (current-ink :initform +black+ :accessor current-ink :dynamic :tls)) (:metaclass class-with-dynamic-slots)) Technique is not limited to streams. It may benefit thread-safe drawing, request processing, resource management and more. By subclassing DYNAMIC-VARIABLE we could create also variables that are local to different objects than processes. I hope that you've enjoyed reading this post as much as I had writing it. If you are interested in a full standalone implementation, with tests and system definitions, you may get it here. Cheers! Full Article
w One Week E-Book Sale of Vacuum Flowers!!! One Full Week!!! By floggingbabel.blogspot.com Published On :: Tue, 15 Oct 2024 21:14:00 +0000 . Open Road Media, which publishes several of my e-books, has announced a one-week reduction in price of Vacuum Flowers. Starting this October 18 and running through October 25, 2024, it will be available for $1.99. That's in the US only.So if you're an e-book reader and have been curious about my novel... well, there you are.And if you don't already know . . .Vacuum Flowers is what used to be called a Grand Tour of the Solar System. Rebel Elizabeth Mudlark is operating off of stolen wetware and on the run from very dangerous people. She arrives in the inner system on a cometary orbit, which takes her through a great variety of human and post-human societies, including the most dangerous one of all--Earth.That bit about the cometary orbit is not incidental. Comets enter the Inner System on either a hyperbolic or a parabolic orbit. The one is open, the other closed. I knew that the book would end with Rebel Elizabeth Mudlark standing in the stardocks with a coffin at her feet. But I didn't know if the person within the coffin would be alive or dead or if REM would someday return to the Inner System or was leaving it forever. I only decided that when I came to write the last page.* Full Article
w Halcyon Afternoon and Why I Wrote It By floggingbabel.blogspot.com Published On :: Thu, 17 Oct 2024 17:37:00 +0000 .This is a particularly pleasant time for me. To begin with, I'm at the World Fantasy Convention, where I'll see a lot of old friends and serve as toastmaster. And I have two new Mongolian Wizard stories coming out (I apologize for the delay), one today and one tomorrow at Reactor Magazine.Today's story, "Halcyon Afternoon," is atypical for the series. It's not as violent as these stories tend to be. That's because I felt that after undergoing so much suffering and loss, Franz-Karl Ritter deserved at least one afternoon of contentment and bliss. Even his wolf, Freki, got the day off.Of course... Ritter's luck being what it is, and all of Europe being entangled in a wizard war, the afternoon would not prove entirely blissful.You can read the story here. Or you can find the entire series of Mongolian Wizard stories--ten so far--here. Or you can simply go to Reactor Magazine and wander about happily. It's full of great stories and terrific non-fiction.And tomorrow . . .Ritter's luck takes a downturn--along with everyone else's--in "Dragons of Paris." An old friend pops up, a relationship turns difficult, and a battle where victory seems certain goes sour.(And now, I'm off to the convention!)Above: Illustration by Dave Palumbo. It's not only beautiful but true to the story. I'm grateful for both of those.* Full Article
w "Dragons of Paris" and the Role of Time in the Mongolian Wizard Series By floggingbabel.blogspot.com Published On :: Fri, 18 Oct 2024 23:20:00 +0000 .The kind people at Reactor Magazine have posted my two latest Mongolian Wizard stories, one yesterday and the other today. Thursday's "Halcyon Afternoon" took place during a rare moment of peace for Franz-Karl Ritter. But in today's "Dragons of Paris," it's warfare as usual. Time has always been a little tricky in this series. The first story was clearly set in the Nineteenth Century but, though only a few years have passed, the series has now reached what is recognizably World War I. Mostly this occurred for reasons explained in "The Phantom in the Maze" and "Murder in the Spook House." (And which I anticipate giving me increasing difficulties in writing the next ten stories.) But also, in a more literary background sense, I wanted to cover the transition from a way of life now alien to us to something more modern, if not contemporary. So time may get a bit more slippery in the future. That's if, of course, the stories go in the direction I intend. Sometimes the fiction has its own ideas where it wants to go and the author can only follow along meekly in its wake.You can read the story here. Or just go to the ezine and poke around. It's a good place to poke around.Above: The illustration is by Dave Palumbo. I'm grateful for that.* Full Article
w My Halloween Season Story, "Unquiet Graves," in CLARKESWORLD By floggingbabel.blogspot.com Published On :: Fri, 01 Nov 2024 20:24:00 +0000 . I am always happiest when a story of mine comes into print. Today, I have the joy of introducing you to "Unquiet Graves," a seasonal tale of graveyard misbehavior and betrayal. Oh, and there's nothing supernatural about it at all.You can read the story here. But if you're like me, you'll just go to Clarkesworld, look over the table of contents, and decide which story you want to read first. Mine by preference, but follow your whim. And for those who like trivia . . .I came up with the handheld's app many long years ago and it took forever to come up with a story for it. You'll notice that it is left unnamed in the story. That's because its secret name was "The Graveyard Reader." Which is the title of a well-known story by Theodore Sturgeon. While I was writing the story, I thought of it as "The New Graveyard Reader." But Sturgeon's story and mine go off in totally different directions, and giving mine (or even the app) a title suggesting there was some implicit connection between the two would only cause confusion.The title I finally came up with was derived from "The Unquiet Grave" by that most prolific of all poets, Anonymous. If you look it up, I suggest you do so after reading my story. It gives away some of the plot.* Full Article
w One-Day E-Book Sale of Vacuum Flowers By floggingbabel.blogspot.com Published On :: Tue, 12 Nov 2024 15:01:00 +0000 .Once again, one of my e-books will briefly be on sale! Vacuum Flowers will be available in the US for only $1.99. Here's the news from Open Road Media:Hello,We are pleased to let you know that the following ebook(s) will be featured in price promotions soon.ISBN13TitleAuthorPromo TypeCountryStart DateEnd DatePromo Price9781504036504Vacuum FlowersSwanwick, MichaelORM - Portalist NLUS2024-11-132024-11-13$1.99Open Road will promote the feature via social media. We hope you can share the deal with your network as well. You can subscribe to the newsletters at the links below so that you will get the direct link to the deal on the day that it appears.NewsletterLink Early Bird Books Subscribe Now The LineupSubscribe NowThe PortalistSubscribe NowMurder & MayhemSubscribe NowA Love So TrueSubscribe NowThe ArchiveSubscribe NowThe ReaderSubscribe NowPlease let us know if you have any questions. We are thrilled to be part of this promotion; hope you are too!Best,The Open Road Editorial TeamAnd because you've probably wondered . . .I've been asked this many times, but the answer is no: I don't have a nude drawing of Gardner Dozois hanging in my living room. It's in the upstairs hallway. Anyway, he's wearing a sheet, so much of him is covered.Robert Walters posed Gardner as the evil genius Jonaman for one of the illos (back when SF magazines had illustrations) that went with the serialization of Vacuum Flowers in Asimov's, way back when.It's not the sightliest picture. But it is treasured.* Full Article
w Who owns Greenland rsquo s island By satwcomic.com Published On :: Who owns Greenland rsquo s island View Comic! Full Article
w Popular destinations in Sweden By satwcomic.com Published On :: Popular destinations in Sweden View Comic! Full Article
w Don 039 t mess with Acorns By satwcomic.com Published On :: Don 039 t mess with Acorns View Comic! Full Article
w When you hate your friend rsquo s new friend By satwcomic.com Published On :: When you hate your friend rsquo s new friend View Comic! Full Article
w Why Virat Kohli, Jasprit Bumrah were missing from Perth nets; India ramp up privacy amid Manchester United-like security - Hindustan Times By news.google.com Published On :: Wed, 13 Nov 2024 04:53:31 GMT Why Virat Kohli, Jasprit Bumrah were missing from Perth nets; India ramp up privacy amid Manchester United-like security Hindustan TimesVirat Kohli in focus: Intense net session begins for upcoming Test series against Australia The Times of IndiaVirat Kohli in Australia for BGT: A timeline India TodayBlack veil of secrecy: India begin training in privacy in Perth ESPNcricinfoIndia to play intra-squad warm-up match at WACA on Friday ahead of Australia Tests but BCCI denies public viewing Hindustan Times Full Article
w Eight injured in explosion at oil refinery in UP's Mathura - ANI News By news.google.com Published On :: Tue, 12 Nov 2024 23:00:00 GMT Eight injured in explosion at oil refinery in UP's Mathura ANI News8 injured, three critical in major explosion at Indian Oil plant in UP's Mathura India TodayEight injured in fire at IndianOil’s Mathura refinery BusinessLineMathura refinery blast: Indian Oil refinery explosion injures 12 people The Financial ExpressAt least 8 injured in explosion at Mathura refinery The Indian Express Full Article
w 3 Children, 3 Women Missing After 10 Suspected Kuki Militants Killed In Encounter In Manipur's Jiribam - NDTV By news.google.com Published On :: Tue, 12 Nov 2024 11:11:01 GMT 3 Children, 3 Women Missing After 10 Suspected Kuki Militants Killed In Encounter In Manipur's Jiribam NDTVManipur on boil: 2 more bodies found, 6 missing The Times of IndiaAdditional paramilitary forces rushed to Manipur amid spike in ethnic violence Hindustan TimesLetters to The Editor — November 13, 2024 The Hindu2 men found dead, 6 of family missing day after militants killed in Manipur India Today Full Article
w he was a good man By www.marriedtothesea.com Published On :: Sun, 09 Oct 2022 04:00:00 EDT Today on Married To The Sea: he was a good manThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w hey man like water By www.marriedtothesea.com Published On :: Sun, 16 Oct 2022 04:00:00 EDT Today on Married To The Sea: hey man like waterThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w you ever wonder By www.marriedtothesea.com Published On :: Sun, 27 Nov 2022 04:00:00 EST Today on Married To The Sea: you ever wonderThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w they want us By www.marriedtothesea.com Published On :: Thu, 01 Dec 2022 04:00:00 EST Today on Married To The Sea: they want usThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w check will bounce By www.marriedtothesea.com Published On :: Thu, 08 Dec 2022 04:00:00 EST Today on Married To The Sea: check will bounceThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w a what a horse By www.marriedtothesea.com Published On :: Sun, 18 Dec 2022 04:00:00 EST Today on Married To The Sea: a what a horseThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w all the way up the hill By www.marriedtothesea.com Published On :: Sun, 08 Jan 2023 04:00:00 EST Today on Married To The Sea: all the way up the hillThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w im allowed to speak By www.marriedtothesea.com Published On :: Sun, 05 Feb 2023 04:00:00 EST Today on Married To The Sea: im allowed to speakThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w why you should be happy By www.marriedtothesea.com Published On :: Wed, 15 Feb 2023 04:00:00 EST Today on Married To The Sea: why you should be happyThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w what happened to my sleepy pants By www.marriedtothesea.com Published On :: Wed, 29 Mar 2023 04:00:00 EDT Today on Married To The Sea: what happened to my sleepy pantsThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w what do we do now By www.marriedtothesea.com Published On :: Wed, 26 Apr 2023 04:00:00 EDT Today on Married To The Sea: what do we do nowThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w america must outlaw children By www.marriedtothesea.com Published On :: Wed, 17 May 2023 04:00:00 EDT Today on Married To The Sea: america must outlaw childrenThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w i wont stand for this By www.marriedtothesea.com Published On :: Wed, 31 May 2023 04:00:00 EDT Today on Married To The Sea: i wont stand for thisThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w w0w b1g f0rtune By www.marriedtothesea.com Published On :: Wed, 07 Jun 2023 04:00:00 EDT Today on Married To The Sea: w0w b1g f0rtuneThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w owns a phone By www.marriedtothesea.com Published On :: Wed, 27 Sep 2023 04:00:00 EDT Today on Married To The Sea: owns a phoneThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w nonrenewable resource By www.marriedtothesea.com Published On :: Wed, 04 Oct 2023 04:00:00 EDT Today on Married To The Sea: nonrenewable resourceThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w dont show me your damn emotions By www.marriedtothesea.com Published On :: Wed, 18 Oct 2023 04:00:00 EDT Today on Married To The Sea: dont show me your damn emotionsThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w wheed smoke By www.marriedtothesea.com Published On :: Wed, 25 Oct 2023 04:00:00 EDT Today on Married To The Sea: wheed smokeThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w old growth grow back By www.marriedtothesea.com Published On :: Wed, 15 Nov 2023 04:00:00 EST Today on Married To The Sea: old growth grow backThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w u would hit me By www.marriedtothesea.com Published On :: Wed, 29 Nov 2023 04:00:00 EST Today on Married To The Sea: u would hit meThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w running this show By www.marriedtothesea.com Published On :: Wed, 06 Dec 2023 04:00:00 EST Today on Married To The Sea: running this showThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w i heard what you said earlier By www.marriedtothesea.com Published On :: Wed, 13 Dec 2023 04:00:00 EST Today on Married To The Sea: i heard what you said earlierThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w i know what youre thinkin By www.marriedtothesea.com Published On :: Wed, 20 Dec 2023 04:00:00 EST Today on Married To The Sea: i know what youre thinkinThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w watch you sled By www.marriedtothesea.com Published On :: Wed, 24 Jan 2024 04:00:00 EST Today on Married To The Sea: watch you sledThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w mom were home By www.marriedtothesea.com Published On :: Wed, 31 Jan 2024 04:00:00 EST Today on Married To The Sea: mom were homeThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w i swear to god lobsters By www.marriedtothesea.com Published On :: Wed, 07 Feb 2024 04:00:00 EST Today on Married To The Sea: i swear to god lobstersThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic
w outta my way By www.marriedtothesea.com Published On :: Wed, 21 Feb 2024 04:00:00 EST Today on Married To The Sea: outta my wayThis RSS feed is brought to you by Drew and Natalie's podcast Garbage Brain University. Our new series Everything Is Real explores the world of cryptids, aliens, quantum physics, the occult, and more. If you use this RSS feed, please consider supporting us by becoming a patron. Patronage includes membership to our private Discord server and other bonus material non-patrons never see! Full Article autogen_comic