Project

General

Profile

Actions

Bug #5350

closed

WeakRef で謎の NoMethodError

Added by metanest (Makoto Kishimoto) over 12 years ago. Updated about 11 years ago.

Status:
Closed
Target version:
ruby -v:
-
Backport:
[ruby-dev:<unknown>]

Description

添付のようなスクリプトを r18232 以降の ruby 1.9 で走らせると、以下のように
RefError ではなく NoMethodError が上がってくる、ということが起きます。
( value メソッドを呼ぶ直前に weakref_alive? を呼んでみると true が
返っています )

r18220 以前では正常に動きます( r18221 ~ r18231 では core を吐きます)。

ruby 1.9.0 (2008-07-27 revision 0) [x86_64-freebsd8.2]
../weakref_bug.rb:20:in part': undefined method value' for [17188951200]:WeakRef (NoMethodError)
from ../weakref_bug.rb:29:in block in part' from ../weakref_bug.rb:27:in downto'
from ../weakref_bug.rb:27:in each' from ../weakref_bug.rb:27:in part'
from ../weakref_bug.rb:29:in block in part' from ../weakref_bug.rb:27:in downto'
from ../weakref_bug.rb:27:in each' from ../weakref_bug.rb:27:in part'
from ../weakref_bug.rb:38:in block in <main>' from ../weakref_bug.rb:36:in each'
from ../weakref_bug.rb:36:in `'

ruby 1.9.4dev (2011-07-18 trunk 32577) [x86_64-freebsd8.2]
../weakref_bug.rb:20:in part': undefined method value' for "../weakref_bug.rb:36:in <main>'":WeakRef (NoMethodError) from ../weakref_bug.rb:29:in block in part'
from ../weakref_bug.rb:27:in downto' from ../weakref_bug.rb:27:in each'
from ../weakref_bug.rb:27:in part' from ../weakref_bug.rb:29:in block in part'
from ../weakref_bug.rb:27:in downto' from ../weakref_bug.rb:27:in each'
from ../weakref_bug.rb:27:in part' from ../weakref_bug.rb:29:in block in part'
from ../weakref_bug.rb:27:in downto' from ../weakref_bug.rb:27:in each'
from ../weakref_bug.rb:27:in part' from ../weakref_bug.rb:29:in block in part'
from ../weakref_bug.rb:27:in downto' from ../weakref_bug.rb:27:in each'
from ../weakref_bug.rb:27:in part' from ../weakref_bug.rb:29:in block in part'
from ../weakref_bug.rb:27:in downto' from ../weakref_bug.rb:27:in each'
from ../weakref_bug.rb:27:in part' from ../weakref_bug.rb:38:in block in '
from ../weakref_bug.rb:36:in each' from ../weakref_bug.rb:36:in '

/home/ksmakoto/ruby-working/ruby-weakref/lib/ruby/1.9.0/weakref.rb:58: [BUG] gc_sweep(): unknown data type 0x0(0x801019088)
ruby 1.9.0 (2008-07-25 revision 0) [x86_64-freebsd8.2]

-- control frame ----------
c:0023 p:---- s:0072 b:0072 l:000071 d:000071 CFUNC :_id2ref
c:0022 p:0069 s:0068 b:0068 l:000067 d:000067 METHOD /home/ksmakoto/ruby-working/ruby-weakref/lib/ruby/1.9.0/weakref.rb:58
c:0021 p:0007 s:0065 b:0065 l:000064 d:000064 METHOD /home/ksmakoto/ruby-working/ruby-weakref/lib/ruby/1.9.0/delegate.rb:139
c:0020 p:---- s:0058 b:0058 l:000057 d:000057 FINISH :eql?
c:0019 p:0041 s:0056 b:0055 l:000054 d:000054 METHOD ../weakref_bug.rb:20
c:0018 p:0032 s:0048 b:0047 l:000036 d:000046 BLOCK ../weakref_bug.rb:29
c:0017 p:---- s:0046 b:0046 l:000045 d:000045 FINISH :==
c:0016 p:---- s:0044 b:0044 l:000039 d:000043 IFUNC :==
c:0015 p:---- s:0042 b:0042 l:000041 d:000041 CFUNC :downto
c:0014 p:---- s:0040 b:0040 l:000039 d:000039 CFUNC :each
c:0013 p:0074 s:0037 b:0037 l:000036 d:000036 METHOD ../weakref_bug.rb:27
c:0012 p:0032 s:0030 b:0029 l:000018 d:000028 BLOCK ../weakref_bug.rb:29
c:0011 p:---- s:0028 b:0028 l:000027 d:000027 FINISH :==
c:0010 p:---- s:0026 b:0026 l:000021 d:000025 IFUNC :==
c:0009 p:---- s:0024 b:0024 l:000023 d:000023 CFUNC :downto
c:0008 p:---- s:0022 b:0022 l:000021 d:000021 CFUNC :each
c:0007 p:0074 s:0019 b:0019 l:000018 d:000018 METHOD ../weakref_bug.rb:27
c:0006 p:0013 s:0012 b:0012 l:000005 d:000011 BLOCK ../weakref_bug.rb:38
c:0005 p:---- s:0011 b:0011 l:000010 d:000010 FINISH :method_added
c:0004 p:---- s:0009 b:0009 l:000008 d:000008 CFUNC :each
c:0003 p:0047 s:0006 b:0006 l:000005 d:000005 TOP ../weakref_bug.rb:36
c:0002 p:---- s:0004 b:0004 l:000003 d:000003 FINISH :private_class_method
c:0001 p:0000 s:0002 b:0002 l:000001 d:000001 TOP

/home/ksmakoto/ruby-working/ruby-weakref/lib/ruby/1.9.0/weakref.rb:58: [BUG] object allocation during garbage collection phase
ruby 1.9.0 (2008-07-25 revision 0) [x86_64-freebsd8.2]

-- control frame ----------
c:0023 p:---- s:0072 b:0072 l:000071 d:000071 CFUNC :_id2ref
(上と同じなので省略)
c:0001 p:0000 s:0002 b:0002 l:000001 d:000001 TOP

DBG> : "/home/ksmakoto/ruby-working/ruby-weakref/lib/ruby/1.9.0/weakref.rb:58:in _id2ref'" DBG> : "/home/ksmakoto/ruby-working/ruby-weakref/lib/ruby/1.9.0/weakref.rb:58:in getobj'"
DBG> : "/home/ksmakoto/ruby-working/ruby-weakref/lib/ruby/1.9.0/delegate.rb:139:in method_missing'" DBG> : "../weakref_bug.rb:20:in part'"
DBG> : "../weakref_bug.rb:29:in block in part'" DBG> : "../weakref_bug.rb:27:in downto'"
DBG> : "../weakref_bug.rb:27:in each'" DBG> : "../weakref_bug.rb:27:in part'"
DBG> : "../weakref_bug.rb:29:in block in part'" DBG> : "../weakref_bug.rb:27:in downto'"
DBG> : "../weakref_bug.rb:27:in each'" DBG> : "../weakref_bug.rb:27:in part'"
DBG> : "../weakref_bug.rb:38:in block in <main>'" DBG> : "../weakref_bug.rb:36:in each'"
DBG> : "../weakref_bug.rb:36:in `'"
Abort trap: 6 (core dumped)


