kakts-log

programming について調べたことを整理していきます

redis のsetのnxオプションについて

概要

redisでsetコマンドのnxオプションというものがあり setする際に、キーがまだ存在していない場合にのみ値をセットできるもので、redisによるロック処理に使われます。 このnxオプションについて調べてみます。

SET | Docs

NX -- Only set the key if it does not already exist.

setコマンドでのnxオプションをつけてみる

redis-cli を実行し、redisに対して set とNXオプションとEXによるTTLを設定し 簡単なロック機能を試してみます。

set key value NX EX 10 というコマンドを実行します。
- NX: コマンド実行時に既にキーが存在していない場合のみsetできる。 そうでない場合はnil
- EX 10: 10秒間のTTLを設定する。 キーを登録してから10秒を超えるとキーに紐づくデータが消える

上記のコマンドにより、10秒間のロック機能を実現します。

redis-cli
127.0.0.1:6379> set key value NX EX 10
OK # ここでロックがかかる
127.0.0.1:6379> set key value NX EX 10
(nil)
127.0.0.1:6379> set key value NX EX 10
(nil)
127.0.0.1:6379> set key value NX EX 10
(nil)
127.0.0.1:6379> set key value NX EX 10
(nil)
127.0.0.1:6379> set key value NX EX 10
(nil)
------ ここまでロックをかけてから10秒間の間は set nxをしてもnilとなる
127.0.0.1:6379> set key value NX EX 10
OK # 10秒経ったので新たにロックをかけることができる

これにより、キーがない状態で set key value NX EX 10 を実行してロックをかけ、その後10秒間は 同じコマンドを実行してロックを獲得しようと試みても失敗することを確認できました。

redisがシングルインスタンスの場合、このset でのNXとEXオプションをつけることで簡単なロック機構を実現できます。

set nxオプションが付いていた場合の内部実装について

nxオプションをつけた時にsetコマンドではどういう処理になっているかを説明します。

redis本体のソースコードでの実装箇所は、 src/t_string.cのsetGenericCommand 関数から辿ることができます。

redis/src/t_string.c at 7.2.5 · redis/redis · GitHub

set実行時のNXオプションがついているかの判定

set実行時のオプション引数のパース処理をみてみます。

パース処理はparseExtendedStringArgumentsOrReply 関数で実装されています。 redisに対するコマンドの引数をパースして、 NXオプションが設定されていたら SET NX用のフラグ OBJ_SET_NX を立てる処理を行っています。

...
        if ((opt[0] == 'n' || opt[0] == 'N') &&
            (opt[1] == 'x' || opt[1] == 'X') && opt[2] == '\0' &&
            !(*flags & OBJ_SET_XX) && (command_type == COMMAND_SET))
        {
            *flags |= OBJ_SET_NX;
        } 
...

redis/src/t_string.c at f60370ce28b946c1146dcea77c9c399d39601aaa · redis/redis · GitHub

OBJ_SET_NXフラグがたっている場合の setコマンドの実行

続いて、NX用のフラグが立っている場合にsetコマンドでどういう処理が行われるかをみてみます。

setコマンドの処理はsetGenericCommand 関数で実装されています。

関数の前半の方で、まず指定したキーがデータに存在しているかを確認し、存在していた場合はアーリーリターンして、コマンドの実行を終了し、その後にデータを登録する処理はスキップされます。 これにより、既にキーが存在していた場合 setでnxオプションをつけて実行された場合はデータの登録はされずにスキップされることになります。

    // set予定のキーが登録済みか確認する
    found = (lookupKeyWrite(c->db,key) != NULL);

    // NXフラグまたはXXフラグが設定されていて、キーが見つかった場合
    if ((flags & OBJ_SET_NX && found) ||
        (flags & OBJ_SET_XX && !found))
    {
        if (!(flags & OBJ_SET_GET)) {
            addReply(c, abort_reply ? abort_reply : shared.null[c->resp]);
        }
        return;
    }

// この後、setの処理が続く
...

以上となります。 EXまたはPXオプションを設定した場合、指定した時間が経ったらキーの値が削除され、ロックが解除されることになります。
その後再度ロックをかけることができます。

Go: for rangeにおけるmapのイテレーション順序について

概要

Goにおけるfor rangeでmapをループさせる際のイテレーション順序について整理します。

Goにおいて、for rangeでmapの要素をループさせる際、イテレーションの順序は決まっていません。
これはGo言語の仕様で定められており、ループ順序を前提としたコードを書かないために定められています。

When iterating over a map with a range loop, the iteration order is not specified and is not guaranteed to be the same from one iteration to the next. If you require a stable iteration order you must maintain a separate data structure that specifies that order.

