Hatena::ブログ(Diary)

..たれろぐ.. このページをアンテナに追加 RSSフィード

2016-07-14

2016-07-14

最近、久々に C# 使ってるのよね。

Java と似て非なる部分がちょろちょろあるから怖い。

2016-07-13

InnerException の再スローには ExceptionDispatchInfo を使おう

単発タスク(Task.WaitAllとかで集約していないタスク)を使った非同期処理を扱うメソッドでの例外を投げる場合、 AggregateException をそのまま出すと、メソッドを呼ぶ側で InnerException をチェックしたりしないといけないので面倒です(面倒ですよね?)。

/// <exception cref="AggregateException">非同期処理がIOExceptionした場合. InnerException参照のこと</exception>
void hoge() {
  Task t = Task.Run(() => { // ※ 裏で進められる IOException が出るかもしれない処理 });
  // ※ 表の処理
  t.Wait();    // AggregateException 出るかも
}

try {
  hoge();
} catch (AggregateException e) {
  if (e.InnerException is IOException) {
    // IOException の処理
  }
}

InnerException を throw するとスタックトレースが破棄されて(デバッグ時に)使い物にならない。

/// <exception cref="IOException">IOException出た</exception>
void hoge() {
  Task t = Task.Run(() => { // ※ 裏で進める IOException が出るかもしれない処理 });
  // ※ 表の処理
  try {
    t.Wait();    // AggregateException 出るかも
  } catch (AggregateException e) {
    throw e.InnerException;
  }
}

try {
  hoge();
} catch (IOException e) {
  // IOException の処理
  // ※ e のスタックトレースは throw e.InnerException 時点なので…
}

なので、 .Net 4.5 以降に制限されますが、 ExceptionDispatchInfo.Capture を使ってスタックトレースを保ったまま InnerException を throw しましょう。
呼び出し元では IOException 待つだけなので少し幸せに。

/// <exception cref="IOException">IOException出た</exception>
void hoge() {
  Task t = Task.Run(() => { // ※ 裏で進める IOException が出るかもしれない処理 });
  // ※ 表の処理
  try {
    t.Wait();    // AggregateException 出るかも
  } catch (AggregateException e) {
    ExceptionDispatchInfo.Capture(e.InnerException).Throw();  // スタックトレースを保って InnerException を throw
  }
}

try {
  hoge();
} catch (IOException e) {
  // IOException の処理
  // ※ e のスタックトレースは Task 内での例外発生時点 デバッグが楽に
}


await を使えるなら await でよさそうですが、タスクタイムアウト待ちしたりする場合や async にしたくない場合には使えないので。


.net - In C#, how can I rethrow InnerException without losing stack trace? - Stack Overflow

C# の long は読み書き Atomic じゃないから Interlocked 使おう

複数のスレッドから触られる long 変数(64bit変数)の取り扱い。 Java メインの人は引っかかりそうなのでメモ。

Java ではできる「volatile long」が C# では未サポート(コンパイルエラーになる)なので、 long 変数への Atomic な読み書きをするには読み書き部を lock するか、もしくは Interlocked を使って次の通りにする。

// hoge : マルチスレッドから触りたい変数, local_hoge : スレッドローカルな変数
long local_hoge = Interlocked.Read(ref hoge);   // long の読み込み hoge → local_hoge
Interlocked.Exchange(ref hoge, local_hoge);     // long の書き込み hoge ← local_hoge

Java では volatile long としておけば、読み書きに対する Atomic 性は保証されているので、 JavaC# をうろうろしている人にはちょっとした罠。
C# では 32bit 単位のメモリアクセスを想定しているのか、それを越えるサイズの long や double の Atomic なアクセスは言語機能としてはカバーしない。ライブラリを使えというスタンスな模様。
Java よりハードウェアに近いそういうトレードオフなんだろう。

lock が速いか Interlocked が速いかは評価していないけれど、たぶん後者が速いんだろう。

JavaのAtomicXXXX相当のクラスが欲しいなぁ)

マルチスレッド環境でlongを共有するときの注意 (C#編) - No hack, no life.
volatile (C# リファレンス)
Interlocked クラス (System.Threading)

2016/7/14 追記
Interlocked.Read メソッド (Int64) (System.Threading) の解説によると

解説
The Read method is unnecessary on 64-bit systems, because 64-bit read operations are already atomic.On 32-bit systems, 64-bit read operations are not atomic unless performed using Read.

The Read method and the 64-bit overloads of the Increment, Decrement, and Add methods are truly atomic only on systems where a System.IntPtr is 64 bits long.On other systems, these methods are atomic with respect to each other, but not with respect to other means of accessing the data.Thus, to be thread safe on 32-bit systems, any access to a 64-bit value must be made through the members of the Interlocked class.

『32bit 環境だと 64bit 変数は atomic にアクセス出来ないから Interlocked 使え』とのこと。
64bit 環境だと最適化されて Interlocked.Read とかは空メソッド相当になるんだろうから、 volatile long とすればコンパイラが頑張って Interlocked 経由の操作に置き換えてくれたらいいのに。

2016-07-09

TcpClient#Connect のタイムアウト制御

TcpClient でコネクションを張る際の接続時タイムアウトをユーザコードで任意に指定しようとすると、次のようなコードが今風(C# 5.0 以降風)なのだろうか?

try {
    int timeout = 接続タイムアウト(ms);
    TcpClient client = new TcpClient();
    Task con_task = client.ConnectAsync(remote_addr, remote_port);
    if (!con_task.Wait(timeout))    // 接続拒否などは AggregateException
    {
        client.Close();
        throw new SocketException(10060);   // 10060 -> WSAETIMEDOUT
    }

    // ※接続後の処理

} catch (SocketException e) {
   // timeout でのタイムアウト
} catch (AggregateException e) {
   if (e.InnerException is SocketException) {
       // 接続失敗 拒否されたか Socket でのタイムアウト
   }
}

※timeout にシステムデフォルトタイムアウト時間(約21秒)より長い時間を指定した場合は、システムデフォルト値が優先されます。
timeout 優先にしようとすると、リトライ処理を書くことになりそうな
例外が SocketException と AggregateException に割れるのも美しくない…

WindowsにおけるTCPの接続要求タイムアウト - 涼の成長記録