【並列・並行解説付き】goroutineでマルチスレッド処理を試してみた

#はじめに
タイトルをあえて、**"マルチスレッド"**とだけ書き、`並行・並列`という言葉を使うのを避けました。
理由としては、

- `goroutine`は`runtime.NumCPU()`の値（論理コア数）が複数だった場合、並列処理となる。
- 論理コアごとに`gorotine`と紐づくカーネルスレッドが実行される**`M:Nモデル`**を採用しているので、同時に複数の処理が走る。（並行処理）

ただ、**並行は並列**を包含するので、
強いて言えば、goroutineは、並行処理というのがいいのかもしれませんね。

すでに「並」でゲシュタルト崩壊発生( ；´Д｀)

#マルチスレッドとは

すでに、並列処理・並行処理ってなんやねん！！って状態だと思いますが、
図をご覧下さい。
![スクリーンショット 2021-03-25 11.48.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/466c42e6-c416-e1bd-70be-57d4de34a8c4.png)


##並行(concurrent)
並列とは違い、複数のタスクを1人が処理するイメージ。
シングルコアのCPUが複数の処理を同時に動かすことなどが挙げられる。

たとえば、主婦が、炊事、洗濯、掃除を切り替えながら同時かつ並行的に家事を進める感じ。


##並列(parallel)
並行とは違い、複数人が同時に複数のタスクを処理すること。
複数のコアのCPUが、それぞれのスレッドを同時に動かすのであれば、それは並列処理
※**「並行かつ並列」という状態は起こりうる**

たとえば、主婦が家政婦を引き連れて陣形を組み、
主婦：炊事、家政婦A：洗濯、家政婦B：掃除
みたいに担当を決めて、同時かつ並列的に家事を進める感じ。
ただ、主婦が買出しも引き受けて、炊事と並行的に二つを進めれば、並行かつ並列の状態。


#goroutineの特長
ざっくり言うと、マルチスレッド処理が簡単に実装できてかつ、高速に実行できるすごいやーつ。

理由としては、

- 複数のユーザ・スレッドに対して、複数のカーネル・スレッドをマップする`M:Nモデル`を使用してるので、ユーザスレッドである`goroutine`はコンテクストスイッチするが、カーネルスレッドはコンテクストスイッチせずに、オーバーヘッドが発生しづらいから。
- `goroutine`のスタックはガードページを使わない＆初期サイズが2KBだから、メモリ使用量が少ない

###用語

