おれんじりりぃぶろぐ

きっと何者にもなれないエンジニアのブログ

Unityでの並列プログラミング(Parallel、PLinq)のパフォーマンス比較

概略

.NET4.0からは、簡易に並列処理を記述することができるParallelクラス、Parallel LINQ(PLinq)の提供がはじまりました。 今回は、前半でParallクラス、PLinqの使い方を見て、後半ではUnityでの実際のパフォーマンス調査を行った結果を示します。

Parallelクラス、PLinqクラスの使い方

For文

Parallel.For(0, N, i =>
{
    lock(syncObj){sum += i ;}
});

Foreach文

foreach (var x in data)
{
    lock(syncObj){sum += x;}
}

同じリソース(今の場合はsum)へのアクセスがある場合は排他制御が必要になります。

PLinq

data.AsParallel().Sum(x => x );

通常のLinq構文にAsParallel()をつけるだけになります。 PLinqの場合には、排他制御はシステム側が行ってくれます。

パフォーマンス検証

実行環境

  • Unity2018.1b13
    • .NET4.x
    • il2cpp
  • iPhone7Plus
  • android s8

実施項目

0 〜 29999の単純な加算を以下の4つの方法で処理時間測定した * 通常のFor文(SequentialSum) * Parallel.For文(ParallelSum) * 通常のLinq(PlaneLinqSum) * PLinq(PlinqSum)

ソースコード

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;

public class ParallelSample : MonoBehaviour
{

    private const int N = 30000;
    private System.Object syncObj = new System.Object();
    
    
    private void Start()
    {
        
    }

    public void SequentialSum()
    {
        int starttime = DateTime.Now.Hour * 60 *60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                    DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        int sum = 0;
        for (int i = 0; i < N; i++)
        {
            sum += i;
        }
        Debug.Log($"sum: {sum}");
        
        int now = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
              DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        Debug.Log($"SequentialSum()実行時間: {now - starttime}ms");
    }

    public void ParallelSum()
    {
        int starttime = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                        DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        int sum = 0;
        Parallel.For(0, N, i =>
        {
            lock(syncObj){sum += i ;}
        });
        Debug.Log($"sum: {sum}");
        
        int now = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                  DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        Debug.Log($"ParallelSum()実行時間: {now - starttime}ms");
    }

    public void PlaneLinqSum()
    {
        int starttime = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                        DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        var data = Enumerable.Range(0, N);
        
        int sum = data.Sum(x => x );
        Debug.Log($"sum: {sum}");
        
        int now = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                  DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        Debug.Log($"PlaneLinqSum()実行時間: {now - starttime}ms");
    }

    public void PlinqSum()
    {
        int starttime = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                        DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        var data = Enumerable.Range(0, N);
        
        int sum = data.AsParallel().Sum(x => x );
        Debug.Log($"sum: {sum}");
        
        int now = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                  DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        Debug.Log($"PlinqSum()実行時間: {now - starttime}ms");
    }
}

結果

SequentialSum ParallelSum PlaneLinqSum PlinqSum
Editor 4.4 22.2 7.8 31.2
ios 4.2 16.0 8.0 x
android 2.0 58.6 5.0 x

5回の平均値 単位はミリSec il2cppでのPlinqは以下のランタイムエラーが出て実行不可能

Unity   : ExecutionEngineException: Attempting to call method 'System.Linq.Parallel.SelectQueryOperator`2<System.Int32, System.Int32>::WrapPartitionedStream<System.Int32>' for which no ahead of time (AOT) code was generated. 

まとめ

  • PLinqはil2cpp環境では使用できない
  • 並列ループ内で同じリソースにアクセスする場合には、排他制御を行わなければならず結局は同期的な処理に近くなってしまう。さらにオーバーヘッドが大きく通常のFor文よりも遅くなる
  • Linqは気になるほどではないが通常のFor文に比べると遅い

参考

UnityにおけるAwaitAsyncを使った非同期実装について④