go.dev

この順序が実際にどう決まっているかについて、Goのランタイムでどういう実装になっているかを整理します。

前提 - Go v1.22.4

for rangeにおける mapのループの例

ここでは簡単に、mapをfor rangeによってループさせる例を示します。

package main

import "fmt"

func main() {

    m := map[string]int{
        "a": 100,
        "b": 200,
        "c": 300,
        "d": 400,
        "e": 500,
    }

    for k, v := range m {
        fmt.Printf("%s : %d \n", k, v)
    }
}

mapの要素をイテレートして、そのキーと値を表示させます。

これを実行すると、実行するごとに要素の順番が変わっていることを確認できます。

$ go run main.go
b : 200 
c : 300 
d : 400 
e : 500 
a : 100 

$ go run main.go
a : 100 
b : 200 
c : 300 
d : 400 
e : 500 

$ go run main.go
c : 300 
d : 400 
e : 500 
a : 100 
b : 200 

$ go run main.go
c : 300 
d : 400 
e : 500 
a : 100 
b : 200 

mapのイテレーションで順序が保証されていない理由

この理由については、簡単にいうとGoの古いバージョンからの互換性を保つためのものとなります。
古いバージョンでは、もともとmapのイテレーション順について仕様で定義されていませんでした。そのため実行するハードウェアプラットフォームによって異なっていたようです。
これにより、mapをイテレーションするテストでは実行する環境でテスト結果が異なってしまい、環境によってテストが成功したりしなかったりするという課題がありました。

go.dev

Goのv1系では、for rangeによるmapのイテレーション順は不定であると定義されました。
そのため、順序を想定したmapのfor rangeを扱っている箇所は修正が必要です。

Goランタイムによる mapのイテレーション時の処理について

次に、実際にGoランタイムではmapのイテレーションの順番をどのように決めているかについて整理します。

ランタイムにおけるmapに関する実装は src/runtime/map.go で確認できます。

mapのデータ構造について

Goにおけるmapのデータ構造について説明します。

Goにおいては、mapはハッシュテーブルと同義です。map内にバケットが存在し、そのデータはバケットの配列に格納されます。
バケットは最大8つのキー・バリューのペアを含むことができます。 そのハッシュ値の下位のビットは、どのバケットを選択するかを決めるのに利用されます。
また、上位のビットは単一のバケット内のどのエントリかを決めるのに利用されます。

もしあるバケットで8より多い数のデータが追加された場合、追加のバケットを連結し、そこに格納されます。

ハッシュテーブルを増やす必要がある際、Goランタイムはその時点でのバケット数を2倍にし、新しい配列を持ったバケットを追加します。

mapのイテレータ用の構造体について

mapをイテレーションする際に使う hiter という構造体も用意されています。

go/src/runtime/map.go at ace5bb40d027b718b67556afcd31bf54cff050ab · golang/go · GitHub

// A hash iteration structure.
// If you modify hiter, also change cmd/compile/internal/reflectdata/reflect.go
// and reflect/value.go to match the layout of this structure.
type hiter struct {
    key         unsafe.Pointer // Must be in first position.  Write nil to indicate iteration end (see cmd/compile/internal/walk/range.go).
    elem        unsafe.Pointer // Must be in second position (see cmd/compile/internal/walk/range.go).
    t           *maptype
    h           *hmap
    buckets     unsafe.Pointer // bucket ptr at hash_iter initialization time
    bptr        *bmap          // current bucket
    overflow    *[]*bmap       // keeps overflow buckets of hmap.buckets alive
    oldoverflow *[]*bmap       // keeps overflow buckets of hmap.oldbuckets alive
    startBucket uintptr        // bucket iteration started at
    offset      uint8          // intra-bucket offset to start from during iteration (should be big enough to hold bucketCnt-1)
    wrapped     bool           // already wrapped around from end of bucket array to beginning
    B           uint8
    i           uint8
    bucket      uintptr
    checkBucket uintptr
}

この中で、 hiter.startBucketイテレーションの開始するバケットを選択するものになります。
イテレーションの初期化処理において、この値を設定する箇所があります。

mapのイテレーションの初期化

mapのイテレーションの初期化処理は、 mapiterinit という関数で実装されています。

go/src/runtime/map.go at ace5bb40d027b718b67556afcd31bf54cff050ab · golang/go · GitHub

この初期化処理の中で、map内のどのバケットから処理をするかを決めていますが、この際に乱数をもとにバケットを選択しています。

