Feature #1952

cannot stop with Ctrl+C

Added by Usaku NAKAMURA almost 6 years ago. Updated over 2 years ago.

[ruby-dev:39107]
Status:Closed
Priority:Normal
Assignee:Koichi Sasada

Description

=begin
以下のスクリプトがCtrl+Cで停止せず、Ctrl+C押下後はkill -9でしか殺せません。

Thread.new do
begin
begin
sleep
ensure
raise
end
rescue
retry
end
end.join
=end


Related issues

Related to Ruby trunk - Bug #1963: redo'ing deadlock causes [BUG] Closed 08/20/2009
Related to Ruby trunk - Bug #2558: r24591 causes Segfault Closed 01/05/2010
Related to Ruby trunk - Bug #4285: Ruby don't have asynchrounous exception safe syntax and It should have. Closed 01/17/2011
Related to Ruby trunk - Bug #6131: Ctrl-C handler do not work from exec process (Windows) Closed 03/12/2012
Related to Ruby trunk - Bug #5368: ensure節でsleepするようなThreadがあるとインタプリタが終了しない Assigned 09/26/2011

Associated revisions

Revision 37875
Added by Motohiro KOSAKI over 2 years ago

  • thread.c (rb_thread_terminate_all): broadcast eTerminateSignal again when Ctrl-C was pressed. [Feature #1952]

Revision 37875
Added by Motohiro KOSAKI over 2 years ago

  • thread.c (rb_thread_terminate_all): broadcast eTerminateSignal again when Ctrl-C was pressed. [Feature #1952]

History

#1 Updated by Usaku NAKAMURA almost 6 years ago

=begin
こんにちは、なかむら(う)です。

In message " [Bug #1952] cannot stop with Ctrl+C"
on Aug.18,2009 23:50:32, redmine@ruby-lang.org wrote:

以下のスクリプトがCtrl+Cで停止せず、Ctrl+C押下後はkill -9でしか殺せません。

調査を進めてみました。

当初はpthreadな環境依存かと思っていましたが、mswin32でも同じ
現象が起きました。
1.8だと、1回目のCtrl+Cでは停止せず、2回目のCtrl+Cで死にます。

とりあえずtrunkベースで現象を眺めてみると、

(1) Ctrl+C押下時点でmainスレッドとThread.newされたスレッド
両方がTHREAD_TO_KILLとなる。

(2) mainスレッドはTAG_FATALでrb_longjmp()された後、他のスレ
ッドの死亡待ちループに入る。

(3) Thread.newされたスレッドの方はTAG_FATALでrb_longjmp()さ
れ、sleepが中断される。
そこで内側のbegin〜end内のensure節に入るのでraiseが実行
される。
今度は外側のbegin〜end内のrescue節に入り、retryが実行さ
れる。
以上の過程で、そもそもTAG_FATALだったことが忘れ去られて
しまい、再びsleepが実行される。

(4) Thread.newされたスレッドがsleepで寝たきりなので、mainス
レッドは延々と死亡待ちループを続ける。
再度Ctrl+Cを押しても、(3)が繰り返されるだけ。

ということになってるようです。
おそらく、(3)のTAG_FATALが忘れ去られるのが問題で、外側のrescue
節に入らないようにしないといけないのではないかと思います。
ensure節でraiseするとき、現在TAG_FATALだったらTAG_RAISEでなく
TAG_FATALで飛ぶべきなんでしょうかねえ。難しい。

それでは。
--
U.Nakamura usa@garbagecollect.jp

=end

#2 Updated by Yukihiro Matsumoto almost 6 years ago

=begin
まつもと ゆきひろです

In message "Re: Re: [Bug #1952] cannot stop with Ctrl+C"
on Wed, 19 Aug 2009 15:47:18 +0900, "U.Nakamura" usa@garbagecollect.jp writes:

|おそらく、(3)のTAG_FATALが忘れ去られるのが問題で、外側のrescue
|節に入らないようにしないといけないのではないかと思います。
|ensure節でraiseするとき、現在TAG_FATALだったらTAG_RAISEでなく
|TAG_FATALで飛ぶべきなんでしょうかねえ。難しい。

うーん、ensureでraiseしたり、その外でrescueでretryしたりして
いるわけですから、無限ループそのものは「意図通り」なのではな
いでしょうか。むしろ、raiseがTAG_FAGALで飛ぶ方が気持ちが悪い
です。

これはmain threadが他スレッド待ちの間に割り込みが効かない方を
直すべきではないでしょうか。

=end

#3 Updated by Usaku NAKAMURA almost 6 years ago

=begin
こんにちは、なかむら(う)です。

In message " Re: [Bug #1952] cannot stop with Ctrl+C"
on Aug.19,2009 16:00:28, matz@ruby-lang.org wrote:

うーん、ensureでraiseしたり、その外でrescueでretryしたりして
いるわけですから、無限ループそのものは「意図通り」なのではな
いでしょうか。むしろ、raiseがTAG_FAGALで飛ぶ方が気持ちが悪い
です。

私としてはTAG_FATALが握りつぶし可能であることの方が気持ちが悪
いです。
つまり、いかなる方法でensure節が終了した場合(raiseだろうがreturn
だろうがend到達だろうが)も、TAG_FATALとしての処理を続行すべき
ではないか、ということです。
私の元の文面がよくありませんでしたが、TAG_RAISEでないTAG_FATAL
な例外を発生させる、という意図はありません。

こういう言い方だと「気持ちが悪」くなくなるでしょうか?
それとも、「意図通り」のはずの無限ループが中断されることが「
気持ちが悪い」のでしょうか?

これはmain threadが他スレッド待ちの間に割り込みが効かない方を
直すべきではないでしょうか。

それで1.8と同じ(2回Ctrl+Cでスクリプトを終了できる)にはなりま
すね。

それでは。
--
U.Nakamura usa@garbagecollect.jp

=end

#4 Updated by Yukihiro Matsumoto almost 6 years ago

=begin
まつもと ゆきひろです

In message "Re: Re: [Bug #1952] cannot stop with Ctrl+C"
on Wed, 19 Aug 2009 16:35:56 +0900, "U.Nakamura" usa@garbagecollect.jp writes:

|In message " Re: [Bug #1952] cannot stop with Ctrl+C"
| on Aug.19,2009 16:00:28, matz@ruby-lang.org wrote:
|> うーん、ensureでraiseしたり、その外でrescueでretryしたりして
|> いるわけですから、無限ループそのものは「意図通り」なのではな
|> いでしょうか。むしろ、raiseがTAG_FAGALで飛ぶ方が気持ちが悪い
|> です。
|
|私としてはTAG_FATALが握りつぶし可能であることの方が気持ちが悪
|いです。

これなんですが、そもそもなぜInterruptがTAG_FATALなんでしょう
か。1.8では普通にTAG_RAISEだと思います。もちろん、うささんに
聞いてもしょうがないんですが。

|つまり、いかなる方法でensure節が終了した場合(raiseだろうがreturn
|だろうがend到達だろうが)も、TAG_FATALとしての処理を続行すべき
|ではないか、ということです。
|私の元の文面がよくありませんでしたが、TAG_RAISEでないTAG_FATAL
|な例外を発生させる、という意図はありません。
|
|こういう言い方だと「気持ちが悪」くなくなるでしょうか?
|それとも、「意図通り」のはずの無限ループが中断されることが「
|気持ちが悪い」のでしょうか?

私の感想は

  • C-cでTAG_FATALが発生する現状が気持ち悪い

  • ensure + raise で例外を握り潰すことができるのは以前から
    知られていたことであり、それをできなくする必然性がわから
    ない

ということです。さらに

  • kill -9を必要とするのは問題である。ので、main threadは割
    り込み可能にしておくべき

  • C-cでTAG_RAISEであるべき(だと思う)

  • C-cのことは別にして、うささんがおっしゃるようにTAG_FATAL
    状態でensureした場合、たとえ途中で通常のraiseがあってもそ
    れは無視して、TAG_FATALを継続するという仕様は十分ありえる

と思います。

                             まつもと ゆきひろ /:|)

=end

#5 Updated by Usaku NAKAMURA almost 6 years ago

=begin
こんにちは、なかむら(う)です。

In message " Re: [Bug #1952] cannot stop with Ctrl+C"
on Aug.19,2009 17:06:30, matz@ruby-lang.org wrote:

これなんですが、そもそもなぜInterruptがTAG_FATALなんでしょう
か。1.8では普通にTAG_RAISEだと思います。もちろん、うささんに
聞いてもしょうがないんですが。

あれ、と思って追い直してみたところ、TAG_FATALはvm_exec()の中
で消尽されていて、eval.c内にはTAG_RAISEで処理されていました。

すみません...

というわけで、

  • kill -9を必要とするのは問題である。ので、main threadは割 り込み可能にしておくべき

とりあえずこれですね。
ちなみに、thread.c:320の/* ignore exception */のところになり
ます。

  • C-cのことは別にして、うささんがおっしゃるようにTAG_FATAL 状態でensureした場合、たとえ途中で通常のraiseがあってもそ れは無視して、TAG_FATALを継続するという仕様は十分ありえる

これはそのうちTAG_FATALを起こす方法を考えて研究してみます。

それでは。
--
U.Nakamura usa@garbagecollect.jp

=end

#6 Updated by Yukihiro Matsumoto almost 6 years ago

  • Status changed from Open to Closed
  • % Done changed from 0 to 100

=begin
Applied in changeset r24591.
=end

#7 Updated by Usaku NAKAMURA almost 6 years ago

=begin
こんにちは、なかむら(う)です。

In message " Re: [Bug #1952] cannot stop with Ctrl+C"
on Aug.19,2009 17:49:23, usa@garbagecollect.jp wrote:

  • kill -9を必要とするのは問題である。ので、main threadは割 り込み可能にしておくべき

とりあえずこれですね。
ちなみに、thread.c:320の/* ignore exception */のところになり
ます。

パッチは用意してみましたが、そもそもなぜここでは例外を無視し
ていたのでしょう?>ささださん
普通に考えると、ここで起きうる例外って自身に対する割り込みだ
けのような気がするんですが...

Index: thread.c
===================================================================
--- thread.c (revision 24567)
+++ thread.c (working copy)
@@ -298,6 +298,7 @@ rb_thread_terminate_all(void)
{
rb_thread_t th = GET_THREAD(); / main thread */
rb_vm_t *vm = th->vm;
+ int state;
if (vm->main_thread != th) {
rb_bug("rb_thread_terminate_all: called by child thread (%p, %p)",
(void *)vm->main_thread, (void *)th);
@@ -311,14 +312,12 @@ rb_thread_terminate_all(void)
thread_debug("rb_thread_terminate_all (main thread: %p)\n", (void *)th);
st_foreach(vm->living_threads, terminate_i, (st_data_t)th);

  • while (!rb_thread_alone()) {
  • state = 0;
  • while (state == 0 && !rb_thread_alone()) { PUSH_TAG();
  • if (EXEC_TAG() == 0) {
  • if ((state = EXEC_TAG()) == 0) { rb_thread_schedule(); }
  • else {
  • /* ignore exception */
  • } POP_TAG(); } rb_thread_stop_timer_thread(); それでは。 -- U.Nakamura usa@garbagecollect.jp

=end

#8 Updated by Usaku NAKAMURA almost 6 years ago

=begin
こんにちは、なかむら(う)です。

In message " Re: [Bug #1952] cannot stop with Ctrl+C"
on Aug.20,2009 13:22:10, usa@garbagecollect.jp wrote:

パッチは用意してみましたが、そもそもなぜここでは例外を無視し
ていたのでしょう?>ささださん
普通に考えると、ここで起きうる例外って自身に対する割り込みだ
けのような気がするんですが...

既に直ってたの見落としてました orz

それでは。
--
U.Nakamura usa@garbagecollect.jp

=end

#9 Updated by Yukihiro Matsumoto almost 6 years ago

=begin
まつもと ゆきひろです

In message "Re: Re: [Bug #1952] cannot stop with Ctrl+C"
on Thu, 20 Aug 2009 14:47:18 +0900, "U.Nakamura" usa@garbagecollect.jp writes:

|In message " Re: [Bug #1952] cannot stop with Ctrl+C"
| on Aug.20,2009 13:22:10, usa@garbagecollect.jp wrote:
|> パッチは用意してみましたが、そもそもなぜここでは例外を無視し
|> ていたのでしょう?>ささださん
|> 普通に考えると、ここで起きうる例外って自身に対する割り込みだ
|> けのような気がするんですが...
|
|既に直ってたの見落としてました orz

自身に対する割り込みだけだと確信できれば、うささんのパッチの
方がだいぶシンプルでいいですよねえ。どうなんでしょ。

=end

#10 Updated by Yusuke Endoh about 5 years ago

  • Status changed from Closed to Open
  • Assignee set to Yukihiro Matsumoto
  • Priority changed from Normal to 5

=begin
遠藤です。

このチケットのために r24591

 * thread.c (rb_thread_terminate_all): do not ignore interrupt when
   reaping threads on termination.  

という変更が入りましたが、rb_thread_terminate_all の後は
main thread 以外は死んでいるという仮定があるので、非常に
都合が悪いです。

具体的には、以下のコードで、まだ生きているスレッドがいる
のに vm が destruct されて、SEGV します (たまに) 。

begin
  100.times do |i|
    begin
      Thread.start(Thread.current) {|u| u.raise }
      raise
    rescue
    ensure
    end
  end
rescue
  p 100
end

「終了時には全スレッドに例外を投げ、死ぬまで待つ」という
仕様だと考えると、その例外を潰してしまうようなスレッドが
いれば、終了時に固まるのは当然ではないでしょうか。

begin; sleep; rescue Exception; retry; end

が Ctrl+C で止められないのと同じだと思います。

ということで、当該コミットを revert し、このチケットは
rejected とすることを提案します。

--
Yusuke ENDOH mame@tsg.ne.jp
=end

#11 Updated by Usaku NAKAMURA about 5 years ago

=begin
こんにちは、なかむら(う)です。

In message " Bug #1952 cannot stop with Ctrl+C"
on Apr.09,2010 00:47:49, redmine@ruby-lang.org wrote:

「終了時には全スレッドに例外を投げ、死ぬまで待つ」という
仕様だと考えると、その例外を潰してしまうようなスレッドが
いれば、終了時に固まるのは当然ではないでしょうか。

begin; sleep; rescue Exception; retry; end

が Ctrl+C で止められないのと同じだと思います。

元の再現コードはCtrl+C(=Interrupt)を潰していません。
なので、同じではないと思います。間違ってるかしら。

ということで、当該コミットを revert し、このチケットは
rejected とすることを提案します。

SEGVは困ったもんなので、このこと自体に反対はしません。

それでは。
--
U.Nakamura usa@garbagecollect.jp

=end

#12 Updated by Yusuke Endoh about 5 years ago

=begin
遠藤です。

2010年4月14日10:20 U.Nakamura usa@garbagecollect.jp:

In message " Bug #1952 cannot stop with Ctrl+C"
on Apr.09,2010 00:47:49, redmine@ruby-lang.org wrote:

「終了時には全スレッドに例外を投げ、死ぬまで待つ」という
仕様だと考えると、その例外を潰してしまうようなスレッドが
いれば、終了時に固まるのは当然ではないでしょうか。

begin; sleep; rescue Exception; retry; end

が Ctrl+C で止められないのと同じだと思います。

元の再現コードはCtrl+C(=Interrupt)を潰していません。
なので、同じではないと思います。間違ってるかしら。

全く同じではないですが、「終了しろー」という例外を潰している
点が同じだと思います。

Interrupt によってメインスレッドがプロセスの終了処理を開始し、
生きているサブスレッドに eTerminateSignal (Ruby レベルからは
見えない仮想的な例外) を投げてサブスレッドを全消ししようとし
ているのに、サブスレッド側がその例外潰している、というのが、
実際に起きていることです。
そう考えると、これは仕様かなーと思うのでした。

ということで、当該コミットを revert し、このチケットは
rejected とすることを提案します。

SEGVは困ったもんなので、このこと自体に反対はしません。

どうしても直したいならば、

  • サブスレッドの終了待ち状態で SIGINT を受け取ったら、
    eTerminateSignal を再送する

    • しつこく Ctrl+C を押していればいつか終了できる、かも
  • eTerminateSignal を捕捉できない例外とする

    • サブスレッドの ensure が実行されない
  • eTerminateSignal を投げて数秒しても終わってくれない場合、
    捕捉できない例外を投げる

    • サブスレッドの ensure が実行されない危険が緩和されるが 本質的に解決はしない。あとダサい

くらいを思いつきましたが、どれも問題がある or 面倒ですね。

new feature な気もするので、やるとしても 1.9.3 以降という
ことにして、今回は revert させてもらうことにします。

--
Yusuke ENDOH mame@tsg.ne.jp

=end

#13 Updated by Yusuke Endoh about 5 years ago

=begin
遠藤です。

遅くなりましたが、一旦 revert しました。

1.9.2 はとりあえず現状を仕様とすればいいと思いますが、将来的に
すっきりさせるならば、

  • 終了時のスレッド全消しも Thread#kill も TAG_FATAL でなく
    ただの例外を投げる

    • 子スレッド側でブロックした場合は自己責任とする
    • TAG_FATAL 自体、ほとんど不要?
  • TAG_FATAL で ensure 節が呼ばれた時に再 raise したら、
    TAG_FATAL を投げる

    • 気持ち悪いというのは同意しますが、無引数 raise は同じ 例外を投げるはずなので、正しい気もする

のいずれかがいいように思いました。

Feature トラッカに移動しておきます。

--
Yusuke Endoh mame@tsg.ne.jp
=end

#14 Updated by Shyouhei Urabe almost 5 years ago

  • Status changed from Open to Assigned

=begin

=end

#15 Updated by Hiroshi Nakamura over 3 years ago

  • Description updated (diff)

現在の挙動。

% cat foo.rb
Thread.new do
begin
begin
p :sleep
sleep
ensure
p :raise
raise
end
rescue
p :retry
retry
end
end.join
% ruby foo.rb
:sleep
C:raise
:retry
:sleep
CCCCCCCZ

#16 Updated by Akira Tanaka over 3 years ago

  • Priority changed from 5 to Normal
  • Assignee changed from Yukihiro Matsumoto to Koichi Sasada
  • % Done changed from 100 to 0

開発者会議で笹田さんなどと議論したところ、
以下のスクリプトで、2回め以降の C で 1 が表示されないのはバグであるという
結論になりました。

% ruby -ve '
Thread.new do
begin
begin
p 1
sleep
ensure
raise
end
rescue
retry
end
end.join
'
ruby 2.0.0dev (2012-03-16 trunk 35049) [x86_64-linux]
1
C1
CCCCCCC

C でメインスレッドが死んだ後、他のスレッドを殺すのに
各スレッドに例外を投げるわけですが、一発で死なない場合、
それ以降人間が C を送っても無視される、というのが問題で、
人間が C を送る度に例外を再度投げるのが適切であろう、
という点には合意に達して、笹田さんが直すとのことです。

なお、何回投げても死なない、というのはプログラムがそのように書かれているということで、
それを無理やり殺す、というのは、New Feature であろう、ということで
上記の直す範囲には入りません。

#17 Updated by Koichi Sasada over 2 years ago

  • Priority changed from Normal to 5

I'll fix it soon, at least before 2.0.

#18 Updated by Yusuke Endoh over 2 years ago

ささださん、現状を教えてください。

Yusuke Endoh mame@tsg.ne.jp

#19 Updated by Koichi Sasada over 2 years ago

これはやらんといかんということで,pr2 前か後かわかりませんが,やります.

#20 Updated by Motohiro KOSAKI over 2 years ago

  • Status changed from Assigned to Closed
  • % Done changed from 0 to 100

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


  • thread.c (rb_thread_terminate_all): broadcast eTerminateSignal again when Ctrl-C was pressed. [Feature #1952]

#21 Updated by Motohiro KOSAKI over 2 years ago

ついでというわけではないんですが、mameさんのcomment#10のようなスクリプトへの防御力をあげるため、メインスレッドがサブスレッド終了を待ってる時はステータスをTHREAD_KILLEDに変える修正を r37886 で入れました。メインスレッドはすでに終了しているのだから、thr.raiseが動いてしまっているのがそもそもおかしかった。

Also available in: Atom PDF