あどけない話

Internet technologies

atomicModifyIORef' の歴史

Haskellの並行プログラミングの奥義であるatomicModifyIORef'の歴史。内容は予告なく修正される。

atomicModifyIORef'は「IORef」と「関数」を引数に取る。その関数は、IORefが現在指している「古い値」を受け取り、IORefが新たに指すべき「新しい値」と「帰り値」をタプルで返すように書く。

atomicModifyIORef' :: IORef a -> (a -> (a, b)) -> IO b

新しい値は、サンクが作られた時点で、古い値と不可分にスワップされ、その後評価される。評価は重くとも、サンクを作ることは軽いので、スワップは多くの場合成功する。

atomicModifyIORef の問題点

もうすっかり忘れていたが、Mightyの開発過程で、atomicModifyIORefがスペースリークをすることを発見し、解決策を2011年の Monad.Reader issue 19に執筆した。

以下は、時間の文字列を1秒に一回作ってキャッシュしておくコード:

atomicModifyIORef ref (\_ -> (tmstr, ()))

サンクが潰れずにスペースリークとなる。解決策として、以下を考案。

x <- atomicModifyIORef ref (\_ -> (tmstr, ()))
x ‘seq‘ return ()

これを Monad.Reader に書いた。その後、バンパターンを使うともっとカッコよく書けることが判明。

!_ <- atomicModifyIORef ref (\_ -> (tmstr, ()))

注意深くコードを読んだ人は疑問に思うだろう。タプルの右側の要素しか評価していないのに、どうして左側の新しい値に起因するスペースリークがなくなるか?

このコードでは、新しく作られる値のサンクを潰すことはできない。しかし、右の要素を評価することでタプルが生成される。すると、左側のサンクは、古い値を参照していないことが分かり、GCが回収できる。

このコードは、IORefが指す古い値と返り値を使わないので、現在では atomicWriteIORef が利用できる。

atomicModifyIORef' の登場

Monad.Reader issue 19をきっかけに、Joey Adams氏が atomicModifyIORef' を提案。以下は、2011年にマージされたAdd strict versions of modifyIORef and atomicModifyIORefより:

atomicModifyIORef' :: IORef a -> (a -> (a,b)) -> IO b
atomicModifyIORef' ref f = do
    b <- atomicModifyIORef ref
            (\x -> let (a, b) = f x
                    in (a, a `seq` b))
    b `seq` return b

baseライブラリ4.6.0以降で atomicModifyIORef'が利用可能となった。つまり、2012年にリリースされた GHC 7.6.1 から利用できる。

Simon Marlow氏が、2013年に発行されたParallel and Concurrent Programming in Haskell の284ページでこのパターンを紹介。atomicModifyIORef' 自体への言及はなし。2014年に発行されたHaskellによる並列・並行プログラミングの363ページでは、僕が atomicModifyIORef' に関する訳注を入れた。

参照はサンクが作られた時点でスワップされて、そのサンクは atomicModifyIORef'の終了後、以下のように評価される。

  • atomicModifyIORefの外側で b を返す前に、bをWHNFにしようとする
  • bを評価するには、内部で仕込まれたseqにより、aもWHNFになる
  • その後bがWHNFになる。

このころはプリミティブな命令として atomicModifyMutVar# があり、atomicModifyIORef は、そのラッパーだった。以下は不可分性はないが、atomicModifyIORef の動作をエミュレートした疑似コードである。

pseudoAtomicModifyIORef :: IORef a -> (a -> (a, b)) -> IO b
pseudoAtomicModifyIORef ref f = do
  x <- readIORef ref
  case f x of
    fx -> do  -- thunk 'fx'
      -- only CAS success case
      writeIORef ref $ fst fx -- thunk 'fst fx'
      putStrLn "swap"
      return $ snd fx -- thunk 'snd fx'

これを使い Joey Adams氏の実装をエミュレートする:

joey :: IORef a -> (a -> (a, b)) -> IO b
joey ref f = do
    b <- pseudoAtomicModifyIORef ref
            (\x -> let (a, b) = f x
                    in (a, a `seq` b))
    b `seq` return b

また、評価順序を知るために、たくさん trace 仕込んだ f を用意する:

f :: Int -> (Int, ())
f n = (trace "tupple" ((trace "new" n + 1), (trace "ret" ())))

GHCiで試してみよう。

> ref <-newIORef (0::Int)
> joey ref f
swap
tupple
new
ret

"new" よりも前に "swap" が表示されているのが分かる。

atomicModifyIORef' にどんなに正格な関数を与えても、サンクが先に IORef に格納されて、その後で新しい値が計算される。以下のような正格な関数を考える:

