Skip to content

Commit a5ea90a

Browse files
committed
Updated docs.
1 parent 54c905c commit a5ea90a

File tree

10 files changed

+433
-0
lines changed

10 files changed

+433
-0
lines changed

docs/README.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# golib/weak_ref ライブラリ
2+
3+
弱参照(弱い参照/weak reference)の機能を提供します。
4+
5+
* [weak_ref.WeakRef](WeakRef.md)
6+
* Lock-freeな弱参照の機能を提供します。
7+
8+
* [弱参照(弱い参照/weak reference)の解説](weak_reference.md)
9+
* go言語に限定しない一般的な弱参照の説明です。

docs/WeakRef.md

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# type WeakRef
2+
弱参照の機能を提供するモジュールです。
3+
atomic操作で排他制御を行っているので、Lock-freeです。
4+
5+
## import
6+
7+
```go
8+
import "github.com/l4go/weak_ref"
9+
```
10+
vendoringして使うことを推奨します。
11+
12+
## 利用サンプル
13+
14+
* [弱参照のサンプル](../examples/ex_weak_ref/ex_weak_ref.go)
15+
* [所有権譲渡のサンプル](../examples/ex_ref_move/ex_ref_move.go)
16+
* [CAS処理のサンプル](../examples/ex_ref_cas/ex_ref_cas.go)
17+
* [CAS処理のループを自前で書いたサンプル](../examples/ex_ref_cas2/ex_ref_cas2.go)
18+
19+
## 参照の扱い
20+
\*WeakRefでの参照の扱いは以下の通りです。
21+
22+
* 参照は、ポインタ型の値で表現する。
23+
* 内部へ保存された参照へのアクセスは、atomic操作で行う。
24+
* 参照を無効化でき、nil値を無効状態を示すものとして扱う。
25+
* 入力では、参照のポインタ型の値を`interface{}`で受け取る。
26+
* 出力では、参照のポインタ型の値を`interface{}`として返す。
27+
* 初期化時(New()時)に、参照の型を固定する。(静的な型アサーションが利用可能)
28+
29+
## メソッド概略
30+
31+
### func New(val interface{}) \*WeakRef
32+
\*WeakRefを生成します。
33+
参照の利用終了時に、無効にする(Reset()等)ことで弱参照として動作します。
34+
初期値を使って、値の設定だけでなく、参照の型を固定化も行います。初期化後は、初期値と異なる型の値を入力するとpanicします。
35+
36+
初期値だけは型情報が必要なので、nil型のnil値が利用できません。無効状態(nil値)に初期化したい場合は、以下の例のように型ありのnil値を利用してください。
37+
38+
ポインタ型のnil値(無効状態)で初期化する例
39+
40+
```go
41+
type Test struct {
42+
val int
43+
}
44+
45+
var wr = weak_ref.New((*Test)(nil))
46+
```
47+
48+
### func (wr \*WeakRef) Move() \*WeakRef
49+
参照の所有権を譲渡した\*WeakRefを生成します。
50+
元となった\*WeakRefは無効にされます。
51+
52+
別の処理(関数など)へ、参照の管理ごと譲渡する用途を想定しています。
53+
54+
### func (wr \*WeakRef) Reset()
55+
参照を無効にします。
56+
弱参照として動作させるには、参照の利用が終了した際に、Reset()の呼び出しが必要です。
57+
58+
### func (wr \*WeakRef) Get() interface{}
59+
参照の値を取得します。
60+
61+
### func (wr \*WeakRef) Set(val interface{})
62+
参照の値を変更します。
63+
静的なnilで無効化する場合は、簡素に記述できるReset()を利用するべきです。
64+
goroutine間での排他制御が必要な場合は、CasUpdate()を利用してください。
65+
66+
### func (wr \*WeakRef) Swap(new\_val interface{}) interface{}
67+
参照の値を更新し、変更前の値を返します。
68+
goroutine間での排他制御が必要な場合は、CasUpdate()を利用してください。
69+
70+
### func (wr \*WeakRef) CompareAndSwap(old\_val, new\_val interface{}) bool
71+
CAS(コンペア・アンド・スワップ、Compare-and-swap)での、参照の値の更新を試み、
72+
成功した場合はtureを、失敗した場合はfalseを返します。
73+
74+
通常の用途では、より簡素に記述できるCasUpdate()を利用すべきです。
75+
76+
### func (wr \*WeakRef) CasUpdate(f CasUpdateFunc) interface{}
77+
CASアルゴリズム(CAS方式のループを実施)で、参照の値を更新します。
78+
変更後の値の計算方法をCasUpdateFunc型の関数で渡します。
79+
参照が無効な場合は失敗し、nil値を返します。
80+
更新が成功した場合は変更後の値を返します。
81+
82+
### type CasUpdateFunc func(v interface{}) interface{}
83+
CasUpdate()で利用する、古い値から新しい値への計算方法を記述する関数の型です。
84+
CASアルゴリズムではコリジョン時に再計算が必要なため、CasUpdateFunc型の関数は、1回の更新処理で複数回呼ばれることがあります。
85+
86+
インクリメントするCasUpdateFunc型の関数の例(`func incr()`)
87+
88+
```go
89+
type Test struct {
90+
V int
91+
}
92+
93+
func incr(v interface{}) interface{} {
94+
return &Test{V: v.V+1}
95+
}
96+
```

