けいぞうのメモ帳

言語設計のお勉強

MVarが"thread blocked indefinitely in an MVar operation"する時のデバッグ方法

haskellのmutexであるMVarがデッドロックを起こしたときのデバッグ方法を解説する。 そもそもMVarはhaskellにおけるもっともプリミティブな非同期共有の方法であり、 必要がないならばasyncやstmなどの高度な表現を用いることが推奨される。

環境

  • MAC OS X
  • Version 1.5.1 x86_64 hpack-0.17.1
  • The Glorious Glasgow Haskell Compilation System, version 8.0.1

問題

haskellにおける非同期処理では、データをやり取りする方法がいくつかある。 そのうちのもっともプリミティブなものにMVarがある。

MVarには一つだけデータが入る。 そして、データが入っている間は入る操作を他の箇所から行うことが出来ずスレッドはブロックする。 また、データが存在しない間は読み出す操作を他の箇所から行うことは出来ずスレッドはブロックする。

正しく動くコードの例

module Main where
import Control.Concurrent
import Control.Concurrent.MVar

main :: IO ()
main = run 
run :: IO ()
run = do
  v <- newEmptyMVar
  forkIO v $ \v ->  do
    writeMVar v 1
  d <- readMVar v
  print d
  return ()

forkIOでスレッドを生成し、v :: MVar Intを経由して数字1をスレッドから受け取っている。 もし、このコードが以下のようだった場合デッドロックが起こる。

module Main
import Control.Concurrent
import Control.Concurrent.MVar

main :: IO ()
main = do
  v <- newMVar
  d <- readMVar v
  return ()

この時に表題の"thread blocked indefinitely in an MVar operation"がstdoutに出力されてプログラムは終了する。

対策

この時、GHCのruntimeはBlockedIndefinitelyOnMVarという非同期例外を投げている。 よってこの例外を主眼において操作を行うことでデバッグ情報を取得することが出来る。

方法は2つあり、 - コードでControl.Exception.catchを用いてBlockedIndefinitelyOnMVarを捕まえてレポートする - ghciでスタックトレースを行う

ことである。僕はghciでスタックトレースを取る方法を推奨する。 ref: https://downloads.haskell.org/~ghc/6.12.1/docs/html/users_guide/ghci-debugger.html#ghci-debugger-exceptions

ターミナルでの操作を以下のように行う。

$ stack ghci --ghci-options -fbreak-on-exception
ghci> :l program.hs
ghci> :trace main
Stopped in <exception thrown>, <unknown>
[<unknown>] *Main> :back
Logged breakpoint at program.hs:11:3-12
_result :: IO GHC.Prim.Any
v :: MVar GHC.Prim.Any
[-1: program.hs:11:3-12] *Main> 
  1. ghciをoption-fbrea-n-exceptionつきで起動する。
  2. 対象のプログラムを読み込む
  3. :trace コマンドでmainを走らせる
  4. SomeExceptionが投げられプログラムが中断する
  5. :backコマンドでスタックトレースが行われる

スタックトレースによってどの変数でBlockedしたのか、それがどのファイル名、どの行数で起こったのかなどがわかる。

コードベースで対応する方法はgistにあげておく ref:

handleAsyncExcepOnMVar.hs · GitHub

これは行数、ファイル名が得られないため望ましくない。C言語ライクなFILE, LINEを出力するライブラリを用いて 対応することもあるが、実行時の行数が得られないため、無意味である。

mruby-implerr mgem

github.com

設計上起こり得ないエラーを処理する例外クラスのmgemを公開した。 これは、主に作業中のmgemにおいて使われることを想定している。
分岐や変更の多い処理で一時的に例外をなげて、実装に考慮漏れがあることを明示するために用いる。
テストを走らせたときなどに便利だと思う。

たとえばこんな感じのクラスがあったとして

class Data
  attr_accessor :a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k
  def initialize(a,b,c,d,e,f,g,h,i,j,k)
  end
