Posted by 미스란디르
Thu, 15 Feb 2007 16:23:00 GMT
얼마전에 사사다 코이치씨가 1.9(trunk) 에서 singleton.rb를 사용할 경우 에러가 난다는 포스팅을 했었다. ruby-dev:30263
요녀석은 make test-all을 하면 볼 수 있고, 저 포스팅의 예제코드를 돌려봐도 마찬가지로 볼 수 있다. (현재시각의 trunk에는 workaround처리가 되어 있음)
이 에러는 r11630 에서 발생한다. 무엇인고 하면, @가 인스턴스 변수, @@가 클래스 변수인데, @_이 클래스 로컬 인스턴스 변수를 뜻하는 prefix가 되면서 부터다.
어떻게 동작하는 것일까?
ruby-talk:86984
를 에서 소개한 코드를 보자.
class A
def m
@var = "hi" # regular instance variable
@_var = "hello" # class-local instance variable
end
end
class B < A
def n
puts @var.inspect # same as @var, above
puts @_var.inspect # not same as @_var above
end
end
o = B.new
o.m
o.n
이 코드의 예상 결과는? 주석을 참조한다면 어떻게 될지 짐작이 간다. 결과는 다음과 같다
"hi"
nil
내부적으로 어떻게 동작할까? 당신이라면? 나는 코드를 미리 봐버려서 짐작할 기회가 없었다. 답은 심볼 이름뒤에다 클래스를 붙여버린 것이다. 예를들자면 위의 예에서는 @_var라면 :_var/A가 된다. 이러면 B의 객체의 scope에서는 @_var를 접근할 때 :_var/B 를 찾게 되지만, 이녀석은 당연히 없으니 @_var는 값이 들어간적이 없는 인스턴스 변수, 즉 nil이 된다.
다시 처음의 문제제기로 돌아와서. ivar2(클래스 로컬 인스턴스 변수)를 구현하는데 사용한것이 @다음의 ’_’ 다. 그리고 Singleton의 경우, 인스턴스 변수로
@__mutex__를 사용해서 싱클튼을 구현하고 있다. (동시 접근을 막기 위해). 일단 싱글튼 코드를 먼저 살펴보자.
class << Singleton
def __init__(klass)
klass.instance_eval {
@__instance__ = nil
@__mutex__ = Mutex.new
}
def klass.instance
return @__instance__ if @__instance__
@__mutex__.synchronize {
return @__instance__ if @__instance__
@__instance__ = new()
}
@__instance__
end
klass
end
def inclued(klass)
Singleton.__init__(klass)
end
end
사실은 included에 코드가 몇줄 더 있지만 귀찮으니까 뺐다. included는 모듈이 클래스에 믹스인 될때 호출되는 메서드(말하자면 콜백)이다. 그리고 형식인수로 그 포함한 클래스를 받는다. 그리고 그것을 Singleton.
init으로 넘기고, instance_eval을 사용해서 그 클래스의 scope에서 @
mutex 를 초기화한다. 그리고 겸사겸사 klass.instance 메서드(이건 말하자면 클래스 메서드) 도 정의하는데, 이때 지금 초기화한 @
mutex 를 사용한다.
뭐 보기만 해서는 전혀 문제가 없어보인다.
여기서 잠깐 다른 코드를 보고 넘어가자.
class Merong
def m
@__y__ = "Yes!"
end
attr_accessor :__x__
end
m = Merong.new
m.m
m.instance_eval { @__x__ = "Yes!" }
puts m.instance_variables.inspect
위에서 말한대로라면, @
x__/Merong @y__/Merong 이 보여야 한다. 그러나!
[:@__x__/#<Class:#<Merong:0xb7ec4390>>, :@__y__/Merong]
문제는 요것. instance_eval을 사용할 경우 좀 애매하게 다른 결과가 나온다.
따라서 Singleton의 경우도 그럴거라고 예상할 수 있다. 귀찮아서 실제로 보는건 생략.
현재는 이것과 관련해서 _ 를 계속 쓸 것인지, 아니면 (v) 라던가 기타 다른 형식을 쓸 것인지를 고민하고 있다. (마츠씨와 사사다씨가)
================
사실 이 포스팅을 쓰면서, @__mutex__ 에 대입하는 부분과 사용하는 부분이 scope가 다르기 때문일꺼라고 막연하게 짐작했는데, 예상치 못한 결과가 나왔다. 아마도 ivar2관련 버그이리라.
Posted in 루비 | Tags 루비, 버그, bug, ruby | no comments | no trackbacks
Posted by 미스란디르
Mon, 12 Feb 2007 16:19:00 GMT
kldp에서 번역이 진행중이다. 원서 는 여기서 볼 수 있다. 영어로 된 pdf를 무료로 다운로드 할 수 있고, 프린트된 책을 저렴한 가격에 살 수 있다.
간단히 읽어 본 소감: 책 참 지저분하게 쓴다.
곡괭이 책을 번역하면서도 썰렁한 농담 때문에 고생을 했지만, 그래도 데이브 토머스는 적당히 했다. 이사람은 그 빈도가 좀 심하다. 시도 때도 없이 나오는 내용과 상관 없는 농담은 사람을 짜증나게 한다. 제대로 된 편집자가 있었다면 저 쓸 데 없는 농담들 다 잘라버렸을텐데 아쉽다.
하지만 내용이 거지같지는 않다. 중요한 내용을 짤막하게 잘 전달하고 있다. 프로그래밍 루비가 너무 길어서 읽기 곤란하다면 간단히 읽어볼만한 글이다.
Posted in 루비 | Tags 루비, 문서, article, ruby | no comments | no trackbacks
Posted by 미스란디르
Tue, 06 Feb 2007 17:41:00 GMT
발단은 mechanize-0.6.4.
코카스님이 mechanize-0.6.4를 가지고 OBot(이게 뭘까? =3) 를 돌리면 메모리 릭이 생긴다고 하시길래,
과연 정말 그런지 확인해 봤다.
다음과 같은 간단한 코드를 돌려보았다.
m = WWW::Mechanize.new
m.max_history = 2
loop {
m.get 'http://some.domain.tld/blank_page.html'
}
예로 든 url은 물론 없는 url이고, 실제로는 한글자짜리 html을 만들어서 테스트해봤다.
몇 분 안지나서 수십메가를 잡아먹기 시작한다.
으음. 과연 무엇이 문제일까.
0.6.3의 mehchanize.rb 를 0.6.4에다 복사하고 다시 돌려봤다. 이건 문제가 없다!
이럴땐 무식한게 최고다. 짐작이 안갈땐 손으로 해본다.
한부분씩 고쳐가면서 테스트를 해봤다.
그 결과.
body = StringIO.new
total = 0
response.read_body { |part|
total += part.length
body.write(part)
}
fetch_page 메서드 안의 저 부분이 들어가면서 부터 문제가 생긴다는 것을 알게 되었다.
으음. 혹시 StringIO가 버그인가? 이녀석의 구현은 C로 되어있다. 혹시 GC가 오동작하나? 해서 stringio의 mark함수를 살펴봤으나.. 이거 오직 하나 String객체만 포함할 뿐 다른건 없다. 그리고 free도 간단하기 그지 없는데다, StringIO테스트 코드를 만들어봤지만 역시 leak은 없었다.
StringIO를 String으로 바꿔서 저 코드를 돌려봐도 마찬가지.
그렇다면.. block?
read_body에 붙은 블록의 코드를 다 지우고 블록 뼈대만 남겨봤다. 앗! 역시 마찬가지다.
사실 fetch_code의 정의는
def fetch_page(uri, request, cur_page=current_page(), request_data=[])
이런데, 여기서 current_page()가 무엇인고 하면, @history 라는 배열에서 마지막 녀석을 돌려주는 메서드다.
그리고 @history는 @history_max 갯수만큼만 보관하도록 되어있다. add_to_hitory 메서드가 바로 그것.
코드를 보자.
def add_to_history(page)
@history.push(page)
if @max_history and @history.size > @max_history
# keep only the last @max_history entries
@history = @history[@history.size - @max_history, @max_history]
end
end
뭐 복잡한거 하나도 없다. 미리 최대 길이가 정해져 있으면 그이상 들어왔을 때 나머지를 빼버린다.
저러면 계속 history에 추가되더라도 나머지 page들에 대한(유효기간이 지난) 참조는 더이상 없어지므로 GC대상이 되어서 사라져야 한다. 그러나 mechanize-0.6.4의 경우는 저 객체들이 GC한테 안걸리는데, 그 이유가 무엇일까?
짐작 되는 원인은 이렇다.
용의자는 바로 블록이 클로져라는 사실이다. 클로져는 어떤식으로든 그 자신의 정의될때의 컨텍스트를 가지고 있어야 한다. 예를들면,
def method(arg)
lambda { arg }
end
l = method(2)
l.call => 2
@method(2)@라고 호출하면 arg는 함수 안에서만 유효하고 곧 사라져야한다. 그러나 클로져의 특성상 함수의 실행이 끝나더라도 컨텍스트를 저장하게 되고, arg의 대한 참조가 바로 저곳에 들있게 된다. 아직 루비 소스에 대해서 잘 모르기 때문에 블록이 어떻게 구현되었는지는 정확히 몰라도, GC가 보기에 arg는 저 블록이 존재하는 한 살아있다고 판단할 것이 분명하다.
결국 클로져라는 이점이 발목을 잡은셈. 아직 완전히 추적이 끝나지 않았지만, 이런 상황도 있을 수 있다는 것을 기록 하고자 글을 쓴다.
문제가 뭔지 제대로 파악한 사람은 좀 알려주시면 고맙겠다.
—
문제가 발생하는 최소한의 코드를 만들었다.
class MyMech
def initialize
@arr = []
end
def sabjil(curr = @arr.last)
x = lambda {}
add_to_arr([x])
end
def add_to_arr(item)
@arr.push(item)
if @arr.size > 2
@arr = @arr[@arr.size - 2, 2]
end
end
end
s = MyMech.new
while true
s.sabjil
end
이게 버그가 생기는건 당연하다. 순차적으로 계속 참조하거덩. 이를 어쩐다…
Posted in 프로그래밍, 루비 | Tags debugging, leak, programming, ruby | 1 comment | no trackbacks