Files

weakref_bug.rb (583 Bytes) weakref_bug.rb metanest (Makoto Kishimoto), 09/22/2011 06:48 AM
patch1.diff (932 Bytes) patch1.diff Glass_saga (Masaki Matsushita), 09/23/2011 11:43 PM
patch2.diff (1.55 KB) patch2.diff Glass_saga (Masaki Matsushita), 09/23/2011 11:43 PM

Subtasks 1 (0 open1 closed)

Bug #5439: r33361以降sample/test.rb:systemがFになるClosed10/13/2011Actions
Actions #1

Updated by Glass_saga (Masaki Matsushita) over 12 years ago

=begin
もう少しシンプルなコードにしてみました。
大量のWeakRefオブジェクトを生成すると、たまに正しく参照できない事があるようです。

require "weakref"

class Foo
def hoge; end
end

TIMES = 100000

A = []
TIMES.times do
A.push WeakRef.new Foo.new
end

A.each do |x|
begin
x.hoge
rescue WeakRef::RefError
end
end

正しく参照できなかった場合には、参照先のobject_idに20を足したFixnumが格納されたArrayや、caller、callerの一部と思われるStringなどが返ります。
=end

Updated by sorah (Sorah Fukumori) over 12 years ago

調査結果を報告します.

再現コードさらに短縮--

require "weakref"

class Foo
def initialize
@hoge = "fuga"
end

attr_reader :hoge
end

A = Array.new(5000) do
WeakRef.new Foo.new
end
GC.start

A.each do |x|
begin
x.hoge
rescue WeakRef::RefError
end
end

--

で,軽く調べまわって見たところ

WeakRefの@@finalの中の@@mutex.synchronizeで何故かdeadlock; recursive locking例外が発生していました.
その所為でNoMethodErrorが起きるWeakRefの対象のオブジェクトがGCされていたとしても@@mutex.synchronize内の処理
で死亡したことがHashに書き込まれないため,WeakRef#weakref_alive?がtrueのままである感じです.