end

 このクラスのオブジェクトを対応するC言語の構造体に変換したいとする。 rubyは同じ扱いが出来るのであれば、複数のクラスのオブジェクトを同じように扱う方針があるので、 Data::Nameのような特定のデータを示すクラスとStringクラスのどちらも引数に取れたりする。 これをmrubyのmrb_valueからC言語の文字列 char *に変換したい場合それぞれで処理が異なる。 だからオブジェクトの型チェックなどの判定処理を入れることになると思う。 そうして、求められるC言語での実体をそれぞれの変数から取得して構築する。

 このような変換関数を書くとき、未成熟なライブラリにおいて rubyの構造体の方に入る値はかなり柔軟に変更されることが予想できる。 Stringしか受け取らなかったけど任意のクラスの子クラスのオブジェクトはすべて受け取るだとか、 Fixnumも受け取れた方がいいだとかである 。

 このような変更により処理に不整合が起こることが予想できる部分に書く例外として

ImplementationError

をmruby-implerrとして用意した。

NotImplementedErrorは未実装な場合に投げる例外だと思うし、すべてをRuntimeErrorで処理するのは良くないと思ったためだる。

ImplementationErrorはオプショナル引数を2つもち、数字と文字列を受け取れる。

raise ImplementationError.new(__LINE__,__FILE__)

のようにして、使うことを考えている。
引数の順序はどちらを先にしても問題ない。

C言語から例外を投げる

ImplementationErrorC言語から投げられることが多いと思っている。 正直ruby側ではちゃんとrubyとしてStandardErrorを継承したそれ用の例外をガンガン変更していける。
C言語側で、問題の粒度がはっきりしない場合に一時的にmrb_raise(mrb_state*, struct RObject *, char *)を用いて 処理を中止する用途で用いることを推奨する。

QUICメモ "Design and Internet-Scale Deployment"

この記事は ACM SIGCOMM “17において発表される The QUIC Transport Protocol: Design and Internet-Scale Deployment の紹介です。 僕が読んだことや関連したdraftをつらつらと記述していきます。

このpaperはIETFにおいて標準化作業中のQUIC transport protocol の動機と機能、そしてGoogle内部における導入事例の紹介を行うものです。 paperの前半は機能の説明、ストリーム多重化やAEAD、ハンドシェイクなどの説明、 後半はGoogleにおける運用状況の報告になります。

Googleは、数年前から標準化に先んじて前進となるプロトコルを実装していました。 これはGoogle QUIC(以下 GQUIC)と呼ばれ、IETF quicwgにおいて標準化作業中のinternet draftの仕様を指してIETF QUIC(以下QUIC)と呼びます。

Google内部間やモバイル/ウェブのChromeYoutubeGoogle検索とGoogle間における通信は可能な限りでGQUICで行われています。 これはgoogle chrome(chromium)においてGQUICが有効になっている場合、 chrome://net-internals にてその通信を見ることが出来ます。

今回紹介する"The QUIC Transport Protocol: Desing and Internet-Scale Deployment"では、 Googleが行った数々の計測と実験の結果を公開しています。

  • Google検索とYoutubeにおけるモバイル、ウェブそれぞれの性能計測
  • 地域ごとの性能
  • サーバのCPU使用率
  • さまざまな条件における到達可能性の評価.

などです。

これらはGoogle Chromeのtreeにあり、https://github.com/google/proto-quic として公開されている実装を 用いて行われました。 GoogleはHTTPサーバを独自に実装していますが、それらもproto-quicの実装と同じものを用いています。

UDP関係

QUICはUDP上に実装されているため、UDPの事情に強く影響を受けます。 最たるものはUDPがsend and forgetであることですが、その他にも多くあります。

UDP PMTU

UDPは経路MTUを考慮した通信を行う仕様を含みません。 そのため、QUICは自らでIP フラグメンテーション対策を行う必要があります。
PMTU DiscoveryやMTU Discoveryを自らで行います。 UDPのPMTUの計測結果がFigure12 in Section 7.1に掲載されています。