...
    // decide where to start
    r := uintptr(rand())
    it.startBucket = r & bucketMask(h.B)
    it.offset = uint8(r >> h.B & (bucketCnt - 1))
...

Goにおいて for range でmapのイテレーションの順番が不定となるのは、この処理によるものであることがわかります。

おわりに

以上となります。 この記事では mapについての概要の説明を行いました。
また、for rangeにおけるmapのイテレーションの順序は仕様で不定となっていて、Goランタイムの実装を確認し、実際にイテレーションの初期化処理で開始位置がランダムになっていることを確認できました。

今回は取り上げませんでしたが、Goランタイムの実装は非常に興味深いため、別の記事でも気になる箇所について取り上ようと思います。

Goコンパイラによるコンパイル処理について整理

概要

Goのコンパイラにおいて、コンパイル時にいくつかのフェーズに分かれており、各フェーズについてざっと概要を整理します。

github.com

Goコンパイラによるコンパイルでは、主に下記のようなフェーズをたどります。 - 字句解析 - 構文解析 - 型チェック - IR(中間表現)の生成 - IRの生成 - SSA(静的単一代入)形式への変換 - 最適化 - マシンコードの生成

Goコンパイルのフェーズ

字句解析と構文解析

  • cmd/compile/internal/syntax

github.com

コンパイルの最初のフェーズでは、ソースコードトークナイズ(字句解析)とパース(構文解析)され、それぞれのソースファイルに対してシンタックスツリーが構成されます。

シンタックスツリーはそれぞれのソースファイルを正確に表現したもので、ノードはソースのさまざまな表現(式・宣言・文など)に対応しています。

また、シンタックスツリーには位置情報も含まれており、エラーリポートやデバッグ情報の生成に使用されます。

型チェック

  • cmd/compile/internal/types2

github.com

type2 パッケージはgo/typesgo/astの代わりに構文パッケージのASTを使用するように移植したものです。

IR(中間表現)の生成 (”noding”)

  • cmd/compile/internal/types (compiler types)
  • cmd/compile/internal/ir (compiler AST)
  • cmd/compile/internal/noder (create compiler AST)

ここでは、構文解析と型チェックの結果を、コンパイラが理解できる中間表現(IR)に変換するプロセスを扱います。

Goコンパイラmiddle-end は自身のAST定義とCで書かれた時代から引き継いだGoの型の表現を使います。

そのコードの全ては、型チェック後の次のステップがその構文とtype2での表現を 中間表現と型に変換します。

このプロセスは “Noding” と呼ばれます。

Noding ではUnified IRと呼ばれるプロセスを用います。これはステップ2で型チェックが完了したコードをシリアライズしたものを利用して、Nodeを構成します。

Unified IRはパッケージのimport/exportやインライニングに関わります。

IRに対する最適化 (middle-end)

  • cmd/compile/internal/deadcode (dead code elimination)
  • cmd/compile/internal/inline (function call inlining)
  • cmd/compile/internal/devirtualize (devirtualization of known interface method calls)
  • cmd/compile/internal/escape (escape analysis)

中間表現に対して、いくつかの最適化プロセスが実施されます。

  • デッドコード削除
  • 早期の devirtualizeation
  • 関数呼び出しのインライニング
  • エスケース解析

IRに対する最終処理(Wark)

中間表現に対する最終的な処理は Walk と呼ばれ、2つの目的があります

  1. 複雑な文を個々のより単純な文に分解し、一時変数を導入し、評価の順序を尊重します。このステップをorder とも呼ばれます。
  2. 高レベルのGo構造をよりプリミティブな形式にde-sugaring します。 例えば switch 文はバイナリ検索やジャンプテーブルに変換したり、マップやチャネルの場合はランタイム呼び出しに置き換えられます。

SSA(静的単一代入)形式への変換

SSA形式 は、コンパイラの設計における中間表現(IR)の一つです。

各変数が一度のみ代入されるように定義されたものです。

SSAを利用することにより、コンパイラ最適化アルゴリズムを実現したり、改善をすることができます。

このフェーズでは中間表現は静的単一代入(SSA)形式に変換されます。これは低レベルの中間表現で、最適化を実装しやすくし、最終的に機械語を生成する特性を持ちます。

この変換の過程において、関数intrinsicsが適用されます。これはコンパイラがケースバイケースで十分に最適化されたコードに置き換えるように実装された特殊な関数となります。

ASTからSSAへの変換の間において、特定のノードもよりシンプルな低レイヤーコンポーネントに変化されます。これによって、コンパイラの残りの部分が処理できるようになります。

例えば、copyの組み込み関数はメモリ移動に置き換えられrangeループはforループに書き換えられます。