>コンテキストスイッチとは、コンピュータの処理装置（CPU）が現在実行している処理の流れ（プロセス、スレッド）を一時停止し、別のものに切り替えて実行を再開すること。
[IT用語辞典 e-Words](https://e-words.jp/w/%E3%82%B3%E3%83%B3%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%82%B9%E3%82%A4%E3%83%83%E3%83%81.html)

　
>ガードページは、ヒープブロック間にガードページと呼ばれるアクセスを禁止した領域を置くことで、ヒープ領域を超えて書き込みが行われた場合、プログラムを異常終了させるものです。
[ヒープに対する攻撃とその対策](https://www.atmarkit.co.jp/ait/articles/1408/28/news010_3.html)

#goroutineの構成要素
- `goroutine`：マルチスレッド化
- `channel`：スレッド同士で値のやりとりをできる経路の役割
- `select`：複数のチャネル操作を待つことができる

という3つの機能を駆使するといいらしい。

※本編では `goroutine` のみで実験します。`channel`とか別で挑戦したい。


#書き方
```go:goroutine
// function定義をしてgoroutine実行
func goroutine(){
    //処理
}
func main() {
    go goroutine()
}
```
```go:goroutine
// 無名関数を定義して

func main() {
    goroutine := func() {
        //処理
    }
}
```

```go:goroutine
// 無名関数的に（省略形）
func main() {
    go func() {
        //処理
    }() //←()忘れないように
}

// ちなみにループだと、コピーを引数に渡さないと、参照になってしまい、想定しない数字が出力されるよ
for _, val := range values {
    go func(i int) { // 引数を追加
        fmt.Println(i)
    }(val) // 関数実行時に現在の値を渡す
}
```


#コード
今回は、[go gin](https://github.com/gin-gonic/gin) のフレームワークを使って、`REST API`を作成し、複数のURLを[,]区切りで入力すると、そのページ先からタイトルを抽出して、かかった時間と一緒に結果を返すという`main.go`にしてます。
Webスクレイピングには、[goquery](https://github.com/PuerkitoBio/goquery) という便利なライブラリを使いました。
（※エディタは`goland`を使いました。`Go Modules`とかの管理が楽）

##`main.go`

```go:main.go
package main

import (
	"bytes"
	"fmt"
	"golang.org/x/net/html/charset"
	"io/ioutil"
	"log"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/PuerkitoBio/goquery"
	"github.com/gin-gonic/gin"
	"github.com/saintfish/chardet"
)

func main() {
	// Start HTTP server
	r := gin.Default()
	r.GET("/scrape", scrapeText)
	if err := r.Run(":8080"); err != nil {
		log.Fatal(err)
	}
}

// ページタイトルと取得時間の構造体
type Title struct {
	Title string
	Time string
}

func scrapeText(c *gin.Context) {
	//クエリストリングから値を受け取り配列化
	var urls []string
	urls = strings.Split(c.Query("urls"), ",")

	//syncでURLの数だけ待ち合わせするWaitGroupの宣言
	var wg sync.WaitGroup
	wg.Add(len(urls))

	//titlesを格納する変数宣言
	var results []Title

	//titleをfetchする無名関数を代入
	fetchTitle := func(url string) {
		start := time.Now()
		defer wg.Done()

		// Getリクエスト
		res, _ := http.Get(url)
		defer res.Body.Close()

		// 読み取り
		buffer, _ := ioutil.ReadAll(res.Body)

		// 文字コード判定
		detector := chardet.NewTextDetector()
		detectResult, _ := detector.DetectBest(buffer)
		fmt.Println(detectResult.Charset)

		// 文字コード変換
		bufferReader := bytes.NewReader(buffer)
		reader, _ := charset.NewReaderLabel(detectResult.Charset, bufferReader)

		// HTMLパース
		document, _ := goquery.NewDocumentFromReader(reader)

		// titleを抜き出し
		title := document.Find("title").Text()
		end := time.Now();
		time := (end.Sub(start)).Seconds()
		result := Title{title, strconv.FormatFloat(time, 'f', -1, 64) }
		results= append(results, result)
	}

	//urlsの回数分スレッド実行
	for _, url := range urls {
		go fetchTitle(url)
	}

	//fetchTitle goroutineが終わるまで、wg.Wait()で待つ
	wg.Wait()

	c.JSON(http.StatusOK, results)
}
```

##`index.html`
[Fetch API](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API)をつかってリクエストを送ってみました。
※`'Content-Type': 'application/json'`なので、プリフライトリクエストが送られる模様。
 参考元：[CORS](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#simple_requests)

```html:index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta http-equiv="content-type" charset="utf-8">
  <title>Test Application</title>
</head>

<body>

  <form name="multiScraping" id="multiScraping">
    <p>URL：[,]カンマ区切りで複数入力</p>
    <p><textarea name="urls" rows="10" cols="80" placeholder="URL記入"></textarea></p>
    <p><input type="button" value="scrape" onclick='getTitles()'></p>
  </form>

  <ul id="titles"></ul>

  <script type="text/javascript">
    function getTitles() {
      const urls = document.forms['multiScraping'].elements['urls'].value;
      const params = { // 渡したいパラメータをJSON形式で書く
        urls: urls,
      };
      const queryParams = new URLSearchParams(params);
      const url_string = 'http://localhost:8080/scrape?';
      fetch(url_string + queryParams,
        {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
          }
        })
        .then((res) => {
          if (!res.ok) {
            throw new Error(`${res.status} ${res.statusText}`);
          }
          return res.json();//.json()にするとパースされたJSONが取得できる(promiseが返される)
        })
        .then((data) => {
          console.log(data);
          // 詰め替え処理して表示させる
          for(let i=0, len=data.length;i<len;i++) {
            let title = document.createElement("li");
            title.setAttribute("id", "title-"+i);
            title.innerHTML = data[i]['Title'] + '：' + data[i]['Time'] + '秒';
            document.getElementById("titles").appendChild(title);
          }
        })
        .catch((reason) => {
          console.log(reason);
        });
    }
  </script>
</body>

</html>
```


#早速試してみる
###まずは、URLをカンマ区切りで複数入力してみる。
とりあえず主要どころの`Google`,`Yahoo`,`Facebook`,`Amazon`、もちろん`Qiita`さんもいれてみました。
![スクリーンショット 2021-03-25 12.47.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/365e348c-33c8-8005-e131-614996d69968.png)

###`scrape`ボタンをポチッとなと...

![スクリーンショット 2021-03-25 12.49.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/8f8ecc7b-2263-2838-517e-5956980d56a4.png)

###おおお、タイトルと掛かった時間が取得できた。
URLの順番的には、

1. Google
2. Yahoo
3. Qiita
4. Facebook
5. Amazon

と入力したけど、
###返ってきた結果は、

1. Yahoo
2. Google
3. Qiita
4. Facebook
5. Amazon

の順番でした。
スレッドだから順不同なので、早く終わったものから、
レスポンス用の配列に追加されてった感じですな。

###2回目やると
![スクリーンショット 2021-03-25 13.00.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1075243/63f4fe24-defc-5ecd-eeb2-a8b38090dc91.png)

1. Yahoo
2. Google
3. Facebook
4. Qiita
5. Amazon

###順番が変わった。

#まとめ
`goroutine`奥が深すぎて、難しい印象。だけど記述自体はものすごく簡単だ。

マルチスレッドプログラミングができるようになるには、
メモリのこととか、処理の待ちとか、値の受け渡し方、チャンネルのこととか色々考えていかないといけないんだな〜
って思いました。
まだまだ習得には時間がかかりそうです。

頑張ってまいります！

以上、ありがとうございました！