その場合はRangeErrorのほうでひっかかっても良いんじゃないのかなーと思うのですが,何故ファイナライザー中のsynchronizeで
recursive lockingになるのかを調査した所,rb_gc_finalize_deferred() が関係してるのかなあ.うーん,よくわからない.

#20 0x0000000100059610 in rb_protect (proc=0x1000776c0 <run_single_final>, data=140734799778632, state=0x7fff5fbf9364) at eval.c:709
#21 0x00000001000778db in run_finalizer (objspace=0x10081a800, objid=4303620561, table=4303620400) at gc.c:2919
#22 0x0000000100077a2f in run_final (objspace=0x10081a800, obj=4303620560) at gc.c:2947
#23 0x0000000100074844 in finalize_list (objspace=0x10081a800, p=0x1008409d0) at gc.c:1937
#24 0x0000000100077a8c in finalize_deferred (objspace=0x10081a800) at gc.c:2959
#25 0x0000000100077abd in rb_gc_finalize_deferred () at gc.c:2966
#26 0x000000010021370d in rb_threadptr_execute_interrupts_common (th=0x100401db0) at thread.c:1311
#27 0x0000000100213858 in rb_threadptr_execute_interrupts (th=0x100401db0) at thread.c:1335
#28 0x00000001001fc2f2 in vm_call_method (th=0x100401db0, cfp=0x1005ffbe0, num=2, blockptr=0x0, flag=0, id=337, me=0x100449970, recv=4303690840) at vm_insnhelper.c:676
#29 0x00000001001f39f8 in vm_exec_core (th=0x100401db0, initial=0) at insns.def:1015
#30 0x000000010020a58b in vm_exec (th=0x100401db0) at vm.c:1220

Updated by sorah (Sorah Fukumori) over 12 years ago

a = Object.new
mutex = Mutex.new
ObjectSpace.define_finalizer(a) {
begin
mutex.synchronize { p "hi" }
rescue Exception => e
p e
end
}
mutex.synchronize {
a = nil
GC.stress = true
loop{ Object.new }
}

--