docs/one_ref.png

11.9 KB
Loading

docs/proc_ref.png

13.7 KB
Loading

docs/two_ref.png

14 KB
Loading

docs/weak_reference.md

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
go言語に限定しない一般的な弱参照の説明です。
2+
3+
世の中に「弱参照」のまともな説明がすくないため、仕方なく暫定的に記述したものです。(詳細な理由については、末尾の補足を参照)
4+
5+
# 弱参照(弱い参照/weak reference)
6+
弱参照は、参照先の変数(メモリ)の所有権を持たない参照です。
7+
対語として、所有権のある本来の参照を「強参照(強い参照/strong reference)」と表現することがあります。
8+
弱参照は、所有権を持たないので、参照先の変数の解放で自動的に無効化されます。
9+
また、処理中の無効化を回避するため、参照(強参照)への(一時的な)変換も可能です。
10+
11+
## 弱参照で回避できる問題
12+
利用方法を知らないと弱参照の理解が難しいので、弱参照の利用方法についての記述します。
13+
14+
参照を共有すると、条件によっては、メモリが適切に解放できくなる問題が発生します。
15+
この問題を軽減するための手法の一つが弱参照になります。
16+
17+
参照が共有されたことで、適切にメモリ解放ができなくなる問題を起こす状態には、主に以下の2種類です。
18+
19+
1. 参照が相互に参照している。(循環参照)
20+
* 参照から共有されている
21+
2. 終了が未定な処理が参照を保持している。
22+
* 処理から共有されている
23+
24+
構造的には、この2つは、参照を共有(保有)する対象が違うだけです。
25+
弱参照を使うと、これらの共有状態を、メモリ管理からは共有されていないように見せかけることができ、問題を回避できるわけです。
26+
27+
以降の説明では、それぞれの状態について、もう少し詳しく説明しておきます。
28+
29+
### 1. 参照が相互に参照している状態(循環参照の状態)
30+
単一または複数の参照が、直接または間接的に、循環するように参照した状態です。
31+
32+
この状態にしてしまうと、「参照カウンタ」方式のみメモリ管理では、参照の利用終了を認識できません。
33+
処理が終了しても、循環分のカウンタ増で参照カウンタが0にならず、メモリ管理には利用中だと認識され、結果的にメモリリークが起こります。
34+
35+
この問題は以下の3種類の方法で、回避可能です。
36+
37+
* 後処理で(一部の)参照を無効にして、参照の循環を壊す。
38+
* 一部の参照を弱参照に代えて、メモリ管理的に循環がない状態にする。
39+
* 一部の参照をコピーした値に代えて、参照の循環がない状態にする。
40+
* GCで循環参照を解放する(適切に動作するGCを利用する)。
41+
42+
弱参照で解決する場合の以下の図のような参照関係の変更を行います。
43+
44+
弱参照による問題回避の図1(1要素での循環参照の例)
45+
![1要素での循環参照](one_ref.png)
46+
47+
弱参照による問題回避の図2(複数要素での循環参照の例)
48+
![2要素の循環参照](two_ref.png)
49+
50+
弱参照を使うと、GCやデータコピー等の実行時のコストを軽減することができます。
51+
52+
### 2. 終了が未定な処理が参照を保持している状態
53+
終了が未定な(処理時間が長い)処理に、参照としてデータを渡すと発生する状態です。
54+
例えば、処理時間が長いコールバック関数や、動作し続けるサブシステムへ、参照を渡す(共有する)場合が該当します。
55+
56+
この状態になると、参照を渡した側(依頼元)の処理が終了しても、参照を渡された側(依頼先)の処理は中断されないと続行するので、参照も解放されません。
57+
処理も参照も、どちらも終われない状態になり、実質のメモリリーク状態と、実質の無限ループが起きてしまいます。
58+
59+
この問題は以下のような2種類の方法で回避可能です。
60+
61+
* 中断機能を実装し、依頼元の終了時に確実に実施する。
62+
* 弱参照でデータを渡し、依頼先の処理に参照を保持させない。
63+
* (コピーした値を渡しても、解決しない)
64+
* (実際に参照が利用されてるので、GCでは解決しない)
65+
* (解決せずに放置するプログラムもたまに見かける...)
66+
67+
弱参照を使う場合、自動的に無効化する特徴によって、中断処理も実現が容易になります。
68+
69+
弱参照による問題回避の図3(依頼先の処理も参照を保有する例)
70+
![依頼先の処理が保有](proc_ref.png)
71+
72+
この問題はGCでは回避できないので、弱参照を使うしかないことがあります。
73+
74+
# 補足(こんな説明が存在する理由)
75+
76+
## GC(ガベージコレクション/garbage collection)の定義
77+
前提として、以下のGCの定義を信じると、きちんと「不要になった領域を自動的に解放しない」機能をGCとして書くのは、誤解を生むので、解説としては不適切と判断しています。
78+
79+
Wikipediaより(2021/12/15現在)
80+
> ガベージコレクション(英: garbage collection; GC)とは、コンピュータプログラムが動的に確保したメモリ領域のうち、不要になった領域を自動的に解放する機能である。
81+
82+
この定義は、LISPから始まるGCの歴史もきちんと踏まえて書かれて、十分信用できそうです。
83+
そして、この定義に従うなら、「循環参照を自動的に解放できない」メモリ管理は、GCとは言い難いのです。
84+
そうなると、「参照カウンタ」のみに頼ったメモリ管理も、やはりGCとは言い難いのです。
85+
86+
## 世の「弱参照」
87+
88+
そして、上記のGCの定義を理解した上で、世の中の「弱参照」の定義らしき説明を読むと、何かおかしいのです。
89+
例えば、Wikipediaの説明すら、以下の通りです。
90+
91+
Wikipediaより(2021/12/15現在)
92+
> 弱い参照(英: weak reference、ウィークリファレンス)あるいは弱参照とは、参照先のオブジェクトをガベージコレクタから守ることのできない参照のことである。
93+
94+
実際に、GCがない処理系でも弱参照は使われていて、弱参照はGC専用の仕組みではありません。
95+
そうすると、上記の、GCを前提とした弱参照の説明は、明らかに間違っています。
96+
このような状況なので、仕方なく書いたのがこの説明です。
97+
98+
おそらく、GCのある処理系での弱参照の「動き」を理解した方が、「弱参照」自体を十分に理解しないまま、使ってる言語の「動き」を文章にした結果、このような状況になっていると予想されます。残念な話です。