これらの一部は現在歴史的な理由からSSAへの変換前に行われていますが、将来的には全て cmd/compile/internal/配下に移る予定だそうです。

その後、特定のマシンに依存しないパスとルールが適用されます。これらは特定のコンピュータアーキテクチャに関係せず、全てのGOARCH バリアアントで実行されます。

これらのパスにはデッドコード除去、不要なnilチェックの削除、未使用のブランチの削除が含まれます。

ジェネリックな書き換えルールは主に式に関するもので、いくつかの式を定数値に置き換えたり、乗算や浮動小数点演算を最適化したりします。

マシンコードの生成

  • cmd/compile/internal/ssa (SSAへの変換とアーキテクチャに依存したパス)
  • cmd/internal/obj (マシンコードの生成)

コンパイラのマシン依存のフェーズは”低レベル化”パスから始まり、これは汎用的な値をマシン固有のバリアントに書き換えます

この低レベル化パスは全てのマシン固有の書き換えルールを実施するため、多くの最適化も適用されます。

SSA形式のコードが低レベル化され、ターゲットのアーキテクチャに特化した後、最終的なコード最適化パスが実行されます。

これには、さらにもう一度デッドコードの除去や、値を実際の使用箇所に近づけたり、読み取られないローカル変数の削除や、レジスタの割り当てが含まれます。

このステップの一部として行われるその他の重要な作業には、スタックフレームのレイアウト(ローカル変数にスタックオフセットを割り当てること)と、各GCのセーフポイントにおいて、どのスタック上のポインタが利用されているかを計算するポインタのliveness解析が含まれます。

SSA形式の生成フェーズの終わりには、Go関数はobj.Prog 命令に変換されています。

これらはアセンブラ(cmd/internal/obj ) に渡され、マシンコードに変換されて最終的なオブジェクトファイルが出力されます。

オブジェクトファイルには、リフレクトデータや、エクスポートデータ、デバッグ情報も含まれます。

さいごに

ざっとそれぞれのフェーズについて概要を説明すると以上となります。 最適化処理についてはまた別の機会に詳しく整理できたらと思います。

SystemV メッセージキューの作成とipcsを用いたメッセージキューの確認方法

概要

System V メッセージキューは、Linuxにおけるプロセス間通信方法の一種で、メッセージ形式のデータをプロセス間でやり取りするのに使われます。
主に下記3つがあり、今回はメッセージキューについて扱います。

今回はこのメッセージキューをシステムコールを用いて作成し、作成されたメッセージキューの確認方法について整理します。

msgget()システムコールによるSystem Vメッセージキューの作成

msgget()というシステムコールについて説明します。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);

keyに指定したIPCキーのメッセージキューを取得・作成します。
すでにキーが存在していたらそのキーのメッセージキューのIDを返します。
存在していなかったら新規作成して、その新規作成されたメッセージキューのIDを返します。

  • 第1引数 メッセージキューのキー名を指定する IPCオブジェクトのキーを指定します。 一意なキーを作成するには、 sys/ipc.h で定義されている IPC_PRIVATEを指定します。

  • 第2引数

    • 作成するメッセージキューへ設定するパーミッションマスクを指定します。 次の操作フラグを OR演算により指定可能です。
  • IPC_CREAT
    • keyに対応するメッセージキューが存在しない場合に新規作成する。
  • IPC_EXCL
    • IPC_CREATと併用し、keyに対応するメッセージキューが既存の場合にEEXISTエラーを返す

msgget()を使用したメッセージキューの作成例

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int msqid;

    // System Vメッセージキューの作成 既存の場合はそのメッセージキューIDを返す
    msqid = msgget(IPC_PRIVATE, IPC_CREAT);
    if (msqid == -1) {
        errExit("msgget");
    }
    printf("%d\n", msqid);
    exit(EXIT_SUCCESS);
}

これをコンパイルして、実行すると、作成されたメッセージキューIDを表示して終了します。

root@bca7d0e99ea7:/tlpi/svmsg# ./simple_svmsg_create 
0
root@bca7d0e99ea7:/tlpi/svmsg# ./simple_svmsg_create 
1
root@bca7d0e99ea7:/tlpi/svmsg# ./simple_svmsg_create 
2

作成された System V メッセージキューの確認方法

前項でmsgget()を使用してメッセージキューを作成しました。 作成されたメッセージキューの一覧を確認するには ipcsコマンドを実行します。

-q オプションを使うと、作成されたIPC関連のオブジェクトのうち、メッセージキューの一覧を表示できます。