依存関係を無くしてみました.例外が発生しているのが確認できると思います.
(finalizer中の例外で終了することは無いみたいなので手動でrescue → Kernel#pしています.)

Updated by sorah (Sorah Fukumori) over 12 years ago

書こうと思っていて忘れたので追記.

どうやらmutex.synchronize中に何かの拍子でfinalizerが走り,そのfinalizerの中で同じmutexをsynchronizeすると起こってしまう模様.

そしてさっき古い再現しない再現コードを載せてしまいました… 再現するコードはこちらです.

a = Object.new
mutex = Mutex.new
ObjectSpace.define_finalizer(a) {
mutex.synchronize { p "ho" }
}
mutex.synchronize {
a = nil
GC.start
p "hi"
loop{ Object.new }
}

Updated by Glass_saga (Masaki Matsushita) over 12 years ago

@@final内でのMutex#synchronizeをやめる(添付のpatch1.diff)か、Mutexのロック前にGC.disableして解放後にenableする(patch2)かすれば再現しなくなりました。

Updated by sorah (Sorah Fukumori) over 12 years ago

両方共その場しのぎ,症状に対して防止策をしているだけだと思うのでこの2つのパッチはあまり根本的な解決にはならないと思います.

Updated by sorah (Sorah Fukumori) over 12 years ago

test-allは通ったけど,はたしてこれでいいんだろうか.

diff --git a/thread.c b/thread.c
index 10b73eb..de63c3a 100644
--- a/thread.c
+++ b/thread.c
@@ -1307,7 +1307,7 @@ rb_threadptr_execute_interrupts_common(rb_thread_t *th)
}
th->status = status;

  •   if (finalizer_interrupt) {
    
  •   if (finalizer_interrupt && !th->keeping_mutexes) {
          rb_gc_finalize_deferred();
      }
    

Updated by ko1 (Koichi Sasada) over 12 years ago

  • ruby -v changed from ruby 1.9.4dev (2011-07-18 trunk 32577) [x86_64-freebsd8.2] to -

(2011/09/23 17:44), Shota Fukumori wrote:

test-allは通ったけど,はたしてこれでいいんだろうか.

 良くないです.Mutex の保持と finalizer の実行は関係ありません.

このコードだけ見て反応しているので,元の問題は見ておらず,

そちらの解決策を示していなくてすみません.

--
// SASADA Koichi at atdot dot net

Updated by ko1 (Koichi Sasada) over 12 years ago

 ささだです.

(2011/09/23 18:47), Shota Fukumori (sora_h) wrote:

元の問題がややこしくなっているので一旦まとめると,

a = Object.new
mutex = Mutex.new # finalizer と共有している mutex

ObjectSpace.define_finalizer(a) { # (1) a に対する finalizer
mutex.synchronize { p "ho" } # (2) ここでロックしようとするが, (3) と
# 同じスレッドなので ThreadError 例外発生
# (例外の発生は rescue で確認可能)
}
mutex.synchronize { # (3)
a = nil
GC.start
p "hi"
loop{ Object.new } # このループ中で (1) で定義した finalizer が呼ばれる
}

このようなサンプルコードを実行したときに (3) の mutex.synchronize 中に
オブジェクト a が解放された後,何らかのタイミングで a に対する finalizer
(1) が実行されると, (3) で mutex がロックされたまま同じスレッド (?) で
再び (2) で Mutex#lock を試みるため ThreadError "dead lock; recursive
locking" (thread.c:3555) が発生しています.

で,この問題に対する解決策は

  1. これは仕様という事にしてしまう
    → これが採用された場合,Glass_saga の GC.disable と GC.enable を挟む
    パッチ
    を取り込んで解決の方向?
  2. その他何らかの良い方法

 問題のまとめをありがとうございます.

 ファイナライザの実行は,何時起こるかわからないものなので,デッドロック
の可能性がある処理を行うのは,プログラムが悪い,ということになります.基
本的には,デッドロックを起こさないように書き直す必要がありますが,例えば
上記の場合では,mutex.trylock を利用することで回避することができます.

本質的に,何かしら他の対処が必要な話だろうか?

--
// SASADA Koichi at atdot dot net

Updated by ko1 (Koichi Sasada) over 12 years ago

(2011/09/23 18:58), SASADA Koichi wrote:

 ファイナライザの実行は,何時起こるかわからないものなので,デッドロック
の可能性がある処理を行うのは,プログラムが悪い,ということになります.基
本的には,デッドロックを起こさないように書き直す必要がありますが,例えば
上記の場合では,mutex.trylock を利用することで回避することができます.

 すみません,trylock ではダメですね.

 うーん,これはどうするべきかな.ファイナライズ処理を遅延させるように
コードを書き換えれば解決できますが,ちょっと大がかりな気もしますね.問題
が weakref だけなら,大がかりでもいい気がしますが.

 この問題は,pthread_mutex* で管理している状況をシグナルハンドラでどう
処理するか,みたいなのに似ていると思うのですが,そもそも pthread_mutex*
はシグナルセーフじゃないから使えないんだよな.

--
// SASADA Koichi at atdot dot net

Updated by ko1 (Koichi Sasada) over 12 years ago

(2011/09/23 19:02), SASADA Koichi wrote:

 うーん,これはどうするべきかな.ファイナライズ処理を遅延させるように
コードを書き換えれば解決できますが,ちょっと大がかりな気もしますね.問題
が weakref だけなら,大がかりでもいい気がしますが.

 わかりづらい文章になってしまってすみません.weakref 側を,ファイナライ
ザ処理を遅延できるように大がかりに書き換えればよい,という意図でした.例
えば,ファイナライザはこの処理をファイナライザの 後で 実行するように,
例えば Thread 作っちゃうとかすれば解決できます.

 weakref のコードを見ていないので,どのように解決するのがスマートかわか
りませんが,現状ですと「単に weakref のバグです」としか言えないんじゃな
いかと思います.ただ,Ruby 自体に「こういう機能があるともっと綺麗に書け
る」といった話に発展する可能性は否定しません.

--
// SASADA Koichi at atdot dot net

Updated by sorah (Sorah Fukumori) over 12 years ago

ささださんがパッチを書いてくれました.
(許可を得て)MLに転載しておきます.

手元では再現しなくなりましたが, ([ruby-dev:44525] の再現コードを使用)
どうでしょうか.

diff --git a/lib/weakref.rb b/lib/weakref.rb
index ee5444a..f3e669b 100644
--- a/lib/weakref.rb
+++ b/lib/weakref.rb
@@ -28,7 +28,7 @@ class WeakRef < Delegator
@@id_rev_map = {} # ref -> obj
@@mutex = Mutex.new
@@final = lambda {|id|

  • @@mutex.synchronize {
  • pr = lambda{
    rids = @@id_map[id]
    if rids
    for rid in rids
    @@ -43,6 +43,19 @@ class WeakRef < Delegator
    @@id_map.delete(rid) if @@id_map[rid].empty?
    end
    }
  • if @@mutex.try_lock
  •  begin
    
  •    pr.call
    
  •  ensure
    
  •    @@mutex.unlock
    
  •  end
    
  • else
  •  Thread.new{
    
  •    @@mutex.synchronize{
    
  •      pr.call
    
  •    }
    
  •  }
    
  • end
    }

Updated by nobu (Nobuyoshi Nakada) over 12 years ago

なかだです。

At Sat, 24 Sep 2011 11:38:54 +0900,
SASADA Koichi wrote in [ruby-dev:44538]:

(2011/09/23 19:02), SASADA Koichi wrote:

 うーん,これはどうするべきかな.ファイナライズ処理を遅延させるように
コードを書き換えれば解決できますが,ちょっと大がかりな気もしますね.問題
が weakref だけなら,大がかりでもいい気がしますが.

 わかりづらい文章になってしまってすみません.weakref 側を,ファイナライ
ザ処理を遅延できるように大がかりに書き換えればよい,という意図でした.例
えば,ファイナライザはこの処理をファイナライザの 後で 実行するように,
例えば Thread 作っちゃうとかすれば解決できます.

weakrefのマップからの削除処理は、ファイナライザから遅延させてはまずいで
す。削除が完了する前にファイナライザが終了してしまうと、対象のオブジェ
クトは再利用される可能性があり、その時点でweakrefから参照すると意図しな
いオブジェクトが得られることになります。

第一の問題点は、weakrefのファイナライズ処理が再入不能なのに、ファイナラ
イザ自体は再入することがあり得ることです。[ruby-dev:44527]にある再現コー
ドのように、weakrefのファイナライズ処理で内部的に使っているMutexが外部
からも容易にアクセスできてしまうことも問題といっていいでしょう。

とりあえずファイナライザで再入しないようにするパッチです。


diff --git i/gc.c w/gc.c
index f9a945c..fad49e0 100644
--- i/gc.c
+++ w/gc.c
@@ -345,6 +345,7 @@ typedef struct rb_objspace {
 	int dont_gc;
 	int dont_lazy_sweep;
 	int during_gc;
+	rb_atomic_t finalizing;
     } flags;
     struct {
 	st_table *table;
@@ -387,6 +388,7 @@ int *ruby_initial_gc_stress_ptr = &rb_objspace.gc_stress;
 #define heaps_freed		objspace->heap.freed
 #define dont_gc 		objspace->flags.dont_gc
 #define during_gc		objspace->flags.during_gc
+#define finalizing		objspace->flags.finalizing
 #define finalizer_table 	objspace->final.table
 #define deferred_final_list	objspace->final.deferred
 #define mark_stack		objspace->markstack.buffer
@@ -2064,7 +2066,7 @@ slot_sweep(rb_objspace_t *objspace, struct heaps_slot *sweep_slot)
     }
     objspace->heap.final_num += final_num;
 
-    if (deferred_final_list) {
+    if (deferred_final_list && !finalizing) {
         rb_thread_t *th = GET_THREAD();
         if (th) {
             RUBY_VM_SET_FINALIZER_INTERRUPT(th);
@@ -2968,7 +2970,10 @@ finalize_deferred(rb_objspace_t *objspace)
 void
 rb_gc_finalize_deferred(void)
 {
-    finalize_deferred(&rb_objspace);
+    rb_objspace_t *objspace = &rb_objspace;
+    if (ATOMIC_SET(finalizing, 1)) return;
+    finalize_deferred(objspace);
+    ATOMIC_SET(finalizing, 0);
 }
 
 static int
@@ -3020,6 +3025,8 @@ rb_objspace_call_finalizer(rb_objspace_t *objspace)
     /* run finalizers */
     gc_clear_mark_on_sweep_slots(objspace);
 
+    if (ATOMIC_SET(finalizing, 1)) return;
+
     do {
 	/* XXX: this loop will make no sense */
 	/* because mark will not be removed */
@@ -3082,6 +3089,7 @@ rb_objspace_call_finalizer(rb_objspace_t *objspace)
 
     st_free_table(finalizer_table);
     finalizer_table = 0;
+    ATOMIC_SET(finalizing, 0);
 }
 
 void
@@ -3089,7 +3097,7 @@ rb_gc(void)
 {
     rb_objspace_t *objspace = &rb_objspace;
     garbage_collect(objspace);
-    finalize_deferred(objspace);
+    if (!finalizing) finalize_deferred(objspace);
     free_unused_heaps(objspace);
 }
 

--
--- 僕の前にBugはない。
--- 僕の後ろにBugはできる。
中田 伸悦

Updated by nobu (Nobuyoshi Nakada) over 12 years ago

なかだです。

Thu, 29 Sep 2011 20:48:38 +0900,
Nobuyoshi Nakada wrote in [ruby-dev:44562]:

第一の問題点は、weakrefのファイナライズ処理が再入不能なのに、ファイナラ
イザ自体は再入することがあり得ることです。[ruby-dev:44527]にある再現コー
ドのように、weakrefのファイナライズ処理で内部的に使っているMutexが外部
からも容易にアクセスできてしまうことも問題といっていいでしょう。

ObjectSpace::WeakMapを追加してそれを使うようにしたパッチです。


diff --git i/gc.c w/gc.c
index fad49e0..ba0cac4 100644
--- i/gc.c
+++ w/gc.c
@@ -402,6 +402,9 @@ int *ruby_initial_gc_stress_ptr = &rb_objspace.gc_stress;
 #define nonspecial_obj_id(obj) (VALUE)((SIGNED_VALUE)(obj)|FIXNUM_FLAG)
 
 static void rb_objspace_call_finalizer(rb_objspace_t *objspace);
+static VALUE define_final0(VALUE obj, VALUE block);
+VALUE rb_define_final(VALUE obj, VALUE block);
+VALUE rb_undefine_final(VALUE obj);
 
 #if defined(ENABLE_VM_OBJSPACE) && ENABLE_VM_OBJSPACE
 rb_objspace_t *
@@ -2829,6 +2832,12 @@ os_each_obj(int argc, VALUE *argv, VALUE os)
 static VALUE
 undefine_final(VALUE os, VALUE obj)
 {
+    return rb_undefine_final(obj);
+}
+
+VALUE
+rb_undefine_final(VALUE obj)
+{
     rb_objspace_t *objspace = &rb_objspace;
     st_data_t data = obj;
     rb_check_frozen(obj);
@@ -2849,9 +2858,7 @@ undefine_final(VALUE os, VALUE obj)
 static VALUE
 define_final(int argc, VALUE *argv, VALUE os)
 {
-    rb_objspace_t *objspace = &rb_objspace;
-    VALUE obj, block, table;
-    st_data_t data;
+    VALUE obj, block;
 
     rb_scan_args(argc, argv, "11", &obj, &block);
     rb_check_frozen(obj);
@@ -2862,6 +2869,16 @@ define_final(int argc, VALUE *argv, VALUE os)
 	rb_raise(rb_eArgError, "wrong type argument %s (should be callable)",
 		 rb_obj_classname(block));
     }
+    return define_final0(obj, block);
+}
+
+static VALUE
+define_final0(VALUE obj, VALUE block)
+{
+    rb_objspace_t *objspace = &rb_objspace;
+    VALUE table;
+    st_data_t data;
+
     if (!FL_ABLE(obj)) {
 	rb_raise(rb_eArgError, "cannot define finalizer for %s",
 		 rb_obj_classname(obj));
@@ -2883,6 +2900,17 @@ define_final(int argc, VALUE *argv, VALUE os)
     return block;
 }
 
+VALUE
+rb_define_final(VALUE obj, VALUE block)
+{
+    rb_check_frozen(obj);
+    if (!rb_respond_to(block, rb_intern("call"))) {
+	rb_raise(rb_eArgError, "wrong type argument %s (should be callable)",
+		 rb_obj_classname(block));
+    }
+    return define_final0(obj, block);
+}
+
 void
 rb_gc_copy_finalizer(VALUE dest, VALUE obj)
 {
@@ -3656,6 +3684,157 @@ gc_profile_total_time(VALUE self)
  *  See also GC.count, GC.malloc_allocated_size and GC.malloc_allocations
  */
 
+struct weakmap {
+    st_table *obj2wmap;		/* obj -> [ref,...] */
+    st_table *wmap2obj;		/* ref -> obj */
+    VALUE final;
+};
+
+static int
+wmap_mark_map(st_data_t key, st_data_t val, st_data_t arg)
+{
+    FL_SET((VALUE)val, FL_MARK);
+    return ST_CONTINUE;
+}
+
+static void
+wmap_mark(void *ptr)
+{
+    struct weakmap *w = ptr;
+    st_foreach(w->obj2wmap, wmap_mark_map, 0);
+    rb_gc_mark(w->final);
+}
+
+static int
+wmap_free_map(st_data_t key, st_data_t val, st_data_t arg)
+{
+    rb_ary_resize((VALUE)val, 0);
+    return ST_CONTINUE;
+}
+
+static void
+wmap_free(void *ptr)
+{
+    struct weakmap *w = ptr;
+    st_foreach(w->obj2wmap, wmap_free_map, 0);
+    st_clear(w->obj2wmap);
+    st_clear(w->wmap2obj);
+}
+
+size_t rb_ary_memsize(VALUE ary);
+static int
+wmap_memsize_map(st_data_t key, st_data_t val, st_data_t arg)
+{
+    *(size_t *)arg += rb_ary_memsize((VALUE)val);
+    return ST_CONTINUE;
+}
+
+static size_t
+wmap_memsize(const void *ptr)
+{
+    size_t size;
+    const struct weakmap *w = ptr;
+    if (!w) return 0;
+    size = sizeof(*w);
+    size += st_memsize(w->obj2wmap);
+    size += st_memsize(w->wmap2obj);
+    st_foreach(w->obj2wmap, wmap_memsize_map, (st_data_t)&size);
+    return size;
+}
+
+static const rb_data_type_t weakmap_type = {
+    "weakmap",
+    {
+	wmap_mark,
+	wmap_free,
+	wmap_memsize,
+    }
+};
+
+static VALUE
+wmap_allocate(VALUE klass)
+{
+    struct weakmap *w;
+    VALUE obj = TypedData_Make_Struct(klass, struct weakmap, &weakmap_type, w);
+    w->obj2wmap = st_init_numtable();
+    w->wmap2obj = st_init_numtable();
+    w->final = rb_obj_method(obj, ID2SYM(rb_intern("finalize")));
+    return obj;
+}
+
+static VALUE
+wmap_finalize(VALUE self, VALUE obj)
+{
+    st_data_t data;
+    VALUE rids;
+    long i;
+    struct weakmap *w;
+
+    TypedData_Get_Struct(self, struct weakmap, &weakmap_type, w);
+    obj = NUM2PTR(obj);
+
+    data = (st_data_t)obj;
+    if (st_delete(w->obj2wmap, &data, &data)) {
+	rids = (VALUE)data;
+	for (i = 0; i < RARRAY_LEN(rids); ++i) {
+	    data = (st_data_t)RARRAY_PTR(rids)[i];
+	    st_delete(w->wmap2obj, &data, NULL);
+	}
+    }
+
+    data = (st_data_t)obj;
+    if (st_delete(w->wmap2obj, &data, &data)) {
+	VALUE rid = (VALUE)data;
+	int empty = 1;
+	if (st_lookup(w->obj2wmap, (st_data_t)rid, &data)) {
+	    rb_ary_delete((VALUE)data, obj);
+	    empty = !RARRAY_LEN((VALUE)data);
+	}
+	if (empty) {
+	    data = (st_data_t)rid;
+	    st_delete(w->obj2wmap, &data, &data);
+	}
+    }
+}
+
+static VALUE
+wmap_aset(VALUE self, VALUE wmap, VALUE orig)
+{
+    st_data_t data;
+    VALUE rids;
+    struct weakmap *w;
+
+    TypedData_Get_Struct(self, struct weakmap, &weakmap_type, w);
+    rb_define_final(orig, w->final);
+    rb_define_final(wmap, w->final);
+    if (!st_lookup(w->obj2wmap, (st_data_t)orig, &data)) {
+	rids = rb_ary_tmp_new(1);
+	st_insert(w->obj2wmap, (st_data_t)orig, (st_data_t)rids);
+    }
+    else {
+	rids = (VALUE)data;
+    }
+    rb_ary_push(rids, orig);
+    st_insert(w->wmap2obj, (st_data_t)wmap, (st_data_t)orig);
+    return nonspecial_obj_id(orig);
+}
+
+static VALUE
+wmap_aref(VALUE self, VALUE wmap)
+{
+    st_data_t data;
+    VALUE obj;
+    struct weakmap *w;
+    rb_objspace_t *objspace = &rb_objspace;
+
+    TypedData_Get_Struct(self, struct weakmap, &weakmap_type, w);
+    if (!st_lookup(w->wmap2obj, (st_data_t)wmap, &data)) return Qnil;
+    obj = (VALUE)data;
+    if (!is_id_value(objspace, obj)) return Qnil;
+    if (!is_live_object(objspace, obj)) return Qnil;
+    return obj;
+}
+
 /*
  *  The <code>GC</code> module provides an interface to Ruby's mark and
  *  sweep garbage collection mechanism. Some of the underlying methods
@@ -3710,6 +3889,14 @@ Init_GC(void)
 
     rb_define_module_function(rb_mObSpace, "count_objects", count_objects, -1);
 
+    {
+	VALUE rb_cWeakMap = rb_define_class_under(rb_mObSpace, "WeakMap", rb_cObject);
+	rb_define_alloc_func(rb_cWeakMap, wmap_allocate);
+	rb_define_method(rb_cWeakMap, "[]=", wmap_aset, 2);
+	rb_define_method(rb_cWeakMap, "[]", wmap_aref, 1);
+	rb_define_private_method(rb_cWeakMap, "finalize", wmap_finalize, 1);
+    }
+
 #if CALC_EXACT_MALLOC_SIZE
     rb_define_singleton_method(rb_mGC, "malloc_allocated_size", gc_malloc_allocated_size, 0);
     rb_define_singleton_method(rb_mGC, "malloc_allocations", gc_malloc_allocations, 0);
diff --git i/lib/weakref.rb w/lib/weakref.rb
index ee5444a..1fea9a9 100644
--- i/lib/weakref.rb
+++ w/lib/weakref.rb
@@ -1,5 +1,4 @@
 require "delegate"
-require 'thread'
 
 # Weak Reference class that allows a referenced object to be
 # garbage-collected.  A WeakRef may be used exactly like the object it
@@ -16,6 +15,7 @@ require 'thread'
 #   p foo.to_s                  # should raise exception (recycled)
 
 class WeakRef < Delegator
+  @@__map = ::ObjectSpace::WeakMap.new
 
   ##
   # RefError is raised when a referenced object has been recycled by the
@@ -24,51 +24,17 @@ class WeakRef < Delegator
   class RefError < StandardError
   end
 
-  @@id_map =  {}                # obj -> [ref,...]
-  @@id_rev_map =  {}            # ref -> obj
-  @@mutex = Mutex.new
-  @@final = lambda {|id|
-    @@mutex.synchronize {
-      rids = @@id_map[id]
-      if rids
-        for rid in rids
-          @@id_rev_map.delete(rid)
-        end
-        @@id_map.delete(id)
-      end
-      rid = @@id_rev_map[id]
-      if rid
-        @@id_rev_map.delete(id)
-        @@id_map[rid].delete(id)
-        @@id_map.delete(rid) if @@id_map[rid].empty?
-      end
-    }
-  }
-
   ##
   # Creates a weak reference to +orig+
 
   def initialize(orig)
-    @__id = orig.object_id
-    ObjectSpace.define_finalizer orig, @@final
-    ObjectSpace.define_finalizer self, @@final
-    @@mutex.synchronize {
-      @@id_map[@__id] = [] unless @@id_map[@__id]
-    }
-    @@id_map[@__id].push self.object_id
-    @@id_rev_map[self.object_id] = @__id
+    @@__map[self] = orig
     super
   end
 
   def __getobj__ # :nodoc:
-    unless @@id_rev_map[self.object_id] == @__id
-      Kernel::raise RefError, "Invalid Reference - probably recycled", Kernel::caller(2)
-    end
-    begin
-      ObjectSpace._id2ref(@__id)
-    rescue RangeError
+    @@__map[self] or
       Kernel::raise RefError, "Invalid Reference - probably recycled", Kernel::caller(2)
-    end
   end
 
   def __setobj__(obj) # :nodoc:
@@ -78,7 +44,7 @@ class WeakRef < Delegator
   # Returns true if the referenced object is still alive.
 
   def weakref_alive?
-    @@id_rev_map[self.object_id] == @__id
+    !!@@__map[self]
   end
 end
 

--
--- 僕の前にBugはない。
--- 僕の後ろにBugはできる。
中田 伸悦

Updated by ko1 (Koichi Sasada) about 12 years ago

  • Status changed from Open to Assigned
  • Assignee set to nobu (Nobuyoshi Nakada)
Actions #16

Updated by nobu (Nobuyoshi Nakada) about 12 years ago

  • Status changed from Assigned to Closed

This issue was solved with changeset r34995.
Makoto, thank you for reporting this issue.
Your contribution to Ruby is greatly appreciated.
May Ruby be with you.


Bug #5350

  • gc.c: add ObjectSpace::WeakMap. [ruby-dev:44565][Bug #5350]
  • lib/weakref.rb: use WeakMap instead of _id2ref.

Updated by zzak (zzak _) over 11 years ago

Could someone help me understand WeakMap?

I'm looking for a good example, or use-case for it to use in documentation.

Thank you!

Updated by naruse (Yui NARUSE) over 11 years ago

  • Status changed from Closed to Assigned

Updated by nobu (Nobuyoshi Nakada) about 11 years ago

  • Status changed from Assigned to Closed

Please file new ticket for documentation.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0