examples/ex_ref_cas/ex_ref_cas.go

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"time"
6+
7+
"github.com/l4go/task"
8+
"github.com/l4go/weak_ref"
9+
)
10+
11+
type Test struct {
12+
v uint64
13+
}
14+
15+
func NewTest(i uint64) *Test {
16+
return &Test{v: i}
17+
}
18+
func (t *Test) Value() uint64 {
19+
return t.v
20+
}
21+
22+
func (t *Test) Set(i uint64) {
23+
t.v = i
24+
}
25+
26+
func main() {
27+
m := task.NewMission()
28+
defer m.Done()
29+
30+
tt := NewTest(0)
31+
tt_ref := weak_ref.New(tt)
32+
33+
defer log.Println("Reset")
34+
defer tt_ref.Reset()
35+
36+
do_ch := make(chan struct{})
37+
38+
for i := uint64(1); i <= 5; i++ {
39+
go func(wm *task.Mission, id uint64) {
40+
defer wm.Done()
41+
42+
cas_tries := 0
43+
slow_incr := func(i interface{}) interface{} {
44+
tt := i.(*Test)
45+
46+
cas_tries++
47+
time.Sleep(100 * time.Millisecond)
48+
return NewTest(tt.Value() + 1)
49+
}
50+
51+
<-do_ch // Ready, go!
52+
ref := tt_ref.CasUpdate(slow_incr)
53+
if ref == nil {
54+
log.Printf("Cancel(id:%d)\n", id)
55+
return
56+
}
57+
58+
tt := ref.(*Test)
59+
log.Printf("Success(id:%d, tries:%d, value:%+v)\n",
60+
id, cas_tries, tt)
61+
}(m.New(), i)
62+
}
63+
64+
close(do_ch)
65+
time.Sleep(350 * time.Millisecond)
66+
}