root@bca7d0e99ea7:/tlpi/svmsg# ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x00000000 0          root       0          0            0           
0x00000000 1          root       0          0            0           
0x00000000 2          root       0          0            0         

このようにして、先ほど3回実行した分のメッセージキューが作成されていることを確認できました。

開発におけるドキュメント運用の理想状態と課題

概要

開発におけるドキュメントの運用方法について、自分なりの理想状態についてまとめます。

簡単にまとめると以下の3つとなります。

  1. ドキュメントが容易に見つかる
  2. ドキュメントの正確性
  3. ドキュメントの抜け漏れがないこと

上から順に実行難易度が下がり、容易に対策ができるはずで上から実行することを目指したいと思っています。
もっと項目を洗い出して整理はできると思いますが、一旦は上記を考えています。

それぞれの理想状態の項目についてと、それを実現するにあたり現状課題になりそうなポイントを整理します。

1. ドキュメントが容易に見つかる

  • ドキュメントが書かれているが、歴史的な経緯でドキュメントの場所や管理システム自体がバラバラになったりする。
  • 管理システムの移行が進まずに、古いドキュメントは過去に管理していたドキュメント管理システム上で残り続けてしまう。
  • 結果としてメンバーによってドキュメントの場所がわからなかったり、コードや他のドキュメントでのリンク切れが起きる。

対策

  • GitHubリポジトリのトップディレクトリのREADME.mdなど、リポジトリを最初に見た人が容易に見つけて辿れる状態にする 
    関連する全てのドキュメントはREADMEから容易に辿ることができる。
    リポジトリ外でドキュメント管理している場合でも、リンクを貼るだけでも効果があるため必ずやっておきたい。

  • ドキュメントの階層の構造化
    ドキュメントの置き場所と階層が構造化され、どの機能・システムに関するドキュメントがどこに書かれているか場所が推測できること
    また、その構造化についての情報も開発者のREADMEにまとまっていること
    これにより、近い将来LLMを使ったシステムでドキュメントを扱いやすい状態にし、chatbotなどで聞けば答えれるようにして、そもそもドキュメントを探さない状態に持っていく

2. ドキュメントの正確性

どこで書くか、どういう粒度で決まっており、特定のトピックに関しては特定のドキュメントで運用し続ける状態を目指す。
これが整っていると、メンバーによって同じ内容を別々の場所で書いてしまったり、内容の重複を防ぎ、運用がより回ると考えています。

対策

  • ドキュメントを書く粒度やテンプレートなどが定まっていること
  • ドキュメントの変更履歴がわかるシステム上で管理すること

3. ドキュメントの抜け漏れがないこと

開発案件の大小や、その時の開発チームのメンバーのタスク量によって、ドキュメントを書く・書かないがぶれてしまうことがある

対策

  • そもそも設計・開発においてドキュメントドリブンでの開発体制を敷く
    • 開発担当者個人の頭の中で設計をするのでなく、まずドキュメントにまとめて叩き台を作り、それに対して周りのメンバーのレビューを実施する
      • これによって設計の抜け漏れをなくす。
      • また、メンバー間でのシステム設計の理解度を揃えることができる。

ドキュメント運用上の課題

前述した運用体制を敷いた際に、個人的に運用上の課題になりそうなポイントがいくつかあるので、それについての課題を整理します。

リポジトリを跨いだ大きなシステムの全体構成・設計について

複数リポジトリにまたがるシステムに関するドキュメントはの運用をどうするか
あくまでGitHub内にとどめるなら、該当のGitHub org内にドキュメント用のリポジトリをまとめ、そこで管理するのが良いのか

システムに関する変遷について

開発初期から、アーキテクチャ変更など、途中段階も含めた一連のストーリーを理解する資料があると良さそう
GitHubリポジトリなどではコミットログなどから変更履歴はたどれるが一連の変遷を理解できるようにするとシステムの理解につながると思う。
-> システム上の意思決定の変更についてはArchitectural Decision Records(ADRs)での運用が最適?

さいごに

ドキュメントについて個人的な理想状態と課題を整理しました。

開発においてドキュメントが容易に見つかり、それらのドキュメントが常に正しい内容を維持しており、抜け漏れがない仕組み・体制を作れれば、開発メンバーのキャッチアップが容易になり、開発における心理的不安や、ドキュメント駆動でシステムの設計・開発品質向上のためのメンバー間でのコミュニケーション活発化を目指せるはずです。

チームのメンバーでそれぞれ理想状態と運用コストに関する考え方も異なるので、度々メンバー間で話して、運用方法をブラッシュアップできればと思います。