f' :: Int -> (Int, ())
f' n = let !n' = trace "new" n + 1 in (trace "tupple" (n', (trace "ret" ())))

使ってみよう。

> joey ref f'
swap
new
tupple
ret

"tupple" よりも "new" の方が早く生成されるが、それでも "swap" の後だと分かる。

性能向上

オリジナルの atomicModifyIORef' は、タプルでパターンマッチした後、新しいタプルを作成している。これは無駄である。parcs 氏は、セレクターサンクを使って性能を向上させること提案した

atomicModifyIORef' ref f = do
    b <- atomicModifyIORef ref $ \a ->
            case f a of
                v@(a',_) -> a' `seq` v
    b `seq` return b

このコードはタプルを生成しない。case文はフィールドを取り出すために使われている。これは、セレクターサンクとなって、GCが高速に扱えるらしい。

しかし、本当にこの方法で、新しい値が評価される前にスワップされるのだろうか? v を返す前に a' が評価されているように見える!

違う。違う。あなたの解釈は違うのだ。Joey Adams氏の実装は、タプルの遅延性を利用しているからうまくいくと思っているなら、違うのだ。atomicModifyMutVar# が遅延させるのは、関数(ラムダ式)全体であって、タプルの生成には頼っていない。

先ほど、正格な関数 f' を与えても、うまく動作していたことを思い出そう。

parcs氏の実装をエミュレートする:

parcs ref f = do
    b <- pseudoAtomicModifyIORef ref $ \a ->
            case f a of
                v@(a',_) -> a' `seq` v
    b `seq` return b

使ってみる。

> ref <-newIORef (0::Int)
> parcs ref f'
swap
new
tupple
ret

やはり、"swap" が最初に起きていることが分かる。

atomicModifyMutVar2#

David Feuer氏が、ghc proposals 0149を出し、atomicModifyIORefのスペースリークをなくすために、新たなプリミティブである atomicModifyMutVar2# の作成を提案した。

atomicModifyMutVar では、第一フィールドと第二フィールド両方にサンクを作成していた。atomicModifyMutVar2#では、第一フィールドだけにサンクを作る。第二フィールドは正格に評価されるので「サンクの割り当て直後に潰す」という無駄が生じない。

atomicModifyMutVar2# :: MutVar# s a -> (a -> c) -> State# s -> (# State# s, a, c #)

引数の関数の型から、タプルを仮定してないことが分かる。これを一般的なHaskellのコードから呼ぶための関数にするラッパーが、2つある。

atomicModifyIORef2Lazy :: IORef a -> (a -> (a,b)) -> IO (a, (a, b))
atomicModifyIORef2Lazy (IORef (STRef r#)) f =
  IO (\s -> case atomicModifyMutVar2# r# f s of
              (# s', old, res #) -> (# s', (old, res) #))

atomicModifyIORef2 :: IORef a -> (a -> (a,b)) -> IO (a, (a, b))
atomicModifyIORef2 ref f = do
  r@(_old, (_new, _res)) <- atomicModifyIORef2Lazy ref f
  return r

なぜ2つに分かれているのかは不明。現在のatomicModifyIORef'の実装はこう:

atomicModifyIORef' :: IORef a -> (a -> (a,b)) -> IO b
atomicModifyIORef' ref f = do
  (_old, (_new, !res)) <- atomicModifyIORef2 ref $
    \old -> case f old of
       r@(!_new, _res) -> r
  pure res

以下は、日比野さんが書いた検証コード:

pseudoModifyIORef2Lazy :: IORef a -> (a -> (a, b)) -> IO (a, (a, b))
pseudoModifyIORef2Lazy ref f = do
  x <- readIORef ref
  case f x of
    fx -> do
      -- only CAS success case
      writeIORef ref (fst fx)
      putStrLn "swap"
      return (x, fx)

pseudoModifyIORef2 :: IORef a -> (a -> (a,b)) -> IO (a, (a, b))
pseudoModifyIORef2 ref f = do
  r@(_old, (_new, _res)) <- pseudoModifyIORef2Lazy ref f
  return r

pseudoModifyIORef :: IORef a -> (a -> (a,b)) -> IO b
pseudoModifyIORef ref f = do
  (_old, ~(_new, res)) <- pseudoModifyIORef2 ref f
  pure res

david :: IORef a -> (a -> (a,b)) -> IO b
david ref f = do
    (_old, (_new, !res)) <- pseudoModifyIORef2 ref $
        \old -> case f old of
            r@(!_new, _res) -> r
    pure res

動作順序を調べてみる:

> ref <-newIORef (0::Int)
> david ref f'
swap
new
tupple
ret

"swap" が一番最初に出力されているのが分かる。

TLSの符号化

TLSのデータがどう符号化(シリアライズ)されるかのメモ。簡単にいうと可変長配列だけ先頭に「長さ」が付き、それ以外はそのまま。

基本型

たとえば、ProtocolVersionは uint16 と定義されている。

uint16 ProtocolVersion;

TLSのバージョン1.3の値は、0x0304であり、そのままネットワークバイトオーダー(ビッグエンディアン)で符号化される。

03 04 # TLS 1.3

固定長配列

固定長配列は、角括弧を使って表す。

たとえば、CipherSuiteが以下のように2バイトで定義されている。

uint8 CipherSuite[2];

TLS_AES_256_GCM_SHA384 は 0x1302 と定義されている。

13 02 # TLS_AES_256_GCM_SHA384

可変長配列

可変長配列は、小なり大なりで表す。小なり大なりの中に、長さの最大値が指定されるので、その分の長さを最初に付ける。

CipherSuiteの可変長配列が、後述の構造体の中で、以下のように定義されているとしよう。

CipherSuite cipher_suites<2..2^16-2>;

cipher_suitesはフィールド名である。216 とあるので、長さは2バイトだと分かる。

00 06 # 以下6バイト
13 02 # TLS_AES_256_GCM_SHA384
13 01 # TLS_AES_128_GCM_SHA256
13 03 # TLS_CHACHA20_POLY1305_SHA256

構造体

構造体は struct を使って表す。符号化は、単に上から順に書き出せばよい。

たとえば、ClinetHello は以下のように定義されている。

struct {
    ProtocolVersion legacy_version = 0x0303;    /* TLS v1.2 */
    Random random;
    opaque legacy_session_id<0..32>;
    CipherSuite cipher_suites<2..2^16-2>;
    opaque legacy_compression_methods<1..2^8-1>;
    Extension extensions<8..2^16-1>;
} ClientHello;

opaqueの定義はどこにもないけれど、単なるバイトだと理解する。Extension の定義はこう。

struct {
    ExtensionType extension_type;
    opaque extension_data<0..2^16-1>;
} Extension;

実際のバイト列と照らし合わせてみる。

03 03 # ProtocolVersion: TLS v1.2
96 f7 37 2d 59 66 76 40 68 90 f8 d2 f5 b0 e1 b1 # Random
4b ac 3a ed 7a 25 ab e0 d0 f5 22 15 64 52 51 7f
20 # Session Id Length
95 f8 f0 4d 5c c1 df 3d 72 70 8f c9 28 37 40 eb # Session Id
d0 13 a5 8d 06 fe f2 5e bf 1e 8e 55 fb 70 9a 59
00 06 # Cipher Suites Length
13 02 # Cipher Suite (TLS_AES_256_GCM_SHA384)
13 01 # Cipher Suite (TLS_AES_128_GCM_SHA256)
13 03 # Cipher Suite (TLS_CHACHA20_POLY1305_SHA256)
01 # Compression Methods Length
00 # Compression Method: (null)
00 c4 # Extensions Length
00 33 # ExtensionType (key_share)
00 47 # Extension Length
...
00 0b # ExtensionType (supported_versions)
00 09 # Extension Length
...
00 0d # ExtensionType (signature_algorithms)
00 0a # Extension Length
...
00 0a # ExtensionType (supported_groups)
00 04 # Extension Length
...
00 2d # ExtensionType (psk_key_exchange_modes)
00 03 # Extension Length
...
00 29 # ExtensionType (pre_shared_key)
00 4b # Extension Length
...

supported_versions の定義はこう:

struct {
    select (Handshake.msg_type) {
       case client_hello:
           ProtocolVersion versions<2..254>;
       case server_hello: /* and HelloRetryRequest */
           ProtocolVersion selected_version;
    };
} SupportedVersions;

select があってごちゃごちゃしているが、client_hello用に書き直すとこう。

struct {
    ProtocolVersion versions<2..254>;
} SupportedVersions;

これが Extension の opaque extension_data の部分となる。versionsは、可変長配列だから、長さがだぶったような感じに符号化される。

00 0b # ExtensionType (supported_versions) 再掲
00 09 # Extension Length 再掲
08 # Supported Versions Length
03 04 # TLS 1.3 (0x0304)
7f 1c # TLS 1.3 (draft 28)
7f 1b # TLS 1.3 (draft 27)
7f 1a # TLS 1.3 (draft 26)

Accepting UDP connections

When we implements UDP servers, a pair of recvfrom() and sendto() is used typically. Received UDP packets are dispatched, if necessary, to each connection by our server itself. We might want to delegate this job to the OS kernel for the performance reasons.

This article discusses how to create connected sockets from listening sockets on UDP. Unlike TCP, the accept() system call cannot be used for this purpose. Linux behaves differently from BSD variants. macOS (Monterey) and Windows (10) are buggy. We need to find a reasonable method which can work on all of these.

Terminology

  • Wildcard listening socket: {UDP, *:<service-port>, *:*}
  • Interface-specific listening socket: {UDP, <interface-address>:<service-port>, *:* }
  • Connected socket: {UDP, <interface-address>:<service-port>, <peer-address>:<peer-port>}

First approach based on interface-specific listening sockets

I don't remember why I first chose interface-specific listening sockets instead of wildcard listening sockets. But let's get started with this.

This method was fist described in Implementation status of QUIC in Haskell. Suppose we have a interface-specific listening socket, say {UDP, 192.0.2.1:443, *:*} and peer's address:port is 203.0.113.1:50000.

The following method does not work.

  1. Create a new UDP socket with SO_REUSEADDR ({UDP, *:*, *:*})
  2. Bind it to 192.0.2.1:443 ({UDP, 192.0.2.1:443, *:*})
  3. Connect it to 203.0.113.1:5000 ({UDP, 192.0.2.1:443, 203.0.113.1:5000})

Unfortunately, BSD variants reject (2). Linux accepts (2) but race condition would happen. The improved process is as follows:

  1. Create a new UDP socket with SO_REUSEADDR ({UDP, *:*, *:*})
  2. Bind it to *:443 ({UDP, *:443, *:*})
  3. Connect it to 203.0.113.1:5000. ({UDP, 192.0.2.1:443, 203.0.113.1:5000})

This process succeeds even on BSD variants because there is no duplicated entries at anytime. And there is no race conditions on any platforms.

A bug of the first approach

If a server have multiple interface-addresses of the same protocol family, connect() would select a wrong address. Suppose we have another interface-specific address, say 192.0.2.2.

  1. Create a new UDP socket with SO_REUSEADDR ({UDP, *:*, *:*})
  2. Bind it to *:443 ({UDP, *:443, *:*})
  3. Connect it to 203.0.113.1:5000. ({UDP, 192.0.2.2:443, 203.0.113.1:5000})

Since we cannot specify a local address in 2) and 3), 192.0.2.2 is selected in this example.

Another approach based on wildcard listening sockets

This bug does not exist if we use wildcard listening sockets. Suppose we have ({UDP, *:443, *:*}).

  1. Create a new UDP socket with SO_REUSEADDR ({UDP, *:*, *:*})
  2. Bind it to 192.0.2.1:443 ({UDP, 192.0.2.1:443, *:*})
  3. Connect it to 203.0.113.1:5000 ({UDP, 192.0.2.1:443, 203.0.113.1:5000})

This process succeeds because there is no duplicated entries at anytime. And a local address is specified explicitly.

When the first packet of a connection arrives, recvfrom() only tells you <peer-address>:<peer-port>. To know an interface-specific address, recvmsg() should be used. struct in_pktinfo and struct in6_pktinfo contain the interface-specific address for IPv4 and IPv6, respectively.

Integrating two approaches

Two approaches can co-exist. Step 2) binds anyaddr if an interface-specific listening socket is used. It binds an interface-specific address if a wildcard listening socket is used. Note that the bug above exists if an interface-specific listening socket is used.

recvmsg() of macOS is buggy. If it is used for an IPv4 interface-specific listening port, it changes the local address to anyaddr. IPv6 is OK. So, recvmsg() should be used only for a wildcard socket and recvfrom() should be used for an interface-specific address.

Bug, bug and bug

On OpenBSD, IPV6_V6ONLY is always enabled and cannot be changed. So, we should not use IPv4-IPv6 integrated sockets for the purpose of cross-platform. We should prepare IPv6-only sockets and IPv4-only sockets for listening.

This is not used but recvmsg() on Windows is also buggy. Suppose that recvmsg() used for an IPv4-IPv6 integrated socket and an IPv4 packet is received. struct in6_pktinfo (for IPv4-mapped IPv6 address) is not returned.

Windows

On Windows, connected socket can be used for sending. But for receiving, the dispatching in the kernel works very poorly because matching is done based on local address and local port only (destination only).

  1. Interface-specific listening sockets and connected sockets have the same priority
  2. They wins wildcard listening sockets
  3. For tie break of 1), the first-created one wins.

Suppose we don't' give up connected sockets for sending.

Since packet dispatching to connected sockets in the kernel are impossible, a listing socket should catch all received packets and the server should dispatch them by itself. But wildcard listing sockets cannot be used for this purpose because of 2). On Windows, interface-specific listening sockets should be used. They win against connected sockets because they are created at the beginning.

Integrating Fusion and cryptonite in Haskell quic

While Haskell quic version 0.0.1 or earlier supports the x86_64 architecture only, version 0.0.2 or later will supports non-x86_64 architectures.

cryptonite

When I started implementing the quic library in Haskell, I used cryptonite as crypto engine. Since cryptonite takes the functional style where all results are generated newly without side-effects, it is safe to use it among multiple threads.

I defined the following data for payload encryption and decryption:

data Coder = Coder {
    encrypt :: PlainText  -> AssDat -> PacketNumber -> [CipherText]
  , decrypt :: CipherText -> AssDat -> PacketNumber -> Maybe PlainText
  }

Key and Nonce are partially applied at the initialization period and resulting functions are stored in Code. encrypt returns two ByteString in a list, one is a protected header and the other is an encrypted payload. PlainText, CipherText and AssDat (associative data) are ByteString essentially.

For header protection, I defined the following code:

data Protector = Protector {
    protect   :: Sample -> Mask
  , unprotect :: Sample -> Mask
  }

Sample is taken from an encrypted payload and resulting Mask is used for the xor operation on a header.

Fusion

The maximum record size of TLS is 16KiB. After 16KiB plain text is encrypted and sent from the TLS layer, the TCP layer fragments the cipher text into multiple IP packets. Since 16KiB is large enough, cost of initialization and finalization is invisible.

However, the maximum packet size of QUIC is 1,472 bytes. There was no crypto engine to encrypt and decrypt such small data efficiently. Kazuho Oku invented Fusion crypto engine to solve this problem. I decided switch from cryptonite to Fusion in March 2021. New Coder is as follows:

data Coder = Coder {
    encrypt :: Buffer -> BufferSize -> Buffer -> BufferSize -> PacketNumber -> Buffer -> IO Int
t
  , decrypt :: Buffer -> BufferSize -> Buffer -> BufferSize -> PacketNumber -> Buffer -> IO Int
t
  }

The three buffers are input, associative data and output in order. The output buffer is to be modified. The result is the output size. Fusion encryption also calculates the mask of header protection (but not for unprotection). Protector is changed as follows:

data Protector = Protector {
    setSample :: Buffer -> IO ()
  , getMask   :: IO Buffer
  , unprotect :: Sample -> Mask
  }

setSample and getMask are called before and after encrypt, respectively.

Integration

I misunderstood that Fusion uses a slow crypto engine on non-x86_64 architectures. After releasing quic version 0.0.0, I noticed this is not the case. So, I started integrating Fusion and cryptonite in this December. That is, Fusion should be used on the x86_64 architecture while cryptonite should be used on non-x86_64 architectures.

This project was tough since I needed to integrate the functional style and the imperative style. The key idea is to use cryptonite in the imperative style. The body of resulting ByteStrings are copied into a buffer. This buffer approach should be cerebrated by GSO(Generic Segmentation Offload) in the near future.

The resulting Coder and Protector are as follows:

data Coder = Coder {
    encrypt    :: Buffer -> PlainText  -> AssDat -> PacketNumber -> IO Int
  , decrypt    :: Buffer -> CipherText -> AssDat -> PacketNumber -> IO Int
  , supplement :: Maybe Supplement
  }

data Protector = Protector {
    setSample  :: Buffer -> IO ()
  , getMask    :: IO Buffer
  , unprotect :: Sample -> Mask
  }

I'm satisfied with this signature.

Bugs found

One single buffer is used for decryption in a QUIC connection. So, this buffer should be occupied by the receiver thread. However, the dispatcher thread of a server calls decrypt with this buffer in the migration procedure. Migration is rare but this is a possible race. In the current code, receiver takes care of migration to kick the race out.

In the integration process, I sometime used undefineds as initial values in tentative data structures of Coder. After initializing Coder, we cannot touch the undefineds. However, the test suite was trapped. This reveals that dropping keys (which stores the initial values again) is too early. The current code delays the key drop timing.

If the cryptonite is used, the test suite sometime gets stuck. Visualizing packet sequences illustrates that a client is waiting for Fin forever since a server does not send Fin. In the server side, the server thread and sender thread are racing. Since encrypt called by sender is slow, the server is finished before sender actually sends a packet. So, I modified that shutdownStream and closeStream wait for the completion of Fin packet sending.

我田引水的な「関数プログラミングの入門」資料紹介

これは、Haskell Advent Calendar 2021の2日目を埋めるために書いた記事です。実は単に僕が作った「関数プログラミングの入門」の資料の宣伝です。

ちなみに、僕の関数プログラミングの定義は「不変データプログラミング」であり、おそらく最も厳しい定義です。なので内容が分かれば、関数プログラミングに入門できた言ってもよいのではないかと思います。

関数プログラミングことはじめ

僕は毎年、岡山大学の三年生に向けて、2コマで関数プログラミングを教えています。その資料が、「Cプログラマーのための関数プログラミングことはじめ」です。岡山大学工学部情報系学科の学生は、C言語を習っているので、C言語に似た文法を独自に定義して、関数プログラミングを説明しています。

[入門]関数プログラミング

[入門]関数プログラミングは、WEB+DB PRESS Vol.67に掲載された記事です。編集部のご厚意により、オンラインで公開していただきました。関数型言語は記述力が高いので、この少ない分量で、赤黒木やパーサの実装まで辿り着けています。利用している言語はHaskellなので、Haskellの入門としても使えます。

再帰ドリル

再帰ドリルは、宮崎大学での集中講義の資料です。関数プログラミングとは切っても切り離せない再帰をドリル形式で解説しています。分かりやすく、包括的な内容になっていると思います。使用言語はHaskellです。コードを読めば、Haskellでのテストの書き方も習得できるかもしれません。

GHCのIOマネージャの歴史と僕の苦悩

これは、Haskell Advent Calendar 2021 の8日目の記事です。

Haskellコンパイラとして事実上一択となったGHCには、「軽量スレッド」が実装されています。軽量スレッドは、ネイティブスレッドよりも軽量なスレッドで、他の言語では「グリーンスレッド」とも呼ばれています。Haskellerが並行プログラミングをするときは、軽量スレッドを息を吸うかのように使います。

複数の軽量スレッドの入出力を束ねるのが、IOマネージャです。IOマネージャも単なる軽量スレッドであり、OSから入出力のイベントを受け取り、それぞれの軽量スレッドにイベントを通知します。

軽量スレッド(っぽい)機能を提供する他の言語では、GHCのIOマネージャを参考にしているようです。僕はIOマネージャの開発に深く関わっています。この記事ではIOマネージャの歴史をまとめるとともに、主にmacOSでの実装に関する苦悩を備忘録として残します。

第1世代

  • 論文 "Extending the Haskell Foreign Function Interface with Concurrency" で解説されています。
  • 2005年3月にリリースされたGHC 6.4で導入されました。

pollシステムコールを使って実装されています。pollはイベントの登録と監視が一体となったシステムコールです。このため、ある軽量スレッドが新たに入出力の登録を依頼する場合は、ブロックされているIOマネージャを一旦起こさないといけません(pollをやめさせなければなりません)。

これを実現するための画期的なアイディアが、wakeupパイプです。IOマネージャは、監視対象としてwakeupパイプも指定して、pollを呼びます。軽量スレッドがwakeupパイプにバイト列を書き込めば、IOマネージャはpollから抜けることができ、新たなイベントを加えて再度pollを呼び出します。

pollシステムコールの弱点は以下の通りです。

  • 登録できるイベントの個数に制限がある。
  • 受け取ったイベントを線形探索する必要があるので、イベントの個数が多くなるとスケールしない。

第2世代

  • 論文 "Scalable Event Handling for GHC" で解説されています。
  • 2010年11月にリリースされたGHC 7.0で導入されました。

pollの問題点を克服するために、Linuxではepoll、BSDではkqueueを使うようになりました。epollやkqueueは、pollのスーパーセットという限定的な利用方法が用いられています。

第2世代のIOマネージャの欠点は、マルチコア環境で性能が出ないことです。この理由としては、グローバルロックが使われていたことなどが挙げられます。

第3世代

  • 論文 "Mio: A High-Performance Multicore IO Manager for GHC" で解説されいます。
  • 2014年4月にリリースされたGHC 7.8で導入されました。

マルチコア環境ででスケールさせるために、グローバルロックが分割されると共に、コアごとにIOマネージャが起動されることになりました。

また、デフォルトではwakeupパイプは利用されなくなりました。これが実現できるのは、epollやkqueueが、イベントの登録と監視を独立させているからです。たとえば、epollでは登録がepoll_ctlで、監視はepoll_waitを使います。IOマネージャがepoll_waitでブロックされていても、別のスレッドはepoll_ctlでイベントを登録できるのです。登録されたイベントは削除しないといけませんが、削除の手間を省くために、使われたら削除される「one shot」というモードが利用されています。

Windows

WindowsのIOマネージャに関しては詳しくないので、New Windows I/O manager in GHC 8.12を参照してください。GHC 8.12は、GHC 9.0に置き換えて読んでください。

嗚呼、macOS

第3世代のIOマネージャは、Andreas Voellmyさんがepoll版を開発し、僕がkqueueに移植しました。この移植後、macOS上でGHCの並列ビルドが失敗するようになるという問題が発生しましたBSDでは問題ないのに、macOSでは問題が生じます。この問題は結局解決できずに、macOS上ではone shotモードを諦めて、wakeupパイプを使い続けるという方法で回避されました。(FreeBSDなどでは、one shotモードが使われています。)

その後、kqueueのIOマネージャに書き込みイベントの登録が失敗するというバグが発見されます。epoll_ctlの EPOLLINEPOLLOUT はフラグ(ビットマスク)ですが、kqueue の EVFILT_READEVFILT_WRITE はフラグではありません。kqueueで読み書き両方を登録するには、読み込みイベントと書き込みイベントの2つを登録する必要があります。移植の際に、この事実に気づかずバグを入れ込んでいました。one shot用のコードでも、wakeupパイプ用のコードでも、このバグは直されました。GHC 8.4から恩恵にあずかれます。

これでmacOSの並列ビルドの問題も解決したと勘違いして、macOSでone shotモードを使おうという提案をしました。しかし、macOSでone shotを使うようになったGHC 9.0.1をmacOSで使ってみると、ネットワーク関係のライブラリでテストが通らなくなっていました

結局、macOSのkqueueには、one shotにバグがあるというのが僕の結論です。GHC 9.0.2では、再びwakeupパイプが利用されるようになります。

幅80cmで作る在宅勤務環境

ウチは6人家族なので、コロナ禍での在宅勤務は本当に手狭です。最終的には、あまり人の来ない寝室の敷布団の上に座って壁に寄りかかり、MacBook Proを膝に置いてプログラミングをしていました。

会社では標準的なオフィスチェアに座り、iMacの広い画面を見ながら、Happy Hacking Keyboard(HHKB)を2枚使って腕を肩幅に開いて快適に作業していました。布団に座っているのだと腰は痛くなるし、キーボード1枚では肩はこるし、仕様書を見ながらプログラミングするにはMacBook Proの画面は小さくて苦しいしで、「なんとかならないかなぁ」という状況でした。

最近、嫁が家を片付けて不要な物を捨ててくれたんですが、ふと見ると幅80cm可変棚の下にスペースができてるではありませんか。このスペースを仕事場としてもらって、遅ればせながら仕事の環境を整えることにしました。既存の持ち物をできるだけ利用して作った仕事環境の最終形はこんな感じです。

f:id:kazu-yamamoto:20210928060829j:plain
在宅勤務環境最終形

誰かの役に立つかもしれないので、備忘録も兼ねて、何を考えて環境を整えたのか記しておきます。

捨てようと思っていたIKEAの机を再利用することにしました。幅が110cmぐらいあるので、80cmに切断します。右側の引き出しは全部諦めて、天板をノコギリで切ってみるとスカスカで、拍子抜けがする程あっという間に切れました。

f:id:kazu-yamamoto:20210831094028j:plain:w250
右側の引き出しを取り除いたIKEAの机
f:id:kazu-yamamoto:20210831094924j:plain:w250
天板はスカスカ

ディスプレイ

次はなんといってもディスプレイです。大きさは、幅が61cmの27インチが丁度よさそうで、解像度はWQHD(2560x1440)で十分かなぁと思いました。いろいろ記事を読んでいると、最近だとUSBのType-Cケーブル一本で結ぶだけで、ノートブックに給電までできることを知りました。そんなとき、『EIZO「FlexScan EV2795」で理想を超える仕事環境が! ケーブル1本のデュアルディスプレイからパソコン3台の集約も』という記事を見付けて感動し、FlexScan EV2795の黒を買うことにしました。

僕のEV2795には以下のケーブルが刺さっています。

MacBook Proには、これらの(モニターライトを除いた)全ての情報/電力がType-Cから供給されます。EV2795には概ね満足していますが、Retinaディスプレイに慣れている目からすると、荒いドットが見えたりしてがっかりします。あと、前面下にあるソフトスイッチは、暗いときにも分かるように少し光って欲しかったです。

追記:Type-Cにはアップストリームとダウンストリームの2つがあります。パソコンはアップストリームに繋ぎます。マニュアルには、ダウンストリームは別のEV2795をデイジーチェインで繋ぐため用と書かれています。しかし、単に給電にも使えました。マニュアルにもそう明記してあるべきですね。どなたか、PDに対応しているかチェッカーで調べていただきたいです。

ディスプレイ台

EV2795のスタンドはよくできているんですが、僕はディスプレイの中心を机から50cmぐらいにもってきたいので、高さがあと10cmぐらい足りません。そこで、モニターアームを調べてみました。ヨドバシで実際に触ってみると、50cmも持ち上げられないことに気づきました。モニターアームは、モニターを頻繁に動かす人には便利ですが、そもそも僕は動かさないので本当に必要なのか分からなくなっていました。そんなとき、「モニターアームを使って気づいたメリット・デメリット。モニターアームの選び方。」という記事を見付け、高級なディスプレイのスタンドがあればモニターを好きな位置や角度に固定できるのでモニターアームは不要という意見を聞いて納得しました。

結局10cmぐらい持ち上げられればいいので、ディスプレイ台で十分という結論に達し、BoYataのモニタースタンドを購入しました。これだと、書類を書くときにキーボードをモニタースタンドの下に押し込むことができて快適です。また、後から出てきますが、スタンドをクリップを挟む場所としても使えます。

キーボード

とりあえず、Happy Hacking Keybordの2枚利用を継続しました。Windowsは標準で2枚のキーボードに対応していますが、MacだとKarabiner-Elementsをインストールする必要があります。

机の上にケーブルが2つあるのは邪魔です。とりあえず、USB miniBのL字コネクタを買い、2つのケーブルを見栄えよく中央でまとめられるようにしました。買った左右のL字コネクタは、「変換名人」のUSBM5-LLFとUSBM5-RLFです。amazonでは1個で販売してないので、ヨドバシで買うのがいいと思います。

f:id:kazu-yamamoto:20210925080700j:plain:w250
右キーボードのUSBケーブルを中央に持ってくる

やっぱり机を広くするにはワイヤレスの分割キーボードが必要です。いろいろ調べたのですが、よさそうなのは自作キーボードしかありませんでした。子育てが忙しいので、ハンダ付けは勘弁して欲しいです。Mistel BAROCCO MD770にワイヤレスタイプがあるらしく、ヨドバシでワイヤレスではないMD770を触ってみましたが、どの色の軸も手に馴染みませんでした。メカニカルキーボードは自分には合わないと分かったとき急激にキーボード熱が冷め、結局HHKB 2枚が一番いいという結論に落ち着きました。

キーボードに関しては、『 疲れないキーボードを知りたい。タイピング早打ち日本一の「女王」miriさんに聞いてみた』という記事が入門としては秀逸です。

2つのケーブルをまとめるのには、ルフラップスリーブ(内径8mm)を使いました。

キートップ引き抜き工具を買ったら、娘が面白がってやってキーボードを掃除してくれました。次の目標は、黄ばんだHHKBを流行りのホワイトニングで白くすることです。

マウス

こだわりがなくワイヤレスならなんでもいいので、その辺にあったAppleMagic Mouseを使っています。

充電用のLightningの穴が下にあり、充電中は利用できないのは有名な話ですね。

僕はタイピングのときに、手のひらの付け根を机に置いています。机と接触する部分は汗ばんでくるので、これをなんとかしたく、80cmのマウスパッドを買いました。

パソコン立て

MacBook Proは閉じた状態、いわゆるクラムシェルモードで使っています。クラムシェルモードでは、熱を逃すために縦置きにした方がいいようです。僕の環境では机が狭いので必然的に立てることになりますが、左右に壁があるのでそれに立て掛ければ十分です。でも、ヨドバシでTwelve SouthのBookArcを見て欲しくなってしまったのです。

「単なるアルミに6,800円も払えるか!」と葛藤して、ついにかっこよくて安い「AVLTのノートパソコン縦置きスタンドホルダー」を見付けて買いました(現在、amazonでは売ってないようです)。娘には「こんなのが2,300円もするの?」と言われました。

MacBook Proを壁に寄せるにはType-CのL字コネクタも必要です。Type-Cだと左右がないのがいいですね。僕はカモンのUC-Lを買いました。ちょっときつめです。

f:id:kazu-yamamoto:20210925081239j:plain:w250
Type-CのL字コネクタを使って壁に寄せる

モニターライト

FlexScan EV2795の背中は丸みを帯びているので、モニターライトは設置できるか慎重に選ばないといけません。結局、元祖であるBenQ ScreenBarにしようかなぁと思っていたときに、Exarm Zetaの存在を知りました。高いけれどライト専門の会社だし、日本の会社でサポートもしっかりしていそうなので、これを買いました。

いろんな記事に、暗い部屋でディスプレイだけ付けていると目が疲れるという意見が書かれていました。僕は気にしてなかったのですが、確かにモニターライトが机を照らし、反射光がディスプレイの後ろの壁を照らして全体的に明るいと、目が疲れにくい気がします。

届いたときは軸がずれていて、どうやっても向かって右側が下に傾いていました。スワン電器のサポートからは、着払いで送ってくれれば調整するとのことだったので、さっそくお世話になりました。やはり、日本の会社にしておいてよかったです。

FlexScan EV2795には、下側に主電源があり、前面下にソフトスイッチがあります。ソフトスイッチを消すと画面が消えると共にUSBの給電が止まるので、モニターライトが消えます。逆にソフトスイッチを入れると給電が始まりますが、Exarm Zetaは横の飾りのライトが付くだけです。メインのライトを付けるには、センサーに手をかざす必要があります。

僕の環境のようにExarm Zetaのすぐ上に白い棚があると、ここにiPhoneの光が反射して、メインのライトが付いたり消えたりと誤動作します。まぁ、これは仕様なので仕方ないですね。僕にはセンサーのスイッチ機能は不要でした。

リモート会議環境

リモート会議に関してまず気になるのが、マイクとスピーカーです。いろんなパターンを試しましたが、iPhoneに付いてくるマイク付きのイヤホンをMacBook Proに挿して使うことにしました。これだとハウリングしません。

Webカメラを買うのはもったいないので、EpocCamを購入してiPhoneWebカメラにしました。結局、iPhoneのマイクは使わないことになったので、無償版の方でも十分だった思います。

Exarm Zetaには顔用のライトが付いていますが、これだと光量が足りません。iPhoneのホルダーも付いているリングライトを買うことにました。ヨドバシに置いてあったエレコムのモニターライトが腕がしっかりしていてよさそうでした。クリップタイプを買って、モニター台にクリップ止めしています。厚みが足らなかったので、黒く塗った板をクリップに挟んでいます。

電源タップ

電源タップは不要といえば不要だったのですが、配線をスッキリさせるために以下を満たすものを探しました。

  • ディスプレイの3ピンプラグが刺さること
  • 口の数は、余裕を持って3つあること
  • Type-A で給電できること
  • PD 対応の Type-C の口があること

これを満たすのは Anker PowerPort Strip PD 3 でした。amazonでは、「給電できなくなる」という悪い評判が立っていましたが、Ankerなので買ってみました。今のところ、iPhoneが急速充電できています。

【レビュー】Anker『PowerPort Strip PD 3』:机固定に最適なUSB-C搭載マルチ電源タップ」を読むと、Type-C の PD は若干仕様に準じていないようです。自分で作った規格ぐらい準拠しましょうよ、Anker さん

スイングプラグではないので、アダプターも必要です。

椅子

ゲーミングチェアが欲しかったので、ニトリにもあると聞いて行ってみました。ゲーミングチェアに座ると、なるほど柔らかくて長く座っていられそうですが、腰はパッドで単に押されているだけで楽には感じませんでした。隣にあった、デュオレハイは不恰好でなんじゃこれって感じだったのですが、試しに座ってみると衝撃を受けました。腰を持ち上げてくれ、重力が弱くなった感じで、とっても楽なんです。ニトリの商品には、デュオレハイ 2デュオレハイ DXがあります。

デュオレハイ2:

  • 肘の部分が固定
  • 首が高くならない
  • 平日だと送料がかからない

デュオレハイ DX:

  • ポリエステル
  • 肘の高さが調整できる
  • 首が高くなる
  • 平日でも送料がかかる

僕は身長が182cmあるので、デュオレハイ DX一択でした。本当は皮がよかったんですが。。。しばらく使っていますが、本当に腰が楽です。

昇降デスク (野望)

もし、もう少し広いスペースがあれば、昇降デスクが欲しいです。重いものを乗せないのでシングルモーターでよく、コストパフォーマンス的にはMAIDESITEの昇降デスクがよさそうです。天板は厚み2.5cmの一枚板です。注目していたら、ここ1ヶ月で高評価のレビューがたくさん付くようになりましたね。タイムセールで安く買えるときもあります。110cmの黒色が出れば最高なんですが。サポートが気になるので、レビューを見守りたいと思います。

所感

  • 幅80cmのスペースがあれば、なんとかなる
  • キーボードと椅子だけは、実際に触ったり座ったりしてから買いましょう