summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Uko Kokņevičs2025-10-19 10:12:04 +0300
committerGravatar Uko Kokņevičs2025-10-19 10:12:04 +0300
commit81e69b85c8bcd9ab8da04c01bc30aaea129b7ff9 (patch)
treeea9bcc2f1465f6695f3c6062c0a8edd922b8f117
parentWork on launching scripts a bit (diff)
downloadukkoclot-81e69b85c8bcd9ab8da04c01bc30aaea129b7ff9.tar.gz
ukkoclot-81e69b85c8bcd9ab8da04c01bc30aaea129b7ff9.tar.xz
ukkoclot-81e69b85c8bcd9ab8da04c01bc30aaea129b7ff9.zip
Make the R/W locks somewhat more fair
Diffstat (limited to '')
-rw-r--r--.gitignore1
-rw-r--r--.pre-commit-config.yaml5
-rw-r--r--README.md2
-rw-r--r--REUSE.toml2
-rw-r--r--TODO.org4
-rwxr-xr-xlaunch.sh2
-rw-r--r--src/rw-lock.lisp71
-rw-r--r--test/rw-lock.lisp26
8 files changed, 76 insertions, 37 deletions
diff --git a/.gitignore b/.gitignore
index 074e3ae..a551058 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
2/data.db 2/data.db
3/ocicl 3/ocicl
4*.fasl 4*.fasl
5*~
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e2f455c..f30fd61 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,11 +9,6 @@ repos:
9 entry: ocicl lint 9 entry: ocicl lint
10 pass_filenames: true 10 pass_filenames: true
11 files: \.(lisp|asd)$ 11 files: \.(lisp|asd)$
12 - id: run-testsuite
13 name: run-testsuite
14 language: system
15 entry: ./run-tests.sh
16 pass_filenames: false
17- repo: https://github.com/fsfe/reuse-tool 12- repo: https://github.com/fsfe/reuse-tool
18 rev: v6.1.2 13 rev: v6.1.2
19 hooks: 14 hooks:
diff --git a/README.md b/README.md
index 22acd03..725382b 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
2 2
3A shitty small telegram bot written in Common Lisp. 3A shitty small telegram bot written in Common Lisp.
4 4
5For running the (very few) tests and other repository maintaining things, run `./run_tests.sh`. For having an example 5For running the (very few) tests and other repository maintaining things, run `./run-tests.sh`. For having an example
6way how to launch, see `./launch.sh`. 6way how to launch, see `./launch.sh`.
7 7
8# Licensing 8# Licensing
diff --git a/REUSE.toml b/REUSE.toml
index d2a4142..9d2b746 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -9,7 +9,7 @@ version = 1
9 9
10[[annotations]] 10[[annotations]]
11 label = "Docs or something" 11 label = "Docs or something"
12 path = ["README.md"] 12 path = ["README.md", "TODO.org"]
13 SPDX-License-Identifier = "EUPL-1.2" 13 SPDX-License-Identifier = "EUPL-1.2"
14 SPDX-FileCopyrightText = " 2025 Uko Kokņevičs <perkontevs@gmail.com>" 14 SPDX-FileCopyrightText = " 2025 Uko Kokņevičs <perkontevs@gmail.com>"
15 15
diff --git a/TODO.org b/TODO.org
new file mode 100644
index 0000000..d37a824
--- /dev/null
+++ b/TODO.org
@@ -0,0 +1,4 @@
1* TODO See if I can rely on alexandria more
2* TODO See if I can rely on uiop more
3* TODO See if I can rely on serapeum more
4* TODO Use function-cache to memoize stuff like bot-id, bot-username, get-my-name etc
diff --git a/launch.sh b/launch.sh
index 795db47..f0cd9a9 100755
--- a/launch.sh
+++ b/launch.sh
@@ -5,7 +5,7 @@
5set -eu 5set -eu
6 6
7# Note this still has the debugger and such things enabled, at this point it makes life easier for me :). For a more 7# Note this still has the debugger and such things enabled, at this point it makes life easier for me :). For a more
8# hands-off auto-restarting approach, consider disabling all the fancy SBCL things (similar to ./run_tests.sh) and 8# hands-off auto-restarting approach, consider disabling all the fancy SBCL things (similar to ./run-tests.sh) and
9# putting it in a shell while true loop. 9# putting it in a shell while true loop.
10 10
11exec sbcl \ 11exec sbcl \
diff --git a/src/rw-lock.lisp b/src/rw-lock.lisp
index bd2606c..dc8850d 100644
--- a/src/rw-lock.lisp
+++ b/src/rw-lock.lisp
@@ -3,7 +3,7 @@
3(defpackage :ukkoclot/src/rw-lock 3(defpackage :ukkoclot/src/rw-lock
4 (:documentation "Implementation of a shared/read-write lock.") 4 (:documentation "Implementation of a shared/read-write lock.")
5 (:use :c2cl :bt2 :iterate) 5 (:use :c2cl :bt2 :iterate)
6 (:import-from :alexandria :with-gensyms) 6 (:import-from :alexandria :whichever :with-gensyms)
7 (:import-from :com.dieggsy.f-string :enable-f-strings) 7 (:import-from :com.dieggsy.f-string :enable-f-strings)
8 (:import-from :sb-sys 8 (:import-from :sb-sys
9 :allow-with-interrupts :with-local-interrupts :without-interrupts) 9 :allow-with-interrupts :with-local-interrupts :without-interrupts)
@@ -22,20 +22,22 @@
22(eval-when (:compile-toplevel :load-toplevel :execute) 22(eval-when (:compile-toplevel :load-toplevel :execute)
23 (enable-f-strings)) 23 (enable-f-strings))
24 24
25;; TODO: Get rid of broadcasting for writers, instead have separate condvars
26;; TODO: Make it a bit more fair, 50% chance of waking up writers or readers
27;; TODO: Use atomic-integer in best-case for read locks to decrease contention 25;; TODO: Use atomic-integer in best-case for read locks to decrease contention
28(defstruct (rw-lock (:constructor make-rw-lock%)) 26(defstruct (rw-lock (:constructor make-rw-lock%))
29 (lock nil :type lock :read-only t) 27 (lock nil :type lock :read-only t)
30 (unlocked nil :type condition-variable :read-only t) 28 (reader-cv nil :type condition-variable :read-only t)
31 (readers 0 :type integer) 29 (writer-cv nil :type condition-variable :read-only t)
32 (has-writer nil :type boolean)) 30 (waiting-readers 0 :type (integer 0))
31 (waiting-writers 0 :type (integer 0))
32 (active-readers 0 :type (integer 0))
33 (active-writer nil :type boolean))
33 34
34(defvar *counter-lock* (make-lock :name "rw-lock::*counter-lock*")) 35(defvar *counter-lock* (make-lock :name "rw-lock::*counter-lock*"))
35 36
36(defvar *counter* 0) 37(defvar *counter* 0)
37 38
38(defun gen-name () 39(defun gen-name ()
40 "Generate a name for a rw-lock"
39 (format nil "Read-Write Lock ~A" 41 (format nil "Read-Write Lock ~A"
40 (with-lock-held (*counter-lock*) 42 (with-lock-held (*counter-lock*)
41 (incf *counter*)))) 43 (incf *counter*))))
@@ -44,38 +46,51 @@
44 (check-type name string) 46 (check-type name string)
45 (make-rw-lock% 47 (make-rw-lock%
46 :lock (make-lock :name #f"{name}'s internal lock") 48 :lock (make-lock :name #f"{name}'s internal lock")
47 :unlocked (make-condition-variable :name #f"{name}'s internal unlocked"))) 49 :reader-cv (make-condition-variable :name #f"{name}'s internal reader-cv")
50 :writer-cv (make-condition-variable :name #f"{name}'s internal writer-cv")))
51
52(defun wakeup-waiters (rw-lock)
53 ;; NOTE: RW-LOCK-LOCK HAS TO BE TAKEN!
54 (declare (type rw-lock rw-lock))
55 (with-slots (reader-cv writer-cv waiting-readers waiting-writers) rw-lock
56 (cond
57 ((zerop waiting-readers) (condition-notify writer-cv))
58 ((zerop waiting-writers) (condition-broadcast reader-cv))
59 (t (whichever (condition-notify writer-cv)
60 (condition-broadcast reader-cv))))))
48 61
49(defun acquire-read-lock (rw-lock &key (wait t)) 62(defun acquire-read-lock (rw-lock &key (wait t))
50 ;; TODO: timeout 63 ;; TODO: timeout
51 (check-type rw-lock rw-lock) 64 (check-type rw-lock rw-lock)
52 (with-slots (lock unlocked readers has-writer) rw-lock 65 (with-slots (lock reader-cv active-readers active-writer waiting-readers) rw-lock
53 (let ((lock-acquired)) 66 (let ((lock-acquired))
54 (without-interrupts 67 (without-interrupts
55 (unwind-protect 68 (unwind-protect
56 (when (setf lock-acquired 69 (when (setf lock-acquired
57 (allow-with-interrupts 70 (allow-with-interrupts
58 (acquire-lock lock :wait wait))) 71 (acquire-lock lock :wait wait)))
72 (incf waiting-readers)
59 (with-local-interrupts 73 (with-local-interrupts
60 (when (if wait 74 (when (if wait
61 (iter 75 (iter
62 (unless has-writer 76 (unless active-writer
63 (return t)) 77 (return t))
64 (condition-wait unlocked lock)) 78 (condition-wait reader-cv lock))
65 (not has-writer)) 79 (not active-writer))
66 (incf readers) 80 (incf active-readers)
67 t))) 81 t)))
68 (when lock-acquired 82 (when lock-acquired
83 (decf waiting-readers)
69 (release-lock lock))))))) 84 (release-lock lock)))))))
70 85
71(defun release-read-lock (rw-lock) 86(defun release-read-lock (rw-lock)
72 (check-type rw-lock rw-lock) 87 (check-type rw-lock rw-lock)
73 (with-slots (lock unlocked readers has-writer) rw-lock 88 (with-slots (lock active-readers active-writer) rw-lock
74 (with-lock-held (lock) 89 (with-lock-held (lock)
75 (assert (> readers 0) nil "Trying to release a read lock that's not taken!") 90 (assert (> active-readers 0) nil "Trying to release a read lock that's not taken!")
76 (decf readers) 91 (decf active-readers)
77 (when (zerop readers) 92 (when (zerop active-readers)
78 (condition-broadcast unlocked)))) 93 (wakeup-waiters rw-lock))))
79 rw-lock) 94 rw-lock)
80 95
81(defmacro with-read-lock ((rw-lock) &body body) 96(defmacro with-read-lock ((rw-lock) &body body)
@@ -95,32 +110,34 @@
95(defun acquire-write-lock (rw-lock &key (wait t)) 110(defun acquire-write-lock (rw-lock &key (wait t))
96 ;; TODO: timeout 111 ;; TODO: timeout
97 (check-type rw-lock rw-lock) 112 (check-type rw-lock rw-lock)
98 (with-slots (lock unlocked readers has-writer) rw-lock 113 (with-slots (lock writer-cv active-readers active-writer waiting-writers) rw-lock
99 (let ((lock-acquired)) 114 (let ((lock-acquired))
100 (without-interrupts 115 (without-interrupts
101 (unwind-protect 116 (unwind-protect
102 (when (setf lock-acquired 117 (when (setf lock-acquired
103 (allow-with-interrupts 118 (allow-with-interrupts
104 (acquire-lock lock :wait wait))) 119 (acquire-lock lock :wait wait)))
120 (incf waiting-writers)
105 (with-local-interrupts 121 (with-local-interrupts
106 (when (if wait 122 (when (if wait
107 (iter 123 (iter
108 (unless (or (> readers 0) has-writer) 124 (unless (or (> active-readers 0) active-writer)
109 (return t)) 125 (return t))
110 (condition-wait unlocked lock)) 126 (condition-wait writer-cv lock))
111 (and (zerop readers) (not has-writer))) 127 (and (zerop active-readers) (not active-writer)))
112 (setf has-writer t) 128 (setf active-writer t)
113 t))) 129 t)))
114 (when lock-acquired 130 (when lock-acquired
131 (decf waiting-writers)
115 (release-lock lock))))))) 132 (release-lock lock)))))))
116 133
117(defun release-write-lock (rw-lock) 134(defun release-write-lock (rw-lock)
118 (check-type rw-lock rw-lock) 135 (check-type rw-lock rw-lock)
119 (with-slots (lock unlocked readers has-writer) rw-lock 136 (with-slots (lock active-readers active-writer) rw-lock
120 (with-lock-held (lock) 137 (with-lock-held (lock)
121 (assert has-writer nil "Trying to release a write lock that's not taken!") 138 (assert active-writer nil "Trying to release a write lock that's not taken!")
122 (setf has-writer nil) 139 (setf active-writer nil)
123 (condition-broadcast unlocked))) 140 (wakeup-waiters rw-lock)))
124 rw-lock) 141 rw-lock)
125 142
126(defmacro with-write-lock ((rw-lock) &body body) 143(defmacro with-write-lock ((rw-lock) &body body)
diff --git a/test/rw-lock.lisp b/test/rw-lock.lisp
index fdb60bd..834d0fd 100644
--- a/test/rw-lock.lisp
+++ b/test/rw-lock.lisp
@@ -2,8 +2,8 @@
2;; SPDX-FileCopyrightText: 2025 Uko Kokņevičs <perkontevs@gmail.com> 2;; SPDX-FileCopyrightText: 2025 Uko Kokņevičs <perkontevs@gmail.com>
3(defpackage :ukkoclot/test/rw-lock 3(defpackage :ukkoclot/test/rw-lock
4 (:documentation "Test-suite for :ukkoclot/src/rw-lock.") 4 (:documentation "Test-suite for :ukkoclot/src/rw-lock.")
5 (:use :c2cl :fiveam :ukkoclot/src/rw-lock) 5 (:use :c2cl :fiveam :iterate :ukkoclot/src/rw-lock)
6 (:import-from :bt2 :with-timeout)) 6 (:import-from :bt2 :join-thread :make-thread :with-timeout))
7(in-package :ukkoclot/test/rw-lock) 7(in-package :ukkoclot/test/rw-lock)
8 8
9(def-suite :ukkoclot/rw-lock) 9(def-suite :ukkoclot/rw-lock)
@@ -69,6 +69,28 @@
69 (is-false (acquire-read-lock lock :wait nil)) 69 (is-false (acquire-read-lock lock :wait nil))
70 (is (rw-lock-p (release-write-lock lock))))) 70 (is (rw-lock-p (release-write-lock lock)))))
71 71
72(test read&write-locks.contention
73 (let ((lock (make-rw-lock))
74 (value 0)
75 reader-threads
76 writer-threads)
77 (flet ((reader-fn () (with-read-lock (lock)
78 (sleep (random 0.1))
79 value))
80 (writer-fn () (with-write-lock (lock)
81 (incf value)
82 (sleep (random 0.1))
83 (incf value))))
84 (iter (repeat 100)
85 (push (make-thread #'reader-fn) reader-threads)
86 (sleep (random 0.02))
87 (push (make-thread #'writer-fn) writer-threads)))
88 (iter (for writer-thread in writer-threads)
89 (join-thread writer-thread))
90 (is (every #'evenp
91 (iter (for reader-thread in reader-threads)
92 (collect (join-thread reader-thread)))))))
93
72(test with-read-lock.simple 94(test with-read-lock.simple
73 (let ((lock (make-rw-lock))) 95 (let ((lock (make-rw-lock)))
74 (with-read-lock (lock) 96 (with-read-lock (lock)