examples/ex_ref_cas2/ex_ref_cas2.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"time"
6+
7+
"github.com/l4go/task"
8+
"github.com/l4go/weak_ref"
9+
)
10+
11+
func main() {
12+
m := task.NewMission()
13+
defer m.Done()
14+
15+
var val int = 0
16+
w_ref := weak_ref.New(&val)
17+
18+
defer log.Println("Reset")
19+
defer w_ref.Reset()
20+
21+
do_ch := make(chan struct{})
22+
23+
for i := uint64(1); i <= 5; i++ {
24+
go func(wm *task.Mission, id uint64) {
25+
defer wm.Done()
26+
27+
cas_tries := 0
28+
<-do_ch // Ready, go!
29+
30+
cas_again:
31+
o_ref := w_ref.Get()
32+
if o_ref == nil {
33+
log.Printf("Cancel(id:%d)\n", id)
34+
return
35+
}
36+
37+
time.Sleep(100 * time.Millisecond)
38+
39+
n_val := *(o_ref.(*int)) + 1
40+
if !w_ref.CompareAndSwap(o_ref, &n_val) {
41+
goto cas_again
42+
}
43+
44+
log.Printf("Success(id:%d, tries:%d, value:%d(%+v))\n",
45+
id, cas_tries, n_val, &n_val)
46+
}(m.New(), i)
47+
}
48+
49+
close(do_ch)
50+
time.Sleep(350 * time.Millisecond)
51+
}

examples/ex_ref_move/ex_ref_move.go

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"sync/atomic"
6+
7+
"github.com/l4go/task"
8+
"github.com/l4go/weak_ref"
9+
)
10+
11+
type Test struct {
12+
v uint64
13+
}
14+
15+
func NewTest(i uint64) *Test {
16+
return &Test{v: i}
17+
}
18+
func (t *Test) Value() uint64 {
19+
return atomic.LoadUint64(&t.v)
20+
}
21+
22+
func (t *Test) Set(i uint64) {
23+
atomic.StoreUint64(&t.v, i)
24+
}
25+
26+
func main() {
27+
m := task.NewMission()
28+
defer m.Done()
29+
30+
tt := NewTest(0)
31+
tt_ref := weak_ref.New(tt)
32+
defer tt_ref.Reset()
33+
34+
go free_worker(m.New(), tt_ref.Move())
35+
36+
if tt_ref.Get() == nil {
37+
log.Println("No cleaning required.")
38+
return
39+
}
40+
log.Println("Cleaning.")
41+
}
42+
43+
func free_worker(wm *task.Mission, tt_ref *weak_ref.WeakRef) {
44+
defer wm.Done()
45+
defer tt_ref.Reset()
46+
47+
ref := tt_ref.Get()
48+
if ref == nil {
49+
log.Println("Fail worker")
50+
return
51+
}
52+
log.Println("Success worker")
53+
}

0 commit comments

Comments
 (0)