#はじめに
タイトルをあえて、**"マルチスレッド"**とだけ書き、並行・並列
という言葉を使うのを避けました。
理由としては、
-
goroutine
はruntime.NumCPU()
の値(論理コア数)が複数だった場合、並列処理となる。 - 論理コアごとに
gorotine
と紐づくカーネルスレッドが実行される**M:Nモデル
**を採用しているので、同時に複数の処理が走る。(並行処理)
ただ、並行は並列を包含するので、
強いて言えば、goroutineは、並行処理というのがいいのかもしれませんね。
すでに「並」でゲシュタルト崩壊発生( ;´Д`)
#マルチスレッドとは
すでに、並列処理・並行処理ってなんやねん!!って状態だと思いますが、
図をご覧下さい。
##並行(concurrent)
並列とは違い、複数のタスクを1人が処理するイメージ。
シングルコアのCPUが複数の処理を同時に動かすことなどが挙げられる。
たとえば、主婦が、炊事、洗濯、掃除を切り替えながら同時かつ並行的に家事を進める感じ。
##並列(parallel)
並行とは違い、複数人が同時に複数のタスクを処理すること。
複数のコアのCPUが、それぞれのスレッドを同時に動かすのであれば、それは並列処理
※「並行かつ並列」という状態は起こりうる
たとえば、主婦が家政婦を引き連れて陣形を組み、
主婦:炊事、家政婦A:洗濯、家政婦B:掃除
みたいに担当を決めて、同時かつ並列的に家事を進める感じ。
ただ、主婦が買出しも引き受けて、炊事と並行的に二つを進めれば、並行かつ並列の状態。
#goroutineの特長
ざっくり言うと、マルチスレッド処理が簡単に実装できてかつ、高速に実行できるすごいやーつ。
理由としては、
- 複数のユーザ・スレッドに対して、複数のカーネル・スレッドをマップする
M:Nモデル
を使用してるので、ユーザスレッドであるgoroutine
はコンテクストスイッチするが、カーネルスレッドはコンテクストスイッチせずに、オーバーヘッドが発生しづらいから。 -
goroutine
のスタックはガードページを使わない&初期サイズが2KBだから、メモリ使用量が少ない
###用語
コンテキストスイッチとは、コンピュータの処理装置(CPU)が現在実行している処理の流れ(プロセス、スレッド)を一時停止し、別のものに切り替えて実行を再開すること。
IT用語辞典 e-Words
ガードページは、ヒープブロック間にガードページと呼ばれるアクセスを禁止した領域を置くことで、ヒープ領域を超えて書き込みが行われた場合、プログラムを異常終了させるものです。
ヒープに対する攻撃とその対策
#goroutineの構成要素
-
goroutine
:マルチスレッド化 -
channel
:スレッド同士で値のやりとりをできる経路の役割 -
select
:複数のチャネル操作を待つことができる
という3つの機能を駆使するといいらしい。
※本編では goroutine
のみで実験します。channel
とか別で挑戦したい。
#書き方
// function定義をしてgoroutine実行
func goroutine(){
//処理
}
func main() {
go goroutine()
}
// 無名関数を定義して
func main() {
goroutine := func() {
//処理
}
}
// 無名関数的に(省略形)
func main() {
go func() {
//処理
}() //←()忘れないように
}
// ちなみにループだと、コピーを引数に渡さないと、参照になってしまい、想定しない数字が出力されるよ
for _, val := range values {
go func(i int) { // 引数を追加
fmt.Println(i)
}(val) // 関数実行時に現在の値を渡す
}
#コード
今回は、go gin のフレームワークを使って、REST API
を作成し、複数のURLを[,]区切りで入力すると、そのページ先からタイトルを抽出して、かかった時間と一緒に結果を返すというmain.go
にしてます。
Webスクレイピングには、goquery という便利なライブラリを使いました。
(※エディタはgoland
を使いました。Go Modules
とかの管理が楽)
##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をつかってリクエストを送ってみました。
※'Content-Type': 'application/json'
なので、プリフライトリクエストが送られる模様。
参考元:CORS
<!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
さんもいれてみました。
###scrape
ボタンをポチッとなと...
###おおお、タイトルと掛かった時間が取得できた。
URLの順番的には、
- Yahoo
- Qiita
- Amazon
と入力したけど、
###返ってきた結果は、
- Yahoo
- Qiita
- Amazon
の順番でした。
スレッドだから順不同なので、早く終わったものから、
レスポンス用の配列に追加されてった感じですな。
- Yahoo
- Qiita
- Amazon
###順番が変わった。
#まとめ
goroutine
奥が深すぎて、難しい印象。だけど記述自体はものすごく簡単だ。
マルチスレッドプログラミングができるようになるには、
メモリのこととか、処理の待ちとか、値の受け渡し方、チャンネルのこととか色々考えていかないといけないんだな〜
って思いました。
まだまだ習得には時間がかかりそうです。
頑張ってまいります!
以上、ありがとうございました!