Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
並行処理入門 -Goで遊ぶ-
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
stonriver
March 16, 2019
Technology
270
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
並行処理入門 -Goで遊ぶ-
stonriver
March 16, 2019
More Decks by stonriver
See All by stonriver
中規模イベントに急造で変なネットワークを構築する
strvworks
1
790
ターミナル雑記
strvworks
2
1.4k
お手軽金盾体験
strvworks
1
800
Kosen_LT_ONLINEのおしらせ
strvworks
0
110
Minecraft概論
strvworks
0
410
日本列島の移動速度に関する考察
strvworks
1
170
PythonにおけるGUIフレームワークのはなし
strvworks
0
430
快適な読書環境のご提案
strvworks
0
150
テクノ手芸
strvworks
0
99
Other Decks in Technology
See All in Technology
2026TECHFRESH畢業分享會 - AI 時代的人生存檔點
line_developers_tw
PRO
0
1.3k
日本 Fintech 未来予測レポート 2027〜2028年(オリジナル版)
8maki
0
2.3k
サイバーエージェントにおけるAI推進戦略と変革への取り組み
shotatsuge
0
110
Oracle AI Database@Azure:サービス概要のご紹介
oracle4engineer
PRO
6
2k
Chainlitで作るお手軽チャットUI
ynt0485
0
280
ぼっちではじめた登壇が「51名」「241件」の発信に化けた
subroh0508
0
230
Oracle Cloud Infrastructure:2026年6月度サービス・アップデート
oracle4engineer
PRO
0
120
Kiroで書いた 設計書 が AI レビューの 採点基準 になる
ezaki
0
130
2026TECHFRESH畢業分享會 - 原生還是跨平台? App 開發踩坑實錄
line_developers_tw
PRO
0
1.3k
自分が詳しくない領域でAIを使う #プロヒス2026
konifar
12
4.4k
OTel × Datadog で 「AI活用」を計測し、改善に繋げる
shihochan
1
390
就職⽀援サービスにおけるキャリアアドバイザーのシフトスケジューリング
recruitengineers
PRO
1
150
Featured
See All Featured
Understanding Cognitive Biases in Performance Measurement
bluesmoon
32
2.9k
<Decoding/> the Language of Devs - We Love SEO 2024
nikkihalliwell
1
250
The MySQL Ecosystem @ GitHub 2015
samlambert
251
13k
Side Projects
sachag
455
43k
The Invisible Side of Design
smashingmag
302
52k
Embracing the Ebb and Flow
colly
88
5.1k
Ruling the World: When Life Gets Gamed
codingconduct
0
260
SEO in 2025: How to Prepare for the Future of Search
ipullrank
3
3.5k
Music & Morning Musume
bryan
47
7.2k
Thoughts on Productivity
jonyablonski
76
5.2k
Designing for Performance
lara
611
70k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
25
2k
Transcript
並行処理入門 Goで遊ぶ すとんりばー
自己紹介 名前:すとんりばー Twitter : @strvert Facebook : strvert Web :
strvworks.github.io 2
並行処理とは What is the concurrent processing
並行処理とは 並行処理と聞いて、「並列処理じゃないの?」と 思った方がいるんじゃないでしょうか。 並行処理と並列処理は字面こそ似ていますが、それ ぞれ異なる定義がなされている用語です。 4
並行処理とは ざっくりと言うと、並行処理は「複数状態の並行性」 に焦点が置かれ、並列処理は「複数処理の並行性」に焦 点が置かれた概念であると言えます。 次ページから概念図を示します。 5
並列処理(parallel processing) 並列処理は実際に複数の 処理が複数のプロセッサな どによって行われている処 理を指します。 6
並行処理(concurrent processing) 対して並行処理は、処理 ごとの状態が並行して存在 し、人間の視点からみると 同時に実行されているよう に見える物も含みます。 逐次処理を高速にスイッチ ングしていると言うことも できます。
7
並行処理とは などと説明すると、並列処理と並列処理にあらゆる処 理が分類可能なように思われてしまうかもしれません。 しかし、プログラマの書くソースコード上で、両者に 大きな差はありません。(GPGPUなどは特殊) 8
並行処理とは つまり、並行性を持つソースコードをプログラマが記 述した時、それが実際に並行と並列の実行されるかはそ の実行環境に依存すると言うことができます。 同じ並行性を持ったコードであっても、その環境に よって利用可能なコアの数だけスケールして並列実行さ れたり、シングルコアで並行に実行されたりするという ことです。 9
並行処理とは しかし、どちらで実行されるにせよ、一般的な逐次処 理を記述したソースコードでは効率的な並行/並列実行は 行われません。 少なくとも現状では、プログラマが意識して並行性を 持ったソースコードを記述することが求められていま す。 10
並行処理の必要性 Necessity of concurrent processing
並行処理の必要性 そもそもなぜ並行処理のコードを記述する必要がある のでしょうか。 なお、ここでいう並行処理のコードとは実行環境に よって並行実行される場合と並列実行される両方の可能 性があります。 12
並行処理の必要性 身近な並行処理のコード記述に、GUIソフトウェアが 挙げられます。 例えば画像処理などの比較的重い処理を行うGUIソフ トウェアにおいて、ソフトウェア内部で画像処理が行わ れている途中にGUIがフリーズしないのは、画像処理の スレッドとGUIのスレッドが並行して動作しているから です。 13
並行処理の必要性 もっと重要で普段からお世話になっている並行処理を 実現しているソフトウェアとして、OSも挙げることがで きます。 普段利用するOS上で、CPUのコア数よりも多く並行し てソフトウェア(プロセス)を起動できるのは、OSがCPU の計算資源を複数のソフトウェアに分配してくれている からです。→コンテキストスイッチ 14
並行処理の必要性 また、並列して処理を実行可能な環境において、複数 の演算器(コアなど)を用いて演算を高速化することを目 的とした並列アルゴリズムも考案されています。 容易に並列化が可能で、並列化された処理同士の依存 性をほぼ無くすことが可能な問題を、「驚異的並列」と 呼んだりします。 →円周率計算/画像の幾何学変換/データベース検索/数値計算の区間分割 15
今回は、Goのサンプルコードと共に、並行処理の基礎 についてお話します。 Goの軽い解説はしますが、並行処理の例としてですの で細部を理解する必要はありません。 並行処理の必要性 16
ひとくちGo HITOKUTI Go
ひとくちGo Goは並行処理に強い言語であり、他の言語と比較して 簡単に並行処理を記述することができます。(後述) とりあえずサンプルコードが読めればいいので、ざっ くりとした話だけをします。まずは関数です。 18
関数 Goは通常の関数以外に無名 関数を利用することができ ます。無名関数は一般の関 数と同等に扱えます。 出力はどうなるでしょうか 19 // hello関数宣言 func
hello() { fmt.Println("Hello.") } // main関数宣言(エントリポイント) func main() { // 無名関数を変数に bye := func() { fmt.Println("Bye.") } // 呼び出し hello() bye() // 無名関数呼び出し func() { fmt.Println("Hai.") }() }
関数 こうなります(自明) 脳死ですね。 20 Hello. Bye. Hai.
ゴルーチン(簡易説明) ゴルーチンは、Goのスレッ ドのようなものです。(後述) なんと関数呼び出しに"go"を つけるだけで、独立して実行 されます。 検証のため、sleepを入れて います。 21 func
hello() { time.Sleep(time.Second * 2) // 2秒 sleep fmt.Println("Hello.") } func main() { bye := func() { time.Sleep(time.Second) // 1秒 sleep fmt.Println("Bye.") } // ゴルーチンとして関数呼び出し go hello() go bye() go func() { time.Sleep(time.Second * 3) // 3秒 sleep fmt.Println("Hey.") }() time.Sleep(time.Second * 4) // 4秒 sleep } 出力はどうなるでしょうか
ゴルーチン(簡易説明) 1秒おきに、ゴルーチンの 起動順とは異なる順で出力 されました。 ゴルーチンが独立して非同 期に実行されていることが わかります。 なお、mainもゴルーチンで す。 22
Bye. Hello. Hey.
ひとくちGo とりあえずこれだけでOKです。あとは感覚で読めるは ずです。 後ほどGoの並行処理についてもう少し掘り下げた話を します。 23
並行処理の難しさ Concurrent processing difficulty
並行処理の難しさ 並行処理のプログラミングには、逐次処理の場合には 表面化しづらい多くの壁が存在します。 その多くが、並行して実行されるスレッド同士の競合 や、それらの実行・終了タイミングによるものです。 開放されずに残ったスレッドのリークが問題となるこ ともあります。 試しに1つ、簡単な例を出してみます。 25
実行タイミング ここに、並行処理を用いた サンプルコードを用意しま した。 先程紹介したGoの機能の範 疇ですが、ある問題が存在 します。 26 func main()
{ // ゴルーチンとして関数を起動 go func() { fmt.Println("CALL GOROUTINE!!") }() fmt.Println("MAIN GOROUTINE!!") } 出力はどうなるでしょうか
実行タイミング 正解は、 「MAIN GOROUTINE!!」 のみが出力されるです。 わかりましたか? 27 MAIN GOROUTINE!!
実行タイミング これは、並行処理が起動す るまでの時間が、直後の main関数の終了よりも遅 かったことで、標準出力が 行われるよりも前にプロセ ス自体が終了してしまった ことが原因です。 sleepを入れてみます。 28
func main() { // ゴルーチンとして関数を起動 go func() { fmt.Println("CALL GOROUTINE!!") }() fmt.Println("MAIN GOROUTINE!!") time.Sleep(time.Second) // 1秒 sleep } 出力はどうなるでしょうか
実行タイミング 想定したであろう出力が得ら れました。 しかし、Sleepで処理をブロッ キングすることはあくまで対 処療法であり、正解ではあり ません。環境によってゴルー チンの起動時間は異なるた め、本来は処理の終了を待っ て判断する必要があります。
29 CALL GOROUTINE!! MAIN GOROUTINE!!
並行処理の難しさ これは勿論Goに限った話ではなく、(ピンポイントに対 策されていなければ)どんな並行処理でも起こりうるもの です。 次ページから、並行処理に関するさまざまな特性を紹 介します。 30
アトミック性 アトミック性とは、ある操作について、それを操作し ている場所から見て不可分である操作の性質のことで す。 (??????) 自分でも言っていてよくわからないので、次に例を示 します。 31
アトミック性 あるCPUの命令セットでは、メモリ値のインクリメント を行う時、以下の操作が行われるとします。 1. 値の読み込み 2. 読み込んだ値に1を加算 3. 結果を元の位置に書き込み 32
アトミック性 このインクリメント操作が並行して2つ呼び出され、以 下の順で実行されたとします。 1. Aによる値の読み込み 2. Aが読み込んだ値に1を加算 3. Bによる値の読み込み 4.
Aが結果を元の位置に書き込み 5. Bが読み込んだ値に1を加算 6. Bが結果を元の位置に書き込み 33
アトミック性 この命令を実行したエンジニアは、インクリメント命 令を2つ並行で呼んだことで値が2加算されると考えたか もしれませんが、そうはなっていません。 AとBがそれぞれ0を読み出して同じ場所に1を重ね書き しただけです。結果として、Aの計算の結果は一切反映 されていません。 34
アトミック性 これは「インクリメント」という1つの操作に思われ る操作が、このコンピュータでは分割可能な複数の命令 で構成されていたことから、並行処理を行った場合に偶 然意図しない動作を引き起こしたとうことになります。 このような、ある操作のまとまりが分割可能で外部か ら介入や参照が可能な操作にはアトミック性がありませ ん。 35
アトミック性 今度は、CPUの命令セットに以下の1命令でインクリメ ントを行う命令がある場合を考えます。 1. あるメモリ位置の値をインクリメント 36
アトミック性 このインクリメント操作が並行して2つ呼び出され、以 下の順で実行されたとします。 1. Aがメモリ位置の値をインクリメント 2. Bがメモリ位置の値をインクリメント 37
アトミック性 この命令を実行したエンジニアは、インクリメント命 令を2つ並行で呼んだことで値が2加算されると考え、事 実どのような場合でも想定した動作が行われることがわ かります。 このような場合に、この操作はアトミック性があると 言えます。 38
クリティカルセクション ここからわかる通り、並行処理を行う場合において並 行して実行される命令はアトミック性を持つことが重要 なのです。 そして、アトミック性が無く、並行して実行されると 破綻をきたしてしまう操作区間のことを、クリティカル セクションといいます。 39
クリティカルセクション このコードにはアトミック 性が無く、クリティカルセ クションが存在します。 実行してみましょう。 40 func main() { var
wg sync.WaitGroup val := 0 // インクリメント無名関数 inc := func() { fmt.Println(val) val++ } const routinenum = 20 wg.Add(routinenum) // ゴルーチン起動数を登録 for i := 0; i < routinenum; i++ { // ゴルーチン呼び出し go func() { // ゴルーチン終了時に終了報告 defer wg.Done() inc() }() } wg.Wait() // 登録ゴルーチンの終了待ち fmt.Println("Finish.") } 出力はどうなるでしょうか
クリティカルセクション なんだかカオスなことになり ました。 本来は0~19が順不同で出力さ れるはずですが、重複する数 字も存在します。 出力その時の場合によって変 動し、不安定です。 確率でうまく行きます! 41
0 1 2 3 4 5 6 7 6 8 8 10 8 8 8 8 8 11 10 16 Finish.
クリティカルセクション このコードのクリティカルセ クションはここです。 ゴルーチンとして呼び出され る無名関数の中で同じスコー プの変数にアクセスが行われ ています。 メモリアクセス競合の可能性 があり、アトミック性があり ません。
42 func main() { var wg sync.WaitGroup val := 0 inc := func() { // クリティカルセクション fmt.Println(val) val++ } const routinenum = 20 wg.Add(routinenum) // ゴルーチン起動数を登録 for i := 0; i < routinenum; i++ { go func() { defer wg.Done() inc() }() } wg.Wait() // 登録ゴルーチンの終了待ち fmt.Println("Finish.") }
クリティカルセクション ここで、クリティカルセク ションの間に他のゴルーチン のメモリアクセスをロックす ることでアトミック性を持た せてみます。 43 func main() {
var lock sync.Mutex var wg sync.WaitGroup val := 0 inc := func() { // スコープに存在する変数への他アクセスをロック lock.Lock() defer lock.Unlock() fmt.Println(val) val++ } const routinenum = 20 wg.Add(routinenum) for i := 0; i < routinenum; i++ { go func() { defer wg.Done() inc() }() } wg.Wait() fmt.Println("Finish.") }
クリティカルセクション アトミック性を保証するよう にすると、正常に動作するこ とがわかります。 実装上、出力が順番に並ぶと は限りません。 44 0 1 2
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Finish.
Mutex ここではアトミック性の保証のためにGoのsync.Mutexに よるロックを利用しました。 Mutexは多くの言語から利用可能な排他制御/同期機構 で、Goではチャネルの利用が推奨されていますが、簡単な 例では理解しやすく、チャネルよりも高速です。 Goにおいても、パフォーマンスの要求されるクリティカ ルセクションではMutexの利用が推奨されています。 45
デッドロック メモリのロックを利用することでアトミック性は保証 できましたが、メモリのロックによって発生する問題も 存在します。 デッドロックはその1種で、並行する処理がお互いの ある処理を無限に待機し合ってしまう状況を指します。 46
デッドロック デッドロックにはCoffman conditionsという以下の4つ の発生条件が存在します。 • 相互排他 • 保持&待ち 47 •
横取り不能 • 循環待ち
デッドロック sumは、引数を2つ取って、 アトミック性を確保するため に受け取った第1引数を排他 的権利でロックします。 特定の引数で並行して起動す ると、お互いのロックする変 数を待ち合います。 これは外部から止めることが できません。
48 type value struct { mu sync.Mutex value int } func main() { var wg sync.WaitGroup sum := func(v1, v2 *value) { defer wg.Done() v1.mu.Lock() defer v1.mu.Unlock() time.Sleep(time.Second * 2) // 戦犯 v2.mu.Lock() defer v2.mu.Unlock() fmt.Printf("%d+%d=%d\n", v1.value, v2.value, v1.value+v2.value) } a := value{value: 10} // 値初期化 b := value{value: 20} wg.Add(2) go sum(&a, &b) // sum起動その1 go sum(&b, &a) // sum起動その2 wg.Wait() }
デッドロック 今回の場合、sleepによって変 数がロックされる時間が伸び ていることで、デッドロック の確率が上昇しています。 そして、これらの状況はデッ ドロックの条件に一致し、 デッドロックだという事がで きます。 49
ライブロック 無限に処理が待ち合って停止するデッドロックに対し て、無限に処理同士が避け合うことで結果として避けた 先で競合し続けるライブロックというものも存在しま す。 廊下で同じ方向に避け続けることで、動いている(生き ている)にもかかわらず先に進めない状況によく例えられ ます。 (実装例がちょっとだけ長いので割愛) 50
リソース枯渇 いかに処理が正常に動いていても、それが適切な形で 実装されているかどうかは別の話です。 ここまでメモリ同期(ロック)によってアトミック性を 保証する方法などを挙げてきましたが、ロックが行われ るということは他のスレッド(ゴルーチン)がそのメモリ 領域を利用する処理ができないということでもありま す。 51
リソース枯渇 これにより、ロックを広範囲に適用したコードは、こ まめにロックすべき箇所を分割して記述した場合と比較 して遥かに長くプロセッサ時間を専有してしまいます。 確かにアトミック性は重要ですが、その保証方法も考 える必要があるのです。 52
並行処理の難しさ ここまで挙げてきたように、並行処理は便利な反面、 逐次処理の記述と比較して低いレイヤまでプログラマが 意識して記述する必要があります。結構なんとなく書い ても動いてしまったりするのですが、実はメモリリーク が発生していたり、正常に動いているように見えて実は メモリ競合で値が想定するものではなかったりなど、バ クの発見も一癖あったりします。 53
並行処理の難しさ しかし!Goには、並行処理をより自然に記述するため の多くの機能が存在します。ここからはGoの布教です。 ここまでのコードは多くの言語に存在する機能を用い て記述してきましたが、ここからはGoに存在する並行処 理に関する機能を紹介します。 54
Goの並行処理 Concurrent processing of Go
Goの並行処理 多くのプログラミング言語には、OSスレッドに関する ライブラリが存在します。 そのため、それらを通してOSスレッドを操作し、並行 処理を用いたソフトウェアの作成が可能となっていま す。 つまり、一般的にはOSスレッドの概念を直接操作して 処理を実現しているということです。 →Python: threading,
C: pthread, Java: Thread …… 56
Goの並行処理 対して、Goにはゴルーチンとチャネルという並行処理 のための概念が導入されており、それらが言語機能その ものにビルトインされています。 これにより、言語機能の上のライブラリとして構築さ れているOSスレッド管理よりも自然に、ライブラリ内の 複雑性を持つことなく並行処理を記述することができま す。 57
Goの並行処理 Goのゴルーチンとチャネルは、CSP(Communicating Sequential Processes)という並行処理の手法に基づいた概 念です。 一般的な並行処理では、スレッド間でデータをやりとり する時、共有メモリが用いられます。複数のスレッドが同 一のメモリ位置を参照することで、瞬時にデータをやりと りできるということです。前節までで利用してきたのもこ ちらです。
58
Goの並行処理 一方CSPでは、プロセス間のやりとりを通信に見立て ることで、通信用に確保した領域をトンネルのように用 いてデータを"送受信する"という考え方が採用されてい ます。 プロセスに対するI/Oであると捉えることもできます。 59
Goの並行処理 そして、CSPのプロセスに当たるのがゴルーチン、通 信経路に当たるのがチャネルです。 次ページから、いくつかGoの並行処理に関するコードを 見てみます。 60
ゴルーチン スレッドと類似のGo独自の 概念で、Goの最も基本的な 構成単位。 自動で環境に合わせてスケー ルし、並行や並列で動作す る。 "go"をつけるだけで起動可能 で非常に高機能。 61
func hello() { time.Sleep(time.Second * 2) // 2秒 sleep fmt.Println("Hello.") } func main() { bye := func() { time.Sleep(time.Second) // 1秒 sleep fmt.Println("Bye.") } // ゴルーチンとして関数呼び出し go hello() go bye() go func() { time.Sleep(time.Second * 3) // 3秒 sleep fmt.Println("Hey.") }() time.Sleep(time.Second * 4) // 4秒 sleep }
ゴルーチン 遅延の短い順に出ています 62 Bye. Hello. Hey.
ゴルーチンの特性 ゴルーチンはとても簡単に使えるだけでなく、非常に 軽量であるという特徴も持ち合わせています。なんとゴ ルーチンは1つにつき数キロバイトしかスタックメモリ を確保しません。 Windowsのスレッドが1MB、POSIXが2MBであること を考えるとかなり小さな値です。 63
ゴルーチンの特性 また、OSスレッドのOSコンテキストスイッチと比較し て、ゴルーチンのコンテキストスイッチは90%近く高速 に動作するため、コンテキストスイッチによってプロ セッサ時間を占領してしまう心配も小さく済みます。 64
チャネル チャネルは、主に並行プロセ スが通信に利用するための キューです。 makeの引数で格納可能数を指 定できます。 "<-"をチャネルの左右に置く ことで、チャネル内部の キューと値を送受信します。 65
func f(ch chan int) { defer close(ch) ch <- 10 ch <- 15 } func main() { ch := make(chan int, 1) go f(ch) fmt.Println(<-ch) fmt.Println(<-ch) // ゴルーチン起動前に終了してしまわないか? } 出力はどうなるでしょうか
チャネル "<-ch"のようにチャネルか ら値を受信しようとする と、値を受信するまでその 場で受信側のゴルーチンを 自動ブロックして待機して くれるため、タイミングを 考慮する必要がありませ ん。 66
10 30
チャネル 先程デッドロックを起こして いたコードをチャネルを使っ て実装してみました。 67 func main() { var wg
sync.WaitGroup sum := func(ch chan int) { defer wg.Done() time.Sleep(time.Second * 2) v1 := <-ch v2 := <-ch fmt.Printf("%d+%d=%d\n", v1, v2, v1+v2) } ch := make(chan int, 2) wg.Add(2) a, b := 10, 20 go sum(ch) go sum(ch) ch <- b ch <- a ch <- a ch <- b wg.Wait() } 出力はどうなるでしょうか
チャネル 実行2秒後に一斉に右の出 力が得られました。 メモリを共有していないた めメモリをロックする必要 がなく、デッドロックを起 こしていたスリープを残し たまま想定する結果が得ら れました。 68
20+10=30 10+20=30
チャネル 通信に向けた機能を持つ キューであるため、普通に FIFOのキューとして利用する ことも可能。 69 func main() { ch
:= make(chan int, 10) defer close(ch) ch <- 1 ch <- 2 ch <- 3 ch <- 4 fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) } 出力はどうなるでしょうか
チャネル 最初に入れたやつからちゃ んと出てきました 70 1 2 3 4
select文 多数のチャネルの受信を管理 し、チャネルごとに処理を場 合分けできる。 switch文に似ているが、順序 は無くすべての場合が同時に 判定されている。 ある程度の規模になると必須 71 func
main() { ch1 := make(chan int) ch2 := make(chan string) chend := make(chan struct{}) go func(chint chan<- int, chstr chan<- string, chend chan<- struct{}) { for i := 1; i < 16; i++ { if i % 5 + i % 3 == 0 { chstr <- "FizzBuzz" } else if i % 3 == 0 { chstr <- "Fizz" } else if i % 5 == 0 { chstr <- "Buzz" } else { chint <- i } } close(chend) }(ch1, ch2, chend) for { select { case val := <-ch1: fmt.Println(val) case str := <-ch2: fmt.Println(str) case <-chend: fmt.Println("finish.") return } } }
Goの並行処理 このように、他言語と比較してGoでは非常に自然に並 行処理が記述できる仕組みが整っています。 扱いやすいものを提供するが中身を隠してしまうわけ ではない抽象化の仕方がなされているため、大変実装が しやすいと感じています。 72
まとめ Summary
まとめ - 並行処理と並列処理は違う - 逐次処理の実装とは毛色が違う - ちょっと低レイヤ側の仕組みを把握する必要がある - Goには独自のゴルーチンとチャネルを用いた抽象化 概念があって実装の助けになる
- みんなもやろう 74
おわり 75