在 lisp 中执行期间的函数重定义
Function redefinitions during execution in lisp
假设我们有两个函数 fct1
和 fct2
:
fct1
呼叫 fct2
,
fct1
将应用程序中的某些对象 O1
设置为状态 A
,
fct2
将应用程序中的某些对象 O2
设置为状态 B
.
让我们假设以下约束必须始终为真:
- (
01
处于 A
状态并且 02
处于 B
状态),
- XOR(
01
处于 not(A)
状态并且 02
处于 not(B)
状态)。
如果 在 到 fct1
的通话期间会发生什么:
- 重新定义:
fct1
现在将某些对象 01
设置为状态 not(A)
,
- 重新定义:
fct2
现在将某些对象 02
设置为状态 not(B)
。
是否可以通过将 01
设置为状态 A
并将 02
设置为状态 not(B)
来 "breaks" 约束?
我找到了这个答案:
If a recursive function redefines itself, the recursive calls still to be made in the same invocation may keep going to the same body.
[...]
More generally, Common Lisp allows compilers to generate efficient calls among functions that are in the same file. So you normally have to think of replacement of running code as being at the module level rather than individual function level. If functions A and B are in the same module, and A calls B, then if you merely replace B without replacing A, A may continue calling the old B (because B was inlined into A, or because A doesn't go through the symbol, but uses a more direct address for B). You can declare functions notinline to suppress this.
我的问题是:
- 是否会出现这种现象(即
01
设置为A
状态,02
设置为not(B)
状态)?它有名字吗?
如果"yes":
- 是否依赖于实现?
- 有没有办法强制执行正确的行为,例如通过内联函数?
- 我可以使用什么工具来测试函数是否以正确的方式工作?测试似乎很痛苦:我不知道如何在不改变基本源代码的情况下测试重定义。
- 如何检测我的代码中可能出现此问题的部分?
这是一个示例,说明您所描述的情况在多线程环境中是如何发生的:
(progn
(defun f2 (o2)
(setf (car o2) :b))
(defun f1 (o1 o2)
(setf (car o1) :a)
;; the sleep here is to increase the risk of data-race
(sleep 3)
(f2 o2))
;; call the functions in a separate thread
(sb-thread:make-thread
(lambda ()
(let ((o1 (list 0))
(o2 (list 0)))
(f1 o1 o2)
(print (list o1 o2)))))
;; in parallel, redefine f2, then f1
(defun f2 (o2)
(setf (car o2) :not-b))
(defun f1 (o1 o2)
(setf (car o1) :not-a)
(f2 o2)))
3 秒后,REPL 打印
((:A) (:NOT-B))
如果在定义f2
之前加上(declaim (inline f2))
再测试,那么oldf2
的代码还是从old f1
,在线程内部,3 秒后打印以下内容:
((:A) (:B))
进一步调用更新函数 f1
得到:
((:NOT-A) (:NOT-B))
What tool(s) can I use to test if the functions work the correct way ? It seems a pain to test : I have no idea how to test redefinitions without altering the base source code.
也许您正在使用新代码更新 运行 服务器,并且您希望避免服务器在您加载定义时使用函数的部分重新定义。
与基础架构的所有其他方面一样,重要的是提前计划如何可靠地进行备份和更新(数据库、配置等)。
一种可能的方法是逐包更新。你可以在你的包后加上版本号:
(in-package :web-0 ...)
(defun f2 () ...)
(defun f1 () ...)
;; new version
(in-package :web-1 ...)
(defun f2 () ...)
(defun f1 () ...)
当需要更新时,您可以编译和加载 :web-1
的代码,而不会干扰 运行 的代码。那么您应该能够更改调用者以使用新的实现:
;; somewhere in the main server package
(handle-request (request)
(web-0:handle request))
;; update it
(handle-request (request)
(web-1:handle request))
它甚至可以在没有版本号的情况下工作,如果你先删除包然后用相同的名称重新创建它,但是你不能轻易恢复。
有些地方可能还需要全局锁,您必须在应用程序中和更新期间管理它。
假设我们有两个函数 fct1
和 fct2
:
fct1
呼叫fct2
,fct1
将应用程序中的某些对象O1
设置为状态A
,fct2
将应用程序中的某些对象O2
设置为状态B
.
让我们假设以下约束必须始终为真:
- (
01
处于A
状态并且02
处于B
状态), - XOR(
01
处于not(A)
状态并且02
处于not(B)
状态)。
如果 在 到 fct1
的通话期间会发生什么:
- 重新定义:
fct1
现在将某些对象01
设置为状态not(A)
, - 重新定义:
fct2
现在将某些对象02
设置为状态not(B)
。
是否可以通过将 01
设置为状态 A
并将 02
设置为状态 not(B)
来 "breaks" 约束?
我找到了这个答案:
If a recursive function redefines itself, the recursive calls still to be made in the same invocation may keep going to the same body. [...] More generally, Common Lisp allows compilers to generate efficient calls among functions that are in the same file. So you normally have to think of replacement of running code as being at the module level rather than individual function level. If functions A and B are in the same module, and A calls B, then if you merely replace B without replacing A, A may continue calling the old B (because B was inlined into A, or because A doesn't go through the symbol, but uses a more direct address for B). You can declare functions notinline to suppress this.
我的问题是:
- 是否会出现这种现象(即
01
设置为A
状态,02
设置为not(B)
状态)?它有名字吗?
如果"yes":
- 是否依赖于实现?
- 有没有办法强制执行正确的行为,例如通过内联函数?
- 我可以使用什么工具来测试函数是否以正确的方式工作?测试似乎很痛苦:我不知道如何在不改变基本源代码的情况下测试重定义。
- 如何检测我的代码中可能出现此问题的部分?
这是一个示例,说明您所描述的情况在多线程环境中是如何发生的:
(progn
(defun f2 (o2)
(setf (car o2) :b))
(defun f1 (o1 o2)
(setf (car o1) :a)
;; the sleep here is to increase the risk of data-race
(sleep 3)
(f2 o2))
;; call the functions in a separate thread
(sb-thread:make-thread
(lambda ()
(let ((o1 (list 0))
(o2 (list 0)))
(f1 o1 o2)
(print (list o1 o2)))))
;; in parallel, redefine f2, then f1
(defun f2 (o2)
(setf (car o2) :not-b))
(defun f1 (o1 o2)
(setf (car o1) :not-a)
(f2 o2)))
3 秒后,REPL 打印
((:A) (:NOT-B))
如果在定义f2
之前加上(declaim (inline f2))
再测试,那么oldf2
的代码还是从old f1
,在线程内部,3 秒后打印以下内容:
((:A) (:B))
进一步调用更新函数 f1
得到:
((:NOT-A) (:NOT-B))
What tool(s) can I use to test if the functions work the correct way ? It seems a pain to test : I have no idea how to test redefinitions without altering the base source code.
也许您正在使用新代码更新 运行 服务器,并且您希望避免服务器在您加载定义时使用函数的部分重新定义。
与基础架构的所有其他方面一样,重要的是提前计划如何可靠地进行备份和更新(数据库、配置等)。
一种可能的方法是逐包更新。你可以在你的包后加上版本号:
(in-package :web-0 ...)
(defun f2 () ...)
(defun f1 () ...)
;; new version
(in-package :web-1 ...)
(defun f2 () ...)
(defun f1 () ...)
当需要更新时,您可以编译和加载 :web-1
的代码,而不会干扰 运行 的代码。那么您应该能够更改调用者以使用新的实现:
;; somewhere in the main server package
(handle-request (request)
(web-0:handle request))
;; update it
(handle-request (request)
(web-1:handle request))
它甚至可以在没有版本号的情况下工作,如果你先删除包然后用相同的名称重新创建它,但是你不能轻易恢复。
有些地方可能还需要全局锁,您必须在应用程序中和更新期间管理它。