google/wireでDIする際のgenerics対応について

概要

この記事はQiita Advent Calendar 2023 Go言語の第十七日の記事です。

qiita.com

google/wireについて

github.com

google/wireとは、GoでのDependency Injection用のコード生成ツールとなります。
wire自体の使い方は、公式のチュートリアルとしてまとめられているため、そちらをご確認ください。 github.com

google/wireの現状について簡単にまとめると以下の通りになります。

Wire is currently in maintenance mode (i.e. not accepting new features) and investigation is needed for how much work is involved. Full support of go generics will probably not land anytime soon, but we welcome comments and fixes.

Goにおけるgenerics対応済みのDIツールは他に存在しますが、今回はgoogle/wireにおけるgenericsを扱う場合のワークアラウンドの方法についてまとめます。
 

genericsを用いたコードのDIでエラーが出る例

雑コードですが、下記のV interfaceを定義し、 Message 構造体にint型のフィールドを定義します

type V interface {
    int | float32
}

func NewV[T V]() T {
    return T(2)
}
package modelgen

import "fmt"

type Message struct {
    Msg string
    V   int
}

func NewMessage(v int) Message {
    return Message{Msg: "Hi there!", V: v + 1}
}

type Greeter struct {
    Message Message // <- adding a Message field
}

func NewGreeter(m Message) Greeter {
    return Greeter{Message: m}
}

func (g Greeter) Greet() Message {
    return g.Message
}

type V interface {
    int | float32
}

func NewV[T V]() T {
    return T(2)
}

type Event struct {
    Greeter Greeter // <- adding a Greeter field
}

func NewEvent(g Greeter) Event {
    return Event{Greeter: g}
}

func (e Event) Start() {
    m := e.Greeter.Greet()
    fmt.Println(m.Msg, m.V)
}

これをDIさせるために、wire.goで下記のように設定します。 Vインタフェースの実装に関するプロバイダ関数であるNewVをwire.Buildに型パラメータの指定なしで渡してみます。

func InitializeGenericsEvent() modelgen.Event {
    wire.Build(modelgen.NewEvent, modelgen.NewGreeter, modelgen.NewMessage, NewV)
    return modelgen.Event{}
}

この状態でwireコマンドを実行すると、下記のエラーとなり、型パラメータTが指定されていないとエラーが出ます。

$ make wire
wire
wire: /Users/hoge/Documents/code/go-wire-sandbox/wire.go:128:2: cannot infer T (/Users/hoge/Documents/code/go-wire-sandbox/src/modelgen/generics_models.go:30:11)
wire: generate failed
make: *** [wire] Error 1

次に、NewV[int]と型パラメータTにintを指定すると、unknown patternエラーがでて、 wireでは認識できないシンタックスエラーとなります。

// Generics動作確認用
func InitializeGenericsEvent() modelgen.Event {
    wire.Build(modelgen.NewEvent, modelgen.NewGreeter, modelgen.NewMessage, modelgen.NewV[int])
    return modelgen.Event{}
}
➜  go-wire-sandbox git:(main) ✗ make wire
wire
wire: /Users/hoge/Documents/code/go-wire-sandbox/wire.go:128:74: unknown pattern
wire: github.com/kakts/go-wire-sandbox: generate failed
wire: at least one generate failure
make: *** [wire] Error 1

解決策: 型パラメータを指定してDIさせる方法

上記の問題がありますが、下記の通り、型パラメータを指定して関数を呼び出した結果を返すプロバイダー関数(NewVInt)を定義し、それをwire.Buildに渡してあげると解決します。

func NewVInt() int {
        // intを型パラメータに指定してNewVを実行し、その戻り値を返す
    return modelgen.NewV[int]()
}

// Generics動作確認用
func InitializeGenericsEvent() modelgen.Event {
    wire.Build(modelgen.NewEvent, modelgen.NewGreeter, modelgen.NewMessage, NewVInt)
    return modelgen.Event{}
}
$ make wire
wire
wire: github.com/kakts/go-wire-sandbox: wrote /Users/hoge/Documents/code/go-wire-sandbox/wire_gen.go

wireによって生成されたコードは下記のようになり、 型パラメータで指定したint型の値をNewMessageの引数に渡していることを確認できます。

func NewVInt() int {
    return modelgen.NewV[int]()
}


// Generics動作確認用
func InitializeGenericsEvent() modelgen.Event {
    int2 := NewVInt()
    message := modelgen.NewMessage(int2)
    modelgenGreeter := modelgen.NewGreeter(message)
    event := modelgen.NewEvent(modelgenGreeter)
    return event
}

これでwire.Buildでgenericsを使ったプロバイダー関数を定義し、型パラメータを指定した状態でDIさせることができました。

最後に

google/wireにおけるgenericsの対応についての説明は以上となります。

序盤の概要にまとめましたが、google/wireは現在メンテナンスモードになっており、今後のgoの新機能に追従することはないようです。しかし上述したようにgenericsをどうしても使いたい場合の解決策があることがわかりました。

今後新規で開発をする場合は、運用上困らないように他のDIツールの検討をしてみても良いかもしれません。

備考リンク

Update dependencies to support Generics by efueyo · Pull Request #360 · google/wire · GitHub

C: Pthreadによるスレッド作成とデタッチ

概要

CにおいてPthread(POSIX Thread library)ライブラリを使い、pthread_create()によりスレッドの作成をした後、別のスレッドからpthread_join()を実行し、作成したスレッドの終了を待ち、終了状態を得ることが可能です。

場合によっては、作成したスレッドの終了状態を得る必要がない場合もあります。
この場合はスレッドの終了状態が残り続けるよりも、システム側が終了したスレッドを自動的に破棄する方がよいです。
そこで、pthread_detach()を実行することで、スレッドをデタッチ、つまりスレッドの終了状態を破棄することができます。

ここでは、Pthreadのpthread_create()を用いた基本的なスレッド作成方法について説明した上で、pthread_join()によって指定したスレッドの終了を待つ方法や、pthread_detach()を実行した際に、別スレッドからpthread_join()を実行しても終了状態が取れなくなることを確認します。

pthread_create()によるスレッドの作成

pthread_create()について

pthread_create()により、実行したプロセス内でスレッドを新規作成します。

pthreadライブラリでの関数定義は下記のとおりです。 詳細はリンクを確認ください。
Ubuntu Manpage: pthread_create - create a new thread

       #include <pthread.h>

       int pthread_create(pthread_t *restrict thread,
                          const pthread_attr_t *restrict attr,
                          void *(*start_routine)(void *),
                          void *restrict arg);

新規作成されたスレッドは、第3引数のstart_routineで指定した関数を実行します。 第4引数に、start_routineの関数の引数を指定します。

スレッドの作成例

ここではシンプルに、pthread_create()を実行してスレッドを作成する例を示します。

#include <pthread.h>
#include <stdio.h>

static void * threadFunc(void *arg)
{
    char *s = (char *) arg;
    printf("%s", s);
    return (void *) strlen(s);
}

int main(int argc, char *argv[])
{
    pthread_t t1;

    int s; 

    // スレッドの作成
    // 作成されたスレッドは第三引数の関数に第4引数の値を渡して実行する
    s = pthread_create(&t1, NULL, threadFunc, "Hello world\n");

    // スレッド作成失敗
    if (s != 0) {
        printf("pthread_create failed\n");
        return -1;
    }
    printf("pthread_create succeeded\n");
    return 0;
}

基本的なスレッドの作成方法はこのとおりです。

スレッドの終了を待つ。

前項で紹介したコードは、main()でスレッドを作成して、指定した関数を実行したシンプルな処理となります。
fork()におけるwait()のように、スレッドにおいても、作成したスレッドの終了を待って、終了状態を得るためのpthread_join()関数が用意されています。
作成されたスレッドとは別のスレッドから実行することで、該当のスレッドの終了を待つことができます。

pthread_join()について

pthread_join()を別のスレッドで実行することで、指定したスレッドの終了を待って、終了状態を得ることができます。

Ubuntu Manpage: pthread_join - join with a terminated thread

       #include <pthread.h>

       int pthread_join(pthread_t thread, void **retval);

第1引数 threadに、終了を待つ対象のスレッド情報を渡します。 第2引数には、 スレッドによって実行された関数の戻り値を受けるポインタを渡します。

戻り値は、0ならば正常で、それ以外の場合はerrnoを示します。

スレッドの終了後、pthread_join()を実行するまで、システム内部でこの終了したスレッドの情報が残り続けます。 スレッドで実行させる処理によっては、pthread_join()でスレッドの終了を待つ必要がない場合があり、その場合は終了したスレッドの情報は不要となります。

不要な情報が残っているとシステムでリソースを消費してしまうため、スレッド終了時に破棄させるように設定することで、この問題を解決できます。
スレッドの情報を破棄させる方法の1つとして、pthread_detach()の実行があります。

pthread_detach()について

pthread_detach()は、スレッドに割り当てられるリソースが、終了時に回収可能であることを知らせるために使われます。

Ubuntu Manpage: pthread_detach - detach a thread

       #include <pthread.h>

       int pthread_detach(pthread_t thread);

引数に、対象のスレッドの情報を渡します。

前述したように、スレッド終了後に他のスレッドが終了状態を必要としない場合に使用するべきものです。

スレッドのデタッチする・しない場合の挙動について

ここで、スレッドを作成後、スレッドをデタッチした場合とそうでない場合にどういう挙動になるかを説明します。

例1 スレッドをデタッチせず、pthread_join()を使った場合

こちらはスレッドを利用した際に、終了状態を待つ必要がある場合の一般的な例となります。

大まかな流れは下記のとおりです。

  1. メインスレッドでptheread_create()を実行し、スレッドを作成
  2. 作成されたスレッドではpthread_create()で指定した関数を実行し、終了する
  3. メインスレッド側で2のスレッドの終了をpthread_join()を実行して待つ

この流れで、3でメインスレッド側でpthread_join()を実行し、スレッドが終了するまで待ち、スレッドの終了状態を取得します。


#include <pthread.h>
#include <stdio.h>

static void * threadFunc(void *arg)
{
    char *s = (char *) arg;
    printf("%s", s);
    return (void *) strlen(s);
}

int main(int argc, char *argv[])
{
    pthread_t t1;
    void *res = NULL;

    int s; 

    // スレッドの作成
    // 作成されたスレッドは第三引数の関数に第4引数の値を渡して実行する
    s = pthread_create(&t1, NULL, threadFunc, "Hello world\n");

    // スレッド作成失敗
    if (s != 0) {
        printf("pthread_create failed");
        return -1;
    }
    // 作成したスレッドが終了するのを待つ
    sleep(3);
    printf("Message from main()\n");

    // 作成したスレッドの終了を待つ
    // スレッドによって実行された関数の戻り値を&resに格納する
    s = pthread_join(t1, &res);
    if (s != 0) {
        printf("pthread_join failed");
        return -1;
    }

    printf("Thread returned %ld\n", (long) res);
    return 0;
}

main()関数でスレッド作成後、sleep(3)を実行し、作成したスレッドが終了するのを待った後、pthread_join()を実行します。 第2引数に渡したresに関数の実行結果が渡るので、実行後その結果を表示しています。

# ./simple_thread
Hello world
Message from main()
Thread returned 12

スレッドの関数に Hello world\n を渡したので、その分の文字列長を表示できているのがわかります。

例2 自スレッドで終了を待たずにpthread_detach()を実行した場合

作成したスレッド側で、終了前にpthread_detach()を実行させた後、別のスレッドでpthread_join()をした際にどうなるのかを確認します。

例1とは大きくコードは変わらず、pthread_create()で指定した関数内で、終了前にpthread_detach()を実行しています。
自スレッドで実行するため、引数には時スレッドの情報を取得するpthread_self()を渡します。

#include <pthread.h>
#include <stdio.h>

static void * threadFunc(void *arg)
{
    char *s = (char *) arg;
    printf("%s", s);

    int res;
    // 自スレッドをデタッチする
    res = pthread_detach(pthread_self());
    if (res != 0) {
        print("pthread_detach failed");
        return -1;
    }
    
    return (void *) strlen(s);
}

int main(int argc, char *argv[])
{
    pthread_t t1;
    void *res = NULL;

    int s; 

    // スレッドの作成
    // 作成されたスレッドは第三引数の関数に第4引数の値を渡して実行する
    s = pthread_create(&t1, NULL, threadFunc, "Hello world\n");

    // スレッド作成失敗
    if (s != 0) {
        errExitEN(s, "pthread_create");
    }
    sleep(3);
    printf("Message from main()\n");

    // 作成したスレッドの終了を待つ
    // 作成したスレッドが実行する関数内でpthread_detach()を実行しているため
    // ここでエラーが出る
    s = pthread_join(t1, &res);
    if (s != 0) {
        errExitEN(s, "pthread_join");
    }

    printf("Thread returned %ld\n", (long) res);
    exit(EXIT_SUCCESS);
}

この例だと、スレッドで実行する関数でpthread_detach()を実行しており、 メインスレッド側でpthread_join()を実行する際にはすでに作成したスレッドはデタッチされているため、スレッドの終了状態が取れずにエラーとなります。

# ./simple_thread_detach 
Hello world
Message from main()
ERROR [EINVAL Invalid argument] pthread_join

実行結果はこのとおりです。

pthread_create()を実行時にスレッド属性としてデタッチをするように設定もできますが、今回はpthread_detach()によるスレッドのデタッチを行い、デタッチした際の挙動について確認できました。