Server

CPU Utilizatoin

QUICはTLS over TCPに比べていくらかのCPU使用率の増加が見込まれます。 これは

  1. QUICの内部状態
  2. TLSによる常時暗号化
  3. ストリーム並列化

が主な原因であるとされています。 1,2について記述します。

1 QUICの内部表現

QUICは一つのUDPのエンドポイントを複数のコネクションで共有して用います。 また、ひとつのコネクションを複数のストリームで共有して用います。 そのため、他のプロトコルスタックに比べてより複雑な内部状態を持つことになります。

Googleは、これを実装の検討により改善することが出来るとし、より適切な実装を行うことでたしかに性能は 改善されたとしました。また、まだ考慮すべき点は多くあり、性能の向上の余地は未だあるとしています。

  1. TLSによる常時暗号化

QUICはTLS1.3の多くの機能を暗号化や認証のために用いています。 ヘッダーは認証され、ペイロードは暗号化されます。 このコストは相応に高くつくため、CPU負荷となっています。 Googleは独自に最適化したChaCha20を用いることで負荷の軽減としました。

良い性能が出ないケース

QUICは常に他より良い結果が得られるわけではありません。 その事例についての記述も存在します。

  • 広い帯域幅、低いRTT、少ない損失
  • pre warmed endpoint
  • モバイル端末

QUICは高RTT,低帯域幅、高い損失の対策として様々な情報を付加しています。 そのため、それらが起こらない場合は情報を更かした分だけ転送効率が下がることがあります。

また、pre warmed connectionは0 RTT ハンドシェイクの恩恵を受けることができないため、 TCPより性能が低下することがあります。

モバイル端末での計算資源はデスクトップ端末でのそれよりも制約が厳しく、 効率の良いデータ転送を実現したとしても、処理しきれないことがあります。 また、モバイルアプリケーションの通信は、すでに特定用途で特化していることが多いため ネットワークスタックの改善の余地が少ないこともありえます。

haskell のcereal package example

{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DeriveGeneric     #-}
module Main where

import           Data.Serialize
import           GHC.Generics

data Dist = Dist { distInt :: Int, distString :: String }
          deriving (Generic, Show)

instance Serialize Dist

main :: IO ()
main = let bs = encode d
        in do
           print bs
           case ( decode bs) of
             Right res -> print (res  :: Dist)
             Left str  -> print str
    where d = Dist { distInt = 1
                   , distString = "hello"}

ref:

Data.Binary.GetのGet MonadにおけるMonadFail

できること

Get Monadの中でControl.Monad.Fail.failを実行すると、runGetOrFailのときにLeftを取ることが出来る。 runGetOrFailが取る引数はユーザが定義した任意の構造体へのdecoderであるGet Monadのため、 Get Monadの失敗をユーザがハンドルできる。

MonadFail

モナドの中での失敗を意味する型クラス。 もともとはMonadのメソッドだったところを「分離していいよね?」という提案の元、型クラスに分離された経緯がある。

ref:

Control.Monad.Fail

PoC

{-# LANGUAGE OverloadedStrings #-}
module Main where


import           Data.Binary.Get

main :: IO ()
main = print $ f

f = case (runGetOrFail decode "") of
      Right (rest, _,f) -> ""
      Left _            -> "runGetOrFail return Left when called in Get Monad"


data Sample = Sample Int
            deriving Show

decode :: Get Sample
decode = fail "you shoul fail"

b読み飛ばしても良い経緯

inary packageのData.Binary.Getを正常に失敗させる方法を知りたい。
binary packageのData.Binary.Get.runGetOrFailでは 失敗するかもしれないGetモナドを実行することができる。 これをユーザがハンドルする方法がドキュメントに書いてなかった。 しかしGetモナドはMonadFailのインスタンスであり、MonadFailは モナドの失敗を扱うことが出来る型クラスであるためこれを用いたときに、失敗時の処理となるのではないかと思った。