概略

前回までに最新Unityにおけるマルチスレッドプログラミングについて、さらにその時直面する問題点について紹介してきました。今回は、Task、awaitを使ったマルチスレッドでの排他制御について触れていきたいと思います。 今回も特にUnityに特化した情報はありません。

実装方法

複数のスレッドでから同時にアクセスされる可能性があるリソースにはlock文を使い排他制御をします。

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class AsyncSampleScene : MonoBehaviour
{

    private int sum = 0; // 同時にアクセスされる可能性があるデータ
    private object syncObject = new object();

    private async void Start()
    {
        sum = 0;
        
        List<Task> t = new List<Task>();
        t.Add(TestAsyncMethod());
        t.Add(TestAsyncMethod());
        t.Add(TestAsyncMethod());
        await Task.WhenAll(t);
        
        Debug.Log($"sum: {sum}");
    }

    private async Task TestAsyncMethod()
    {
        await Task.Run(() =>
        {
            lockSum();
        });
    }

    // だめな例
    private void nolockSum()
    {
        for (int i = 1; i <= 1000; i++)
        {
            sum += i * i;
        }
    }
    

    private void lockSum()
    {
        for (int i = 1; i <= 1000; i++)
        {
            // 同時にアクセスされる可能性があるデータ
            lock(syncObject) { sum += i * i; }
        }
    }
}

await句の中ではlock文を使うことはできません。ロックをかけたスレッドとロックを解除するスレッドが異なる可能性があるからです。 nolockSum()の場合は、sumに対して競合が発生して毎回異なる結果になってしまいます。

まとめ

AsyncAwaitが導入されようとも漏れなく排他制御を行うのは難しい(´・_・`)

UnityにおけるAwaitAsyncを使った非同期実装について③

これまでTask、AsyncAwaitを使った非同期処理の実装、メインスレッド以外から特定の(メイン)スレッドにアクセスする方法についてみてきました。 今回は、複数の非同期に実行されるTaskをハンドリングする方法について説明します。今回もUnityに特化した情報はありません。

以下のコードがあったとします。

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class AsyncSampleScene : MonoBehaviour {

    private async void Start()
    {
        await TestAsyncMethod1();
        await TestAsyncMethod2();
        await TestAsyncMethod3();
        Debug.Log("All Compleated!");
    }

    private async Task TestAsyncMethod1()
    {
        await Task.Run(() => {
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 1");
            Thread.Sleep(1000); 
        });
    }
    
    private async Task TestAsyncMethod2()
    {
        await Task.Run(() => {
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 2");
            Thread.Sleep(1000); 
        });
    }
    
    private async Task TestAsyncMethod3()
    {
        await Task.Run(() => {
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 3");
            Thread.Sleep(1000); 
        });
    }
}

TestAsyncMethod1()が完了したらTestAsyncMethod2()が実行され、TestAsyncMethod2()が完了したらTestAsyncMethod3()が実行され、 TestAsyncMethod3()が完了したら最後にDebugログが出力されます。

図にすると以下のようになります。 スクリーンショット 2018-04-16 19.33.41.png (77.7 kB)

しかし、TestAsyncMethod1()TestAsyncMethod2()TestAsyncMethod3()に依存関係が特にない場合にはいちいちawait()するのは無駄です。 この場合、このようになってほしいわけです。 スクリーンショット 2018-04-16 19.46.45.png (76.1 kB)

それぞれのTaskをパラレルに実行し、全て完了したらDebugログを出力しています。

このような実装は以下のコードで実現できます。

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class AsyncSampleScene : MonoBehaviour {

    private async void Start()
    {
        List<Task> tasks = new List<Task>(); 
        tasks.Add(TestAsyncMethod1());
        tasks.Add(TestAsyncMethod2());
        tasks.Add(TestAsyncMethod3());

        await Task.WhenAll(tasks);
        
        Debug.Log("All Compleated!");
    }

    private async Task TestAsyncMethod1()
    {
        await Task.Run(() => {
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 1");
            Thread.Sleep(1000); 
        });
    }
    
    private async Task TestAsyncMethod2()
    {
        await Task.Run(() => {
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 2");
            Thread.Sleep(1000); 
        });
    }
    
    private async Task TestAsyncMethod3()
    {
        await Task.Run(() => {
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 3");
            Thread.Sleep(1000); 
        });
    }
}

tasksというTaskのコレクションを生成し、各々のtaskをaddしていきます。最後にTask.WhenAll()awaitよって全てのコレクション内のTaskが完了したタイミングを取得することができます。

参考

UnityにおけるAwaitAsyncを使った非同期実装について②

概略

前回は、最新Unityにおけるマルチスレッドプログラミングの方法についての紹介をしました。 今回はUnityでTask、awaitを使ったマルチスレッドを行う時に起きる問題について取り上げます。

Unityにおけるメインスレッド制約

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class AsyncSampleScene : MonoBehaviour
{

    [SerializeField] private Text text;

    private async void Start()
    {
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, Start Start()");
        await TestAsyncMethod();
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, End Start()");
    }

    private async Task TestAsyncMethod()
    {
        await Task.Run(() =>
        {
            Thread.Sleep(1000);
            text.text = "1";  
        });
    }
}

上記のコードを実行すると以下のエラーが出ます。

get_isActiveAndEnabled can only be called from the main thread.
Constructors and field initializers will be executed from the loading thread when loading a scene.
Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.

これはメインスレッド以外からTextにアクセスを行っているため、メインスレッドで実行してくださいというエラー内容になります。 MonoBehaviourを継承しているクラスの使用についてはメインスレッドからアクセスしなければならないという制約があるためです。 では、この問題にはどのような対処方法が考えられるでしょうか?

対処方法

これに対する対処には以下の方法が考えられます。 まずは、stringを返す方法です。

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class AsyncSampleScene : MonoBehaviour
{

    [SerializeField] private Text text;

    private async void Start()
    {
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, Start Start()");
        text.text = await TestAsyncMethod();
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, End Start()");
    }

    private async Task<string> TestAsyncMethod()
    {
        var text = string.Empty;
        await Task.Run(() =>
        {
            Thread.Sleep(1000);
            text = "1";  
            return text;
        });
        return text;
    }
}

2つ目がコルーチンを使う方法です。 そして、3つ目があるスレッドから特定のスレッド(メインスレッド)にアクセスする方法になります。

あるスレッドからメインスレッドへのアクセス方法

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class AsyncSampleScene : MonoBehaviour
{

    [SerializeField] private Text text;

    private async void Start()
    {
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, Start Start()");
        await TestAsyncMethod();
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, End Start()");
    }

    private async Task TestAsyncMethod()
    {
        var context = SynchronizationContext.Current;

        await Task.Run(() =>
        {
            context.Post((state) =>
            {
                text.text = "1";
            }, null);

            Thread.Sleep(1000);

            context.Post((state) =>
            {
                text.text = "2";
            }, null);

            Thread.Sleep(1000);

            context.Post((state) =>
            {
                text.text = "3";
            }, null);

            Thread.Sleep(1000);
        });
    }
}

SynchronizationContext.Currentは現在の同期コンテキストを取得できるプロパティになり、すなわちメインスレッドを取得することができます。 androidjava層からUnityのメインスレッドのコンテキストを取得して操作を行うイメージに近いと思います。

参考

gasを使ったお手軽WebAPI実装

概要

超お手軽にWebAPIを作る方法です。 GASを使って、POSTしてスプレッドシートにデータを保存してそれをGETする方法です。 40秒でAPI作りなと言われた時の対応法です。

実演

POSTするとspreadsheetに保存してくれます。

curl -X POST -d "timestamp=1523790159&content=hoge" https://script.google.com/macros/s/xxxx/exec

スクリーンショット 2018-04-15 22.43.41.png (80.4 kB)

GETするとjsonを返してくれます。

curl -L -X GET https://script.google.com/macros/s/xxxx/exec
{"timestamp":"1523790159","content":"hoge"}

手順

「新規作成」-> 「スプレッドシート」を選択します。 スクリーンショット 2018-04-15 22.45.01.png (79.8 kB)

「ツール」->「スクリプトエディタ」でgasを起動します。 スクリーンショット 2018-04-15 22.46.02.png (103.3 kB)

以下のソースコードを実装します。

// データ更新
function doPost(e) {
  // パラメータのパース
  var timestamp = e.parameters.timestamp;
  var content = e.parameters.content;

  var sheet = SpreadsheetApp.getActive().getSheetByName('シート1');
  var sheetData  = sheet.getDataRange().getValues();
  sheetData.push([timestamp, content]);
  // シートへの書き込み、getRange(開始行、開始列、行数、列数)
  sheet.getRange(1,1,sheetData.length,2).setValues(sheetData);
}

// データ取得
function doGet(e) {  
  var sheet = SpreadsheetApp.getActive().getSheetByName('シート1');
  var sheetData = sheet.getDataRange().getValues();

// 最終行を取得する
  var timestamp = String(sheet.getRange(sheet.getLastRow(),1,1,1).getValue());
  var content = sheet.getRange(sheet.getLastRow(),2,1,1).getValue();

  var result = {'timestamp': timestamp, 'content': content};

  return ContentService.createTextOutput(JSON.stringify(result)).setMimeType(ContentService.MimeType.JSON);
}

POSTリクエストが来た場合にはdoPost()がGETリクエストが来た場合にはdoGet()が呼ばれます。

JSONでのPOST

jsonでpostしたい場合は以下のようにします。

function doPost(e) {
  var json = e.postData.getDataAsString();
  var data = JSON.parse(json);
  
  var timestamp = data.timestamp
  var content = data.content

  var sheet = SpreadsheetApp.getActive().getSheetByName('シート1');
  var sheetData  = sheet.getDataRange().getValues();
  sheetData.push([timestamp, content]);
  // シートへの書き込み、getRange(開始行、開始列、行数、列数)
  sheet.getRange(1,1,sheetData.length,2).setValues(sheetData);
}

アクセスできるように公開する

[公開] -> 「ウェブアプリケーションとして導入」を選択します。 スクリーンショット 2018-04-16 0.23.24.png (53.4 kB)

以下のように設定して導入をクリックします。 スクリーンショット 2018-04-16 1.04.35.png (77.3 kB)

アプリケーションにアクセスできるユーザーは「全員(匿名ユーザーを含む)」にする必要があります。会社などの組織アカウントでは、制限が書けられていてこの選択肢がないこともあります。その場合は個人アカウントを使ってください。

アクセス先のURLが表示されるのでメモします。

注意

スクリプトの更新を行った時には、ウェブアプリケーションの導入を再度行う必要があります。(面倒くさい)その場合でもURLは変わらないようです。

おまけ

Unityからでも問題なく使えます。

using UnityEngine;
using System.Collections;
using UnityEngine.Networking;
 
public class APITest : MonoBehaviour {
    void Start() {
        StartCoroutine(GetText());
    }
 
    IEnumerator GetText() {
        UnityWebRequest www = UnityWebRequest.Get("https://script.google.com/macros/s/xxxx/exec");
        yield return www.SendWebRequest();
 
        if(www.isNetworkError || www.isHttpError) {
            Debug.Log(www.error);
        }
        else {
            // 結果をテキストとして表示します
            Debug.Log(www.downloadHandler.text);
        }
    }
}
{"timestamp":"1523790159","content":"hoge"}
UnityEngine.Debug:Log(Object)

参考

UnityにおけるAwaitAsyncを使った非同期実装について①

概略

Unity2018.1から正式に.Net4.7、C#6への対応がなされます。 C#5からは、非同期的な実装をまるで同期的に記述できるasync/awaitというキーワードが導入されました。今回はasync/awaitを使ったUnityにおけるマルチスレッドプログラミングについて解説を行います。

非同期実装を行う必要性とは(・・?)

同期実装とは、プログラムを記述通り順番に実行していきます。対して、非同期実装では記述順とは異なる順序での実行がなされます。どうして非同期に処理を行う必要があるのでしょうか。

スクリーンショット 2018-04-13 20.13.05.png (69.3 kB)

この図は同期処理を表したものです。HeavyMethod()の実行中は、メインスレッドを専有してしまいUIが固まる、イベントが受けられないなどといった状況に陥ってしまいます。 非同期処理の必要性は、メインスレッドの専有を避けることにあります。

非同期実装を行う方法

  • Unityコルーチン
  • async/await(C#5以降)

Unityでは、非同期な処理を行えるようにコルーチンの仕組みが用意されています。

スクリーンショット 2018-04-13 20.09.49.png (65.8 kB)

コルーチンでは、各処理を高速に切り替え一つの処理がメインスレッドを専有しない仕組みになっています。しかし、全ての処理をメインスレッドで行っているため処理限界に達することがあります。 重たい処理に関してはメインスレッドではなく別スレッドで実行し処理が終わったらメインスレッドに戻ることが求められます。

スクリーンショット 2018-04-13 20.32.30.png (73.6 kB)

C#5以前でもThreadを扱うクラスがありマルチスレッドな実装を行うことは可能でした。しかし、低レベルなAPIレベルでしか提供がなされておらずやや扱いにくい印象でした。C#5以降ではこれを解消する直感的かつ簡潔にまるで同期処理のように非同期処理が記述できるasync/awaitの仕組みがで導入されました。

Taskの説明

マルチスレッドな非同期処理を見ていく前にTaskの説明を行います。 Taskはある一連の処理をまとめた一つの単位になります。

var task = Task.Run(() =>
{
    Debug.Log("HelloWorld");
    Debug.Log("HelloWorld");
    Debug.Log("HelloWorld");
});

これはデバッグログを3回出力するという一つのTaskになります。 Task.Runは、Taskのファクトリメソッドになります。 Taskでの処理は自動的にスレッドプールになります。

マルチスレッドの実装

さて、Taskについての理解ができました。 このTask単位でマルチスレッドに行う方法を見ていきます。

シンプルなマルチスレッド実装

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class AsyncSampleScene : MonoBehaviour {

    private async void Start()
    {
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, Start Start()");
        await TestAsyncMethod();
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, End Start()");
    }

    private Task TestAsyncMethod()
    {
        return Task.Run(() => {
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 1");
            Thread.Sleep(1000); 
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 2");
            Thread.Sleep(1000);
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 3");
            Thread.Sleep(1000);
        });
    }
}

結果

ID 1, Start Start() //メインスレッド
ID 86, [Async] 1 // 別スレッド
ID 86, [Async] 2 // 別スレッド
ID 86, [Async] 3 // 別スレッド
ID 1, End Start() // メインスレッド

(ここでのIDはスレッドごとに割り当てられる一意のIDです)

メインスレッドでログを1回出力し、別のスレッドで処理を実行し、最後にまたメインスレッドに戻ってログを出力しています。 処理を別スレッドに逃すことができ、その完了をハンドリングしてメインスレッドに戻ることができました。 ポイントは、awaitとTask.Runです。 実行関数の前にawaitをつけるとTestAsyncMethod()の完了を待って、処理が終わったら後続処理をメインスレッドで続行してくれます。awaitを使ったメソッドではシグネチャにasyncをつけることになっています。 Task.RunはTaskのファクトリーメソッドであり、この中のTaskを別スレッドで実行してくれます。

返り値がある場合は以下のようになります。

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class AsyncSampleScene : MonoBehaviour
{
    private async void Start()
    {
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, Start Start()");
        int j = await TestAsyncMethod();
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, {j}");
    }

    private Task<int> TestAsyncMethod()
    {
        return Task.Run<int>(() =>
        {
            int i = 0;
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 1");
            i++;
            Thread.Sleep(1000); // 擬似的な重たい処理
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 2");
            i++;
            Thread.Sleep(1000);
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 3");
            i++;
            Thread.Sleep(1000);
            return i;
        });
    }
}

結果

ID 1, Start Start()
ID 116, [Async] 1
ID 116, [Async] 2
ID 116, [Async] 3
ID 1, End Start()

Task.Wait()の罠

次に示すのは、デットロックを引き起こすヤバイコードです。 (UnityEditorで実行すると応答なくなって強制終了するしかなくなります)

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class AsyncSampleScene : MonoBehaviour {

    private void Start()
    {
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, Start Start()");
        Task t = KusoAsyncMethod();
        t.Wait(); // tのTaskが終わるまで待つ
        Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, End Start()");
    }

    private Task KusoAsyncMethod()
    {
        return Task.Run(() => {
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 1");
            Thread.Sleep(1000);
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 2");
            Thread.Sleep(1000);
            Debug.Log($"ID {Thread.CurrentThread.ManagedThreadId}, [Async] 3");
            Thread.Sleep(1000);
        });
    }
}

なぜこのようなことになってしまうのでしょうか。

スクリーンショット 2018-04-14 0.06.22.png (111.2 kB)

図にすると上図のような状態に陥ってしまいます。 一方で、awaitを使うと下図になり挙動が異なることが分かります。

スクリーンショット 2018-04-14 0.27.05.png (105.1 kB)

まとめ

  • Task、async/awaitを使うことでマルチスレッドな非同期処理を直感的に記述することができる
  • Taskクラスを使うとスレッドプールを自動で行ってくれる
  • 別スレッドで発生した例外もキャッチすることができる
  • Task.Wait()またはTask.Result()をデットロックに陥る危険性がある

参考

gasを使って特定のGoogleドライブ以下のファイル更新を通知する

概要

特定のGoogleドライブ以下のファイル更新を検知してCWで通知する方法です。

なぜgasなのか(・・?)

何と言ってもスクリプトを実行するサーバーを用意しなくて済むからです。chatworkへの通知も非常にお手軽に実装することが可能です。 Googleドライブとの親和性ももちろん高く、簡単な実装で実現が可能です。

手順

ドライブの「新規作成」->「その他」->「Goggle Apps Script」を選択します。 スクリーンショット 2018-03-15 2.30.09.png (142.2 kB)

「リソース」->「ライブラリ」からChatworkClientを導入します。 スクリーンショット 2018-03-15 2.07.11.png (63.6 kB)

ライブラリを追加のところに「M6TcEyniCs1xb3sdXFF_FhI-MNonZQ_sT」を入力します。 * https://github.com/cw-shibuya/chatwork-client-gas

いい感じにスクリプトを書きます。

var Token = "xxxxxx";  // chatwork bot
var RoomId = xxxxx; // CWの部屋ID

//対象とするGoogleDrive親フォルダのID
// IDはURLのfolders/以下の英数字
var PARENT_FOLDER_ID = "xxxx";

// 24時間(ミリsec)
var SEARCH_TIME = 86400;

function myFunction() {
  var parentFolder = DriveApp.getFolderById(PARENT_FOLDER_ID);
  
  var dateobj = new Date();
  var nowUnixTime = Math.floor(dateobj.getTime() / 1000) ;
  var dayAgoUnixTime = nowUnixTime - SEARCH_TIME;
  var mes = "";
  
  mes += "[info][title](F)本日更新されたドキュメント一覧(F)[/title]";
  
  var allFiles = getAllFilesName(parentFolder);
  for(var i = 0; i < allFiles.length; i++){
    // ファイル更新のタイムスタンプ(unixtime)
    var fileTimeStamp = Math.floor(Date.parse(allFiles[i].getLastUpdated())/1000);
    
    if(fileTimeStamp > dayAgoUnixTime){
      mes += "* ";
      mes += allFiles[i].getName();
      mes += "\n";
      
      mes += "  * ";
      mes += allFiles[i].getUrl();
      mes += "\n";
      
    }
    
    //Logger.log(allFiles[i].getName());
  }

  mes += "[/info]";
  
  // CW通知
  var client = ChatWorkClient.factory({token: Token});
  client.sendMessage({room_id: RoomId, body: mes});
  
}

function getAllFilesName(parentFolder){
  var fileList = [];
  var files = parentFolder.getFiles();
  while(files.hasNext()){
    fileList.push(files.next());
  }
  
  var childFolders = parentFolder.getFolders();
  
  while(childFolders.hasNext()){
    var childFolder = childFolders.next();
    
    // 再帰的な呼び出し
    fileList = fileList.concat(getAllFilesName(childFolder));
  }
  return fileList;
}

getFiles()getFolders()では、そのフォルダに属したファイルやフォルダしか取れないため再帰的に取得を行う必要があります。

トリガーを好きな時間に設定します。 「編集」->「現在のプロジェクトのトリガー」->「新しいトリガーを追加」から設定します。

スクリーンショット 2018-03-15 2.17.24.png (55.7 kB)

通知が来ます スクリーンショット 2018-03-18 0.30.01.png (36.6 kB)

リファレンス

gasを使ったお手軽サーバー死活監視

概要

超お手軽にWebサーバーの死活監視をしてchatworkに通知する方法です。

なぜgasなのか(・・?)

何と言っても死活監視のスクリプトをcronするサーバーを用意しなくて済むからです。chatworkへの通知も非常にお手軽に実装することが可能です。 高度なことはできませんが、雑に死活監視するくらいであればgas環境でも十分です。

手順

ドライブの「新規作成」->「その他」->「Goggle Apps Script」を選択します。 スクリーンショット 2018-03-15 2.30.09.png (142.2 kB)

「リソース」->「ライブラリ」からChatworkClientを導入します。 スクリーンショット 2018-03-15 2.07.11.png (63.6 kB)

ライブラリを追加のところに「M6TcEyniCs1xb3sdXFF_FhI-MNonZQ_sT」を入力します。

いい感じにスクリプトを書きます。

var siteURL = "http://xxxxxxxx.com"; // チェックしたいサーバーのURL
var Token = "xxxx"; // chatworkのbot token
var RoomId = xxxx; // 通知したい部屋のID

function myFunction() {

  try {
    // URLをフェッチ - muteHttpExceptions:trueの場合、HTTPエラーの際に例外をスローしない
    var response = UrlFetchApp.fetch(siteURL, { muteHttpExceptions:true });
    // レスポンスコード
    var code = response.getResponseCode();

    // レスポンスコード 200をチェックする
    if(code == 200) {
        Logger.log("access OK");
        Logger.log("Response code: " + code);
    } else {
        var mes = "[info][title]死活監視[/title][To:xxxx]\nhogehogeが落ちています[/info]";
        Logger.log(mes);
        var client = ChatWorkClient.factory({token: Token});
        client.sendMessage({room_id: RoomId, body: mes});
    }
  } catch(err) {
    // catch : DNSエラーなどでURLをfetch出来ないとき
    var mes = "[info][title]死活監視[/title][To:xxxx]\nhogehogeにアクセスできません[/info]";
    Logger.log(mes);
    var client = ChatWorkClient.factory({token: Token});
    client.sendMessage({room_id: RoomId, body: mes});
  }
}

トリガーを設定します。 「編集」->「現在のプロジェクトのトリガー」->「新しいトリガーを追加」から設定します。 スクリーンショット 2018-03-15 2.17.24.png (55.7 kB)

あとは、サーバーが落ちるのを楽しみに待つだけです٩( ‘ω’ )و