Inside Out

自分自身の備忘録。アウトプット用のブログです。

食事する哲学者問題を C# で解いてみる

これは Sansan Advent Calendar 2020 の17日目の記事です。 adventar.org

アドベントカレンダーとか機会がないとブログとかめったに書かないので、一応例年書いてるし今年も何か書こうかなと思ってた頃に同僚から誘われて The Art of Multiprocessor Programming を読み始めたので、それに関連した記事です。

食事する哲学者問題

この本各章毎に Exercise がついており第1章の最初の Exercise に以下のような問題がついておりました。

以下の状況を想像してほしい。
5人の哲学者がいて彼らは思索を巡らすのと食事をする事だけで生きている。彼らは円卓に座っており、そこには5本の箸が置かれている。
彼らは空腹になると自分に最も近い箸を2本取る。2本の箸が取れればしばしの間食事を始める。食事を終えると箸を円卓に置き、再び思索を巡らせる。

1. これを哲学者をスレッドに、箸を共有オブジェクトに見たててプログラムで表現せよ。なお哲学者が同時に同じ箸を取る事は防がなければならない。
2. 全ての哲学者が箸を1本持って2本目の箸が行きわたらない状況を防ぐようにプログラムを修正せよ。
3. 哲学者が飢えないようにプログラムを修正せよ。

ド文系で職業ITエンジニアやってる自分はこの問題について初めて知りましたが、計算機科学の課程ではほぼ含まれてる「食事する哲学者問題(Dining Philosophers Problem)」として有名な話だそうです。元々は構造化プログラミングで有名なエドガー・ダイクストラが5台のコンピュータからテープ装置への競合アクセスする問題を考案しそれを、クイックソートの考案者のアントニー・ホーアが哲学者の話に変形させたものが広まったものらしいです。今回はまぁCS学習者にとっては常識っぽい問題ですがこれをといてみようかなと思います。

1 の回答

この問題が提示しているのはいわゆる排他制御で発生する問題です。本に書かれてる問題 1. を単にやると状況だと各哲学者が左の箸を持って右の箸を取ろうとするとそれは別の哲学者にとっては左の箸であり、すでに別の哲学者が持っているために取得できず延々と待ち続ける状態になります。いわゆる dead lock 状態となります。以下のコードは 1. を表現するコードですが、実行してみると食事にありつけずに延々と考えつづけます。

namespace DiningPhilosophers
{
    class Program
    {
        static void Main(string[] args)
        {
            var chopsticks = Enumerable.Range(0, 5).Select(x => new Chopstick(x)).ToList();
            var philosophers = new List<Philosopher>();

            for (var i = 0; i < chopsticks.Count; i++)
            {
                var philosopher = i == (chopsticks.Count - 1)
                    ? new Philosopher(i,chopsticks[i], chopsticks[0])
                    : new Philosopher(i, chopsticks[i], chopsticks[i + 1]);
                philosophers.Add(philosopher);
            }

            // Task は ThreadPool の状態に依存して必ず別 Thread での実行である事を保証しないので今回は 生 Thread を使う
            var threads = philosophers.Select(x => new Thread(() =>
            {
                while (true)
                {
                    try
                    {
                        x.Think();

                        x.Eat();
                    }
                    catch (ThreadInterruptedException)
                    {
                        Console.WriteLine("thread interrupted.");
                        break;
                    }
                }
            })).ToArray();

            foreach (var thread in threads)
            {
                thread.Start();
            }
        }
    }

    public class Philosopher
    {
        public Chopstick RightChopstick { get; }
        public Chopstick LeftChopstick { get; }

        public int PhilosopherId { get; }

        public Philosopher(int philosopherId, Chopstick rightChopstick, Chopstick leftChopstick)
        {
            PhilosopherId = philosopherId;
            RightChopstick = rightChopstick;
            LeftChopstick = leftChopstick;
        }

        public void Think()
        {
            Console.WriteLine($"Philosopher({PhilosopherId}) is thinking...");
            Thread.Sleep(TimeSpan.FromMilliseconds(1000));
        }

        public void Eat()
        {
            while (RightChopstick.InUse)
            {
                Think();
            }
            
            RightChopstick.Use(PhilosopherId);

            while (LeftChopstick.InUse)
            {
                Think();
            }

            LeftChopstick.Use(PhilosopherId);

            Console.WriteLine($"Philosopher({PhilosopherId}) is eating...");

            Thread.Sleep(TimeSpan.FromMilliseconds(1000));

            RightChopstick.ReturnToTable();
            LeftChopstick.ReturnToTable();
        }
    }

    public class Chopstick
    {
        public int ChopstickId { get; }

        public bool InUse => UserId.HasValue;

        private int? UserId { get; set; }

        public Chopstick(int chopstickId)
        {
            ChopstickId = chopstickId;
        }

        public void Use(int userId)
        {
            UserId = userId;
        }

        public void ReturnToTable()
        {
            UserId = null;
        }
    }
}

2. の回答

  1. を達成すべく解法はいくつかあるみたいですが、C# での Monitor とほぼ同等の lock 構文よって箸を持って食事が終わるまで他の哲学者は待つようにしてみました。このように dead lock が生じない特性の事を dead lock free と呼びます。
namespace DiningPhilosophers
{
    class Program
    {
        private static object LockObject { get; } = new object();

        static void Main(string[] args)
        {
            // 1. で記載ある部分は省略

            var threads = philosophers.Select(x => new Thread(() =>
            {
                while (true)
                {
                    try
                    {
                        x.Think();
                        // Eat 中は他の哲学者をブロックする
                        lock (LockObject)
                        {
                            x.Eat();
                        }
                    }
                    catch (ThreadInterruptedException)
                    {
                        Console.WriteLine("thread interrupted.");
                        break;
                    }
                }
            })).ToArray();

            // 1. で記載ある部分は省略
        }
    }

    // Philosopher と Chopstick は変更がないので省略
}

こちらを実行すると哲学者は1本目を持つ前に他の哲学者が食事をしていたら待つ状態になり dead lock は発生しなくなります。一方で、タイミング次第ですが食事ができる哲学者が偏る問題が発生します。例えばある哲学者Aが食事をしている間に哲学者Bが待っていて、Aが食事が終わった後にすぐにCが食事をはじめ、それも完了後にDやAが食事を始めてしまうみたいな状況が繰り返されるとBはなかなか食事にありつけない状態になります。このように処理に必要なリソース状態は食事できない事に引っ掛けていわゆる starvation と呼ばれます。

3. の回答

  1. への解法ですが、食事回数を見て均等に優先的に食事が可能なように食事回数が少ない場合に優先順位を振り分けてやります。
namespace DiningPhilosophers
{
    class Program
    {
        private static object LockObject { get; } = new object();

        static void Main(string[] args)
        {
            var chopsticks = Enumerable.Range(0, 5).Select(x => new Chopstick(x)).ToList();
            var philosophers = new List<Philosopher>();

            for (var i = 0; i < chopsticks.Count; i++)
            {
                var philosopher = i == (chopsticks.Count - 1)
                    ? new Philosopher(i, chopsticks[i], chopsticks[0])
                    : new Philosopher(i, chopsticks[i], chopsticks[i + 1]);
                philosophers.Add(philosopher);
            }

            var observer = new Observer(philosophers);

            // Task は ThreadPool の状態に依存して必ず別 Thread での実行である事を保証しないので今回は 生 Thread を使う
            var threads = philosophers.Select(x => new Thread(() =>
            {
                while (true)
                {
                    try
                    {
                        x.Think();

                        // observer が食事回数を監視して食ってない哲学者を優先する
                        if (!observer.CanPhilosopherEat(x.PhilosopherId)) continue;
                        
                        lock (LockObject)
                        {
                            x.Eat();
                        }

                    }
                    catch (ThreadInterruptedException)
                    {
                        Console.WriteLine("thread interrupted.");
                        break;
                    }
                }
            })).ToArray();

            foreach (var thread in threads)
            {
                thread.Start();
            }

            Thread.Sleep(10000);

            foreach (var thread in threads)
            {
                thread.Interrupt();
            }

            foreach (var philosopher in philosophers)
            {
                Console.WriteLine($"Philosopher({philosopher.PhilosopherId}) {philosopher.EatCount} times eating.");
            }
        }
    }

    public class Observer
    {
        private IDictionary<int, Philosopher> Philosophers { get; }

        public Observer(IEnumerable<Philosopher> philosophers)
        {
            Philosophers = philosophers.ToDictionary(x => x.PhilosopherId, y => y);
        }

        public bool CanPhilosopherEat(int philosopherId)
        {
            var philosopherEatCount = Philosophers[philosopherId].EatCount;

            var minimumPhilosopherEatCount = Philosophers.Values.Min(x => x.EatCount);

            return philosopherEatCount <= minimumPhilosopherEatCount;
        }
    }

    // 変更分だけ記載
    public class Philosopher
    {
        // 食事回数を追加
        public int EatCount { get; private set; } = 0;

        public void Eat()
        {
            while (RightChopstick.InUse)
            {
                Think();
            }

            RightChopstick.Use(PhilosopherId);

            while (LeftChopstick.InUse)
            {
                Think();
            }

            LeftChopstick.Use(PhilosopherId);

            Console.WriteLine($"Philosopher({PhilosopherId}) is eating...");
            Thread.Sleep(TimeSpan.FromMilliseconds(100));

            EatCount +=1;

            RightChopstick.ReturnToTable();
            LeftChopstick.ReturnToTable();
        }
    }

これで食事の回数が大体均等になるような結果がでるようになります。このように starvation が発生しない状況を starvation free といいます。starvation は deadlock によっても引き起こされる現象です。そのため starvation free は deadlock free も内包する性質であるとも言えます。排他制御はこのような deadlock や starvation を気にしながら実装しないと問題を引き起こすはめになります。

所感

実際このような排他制御は複雑でデバッグも面倒なので、業務コードでは可能な限りしないように設計するのがベターなのであまり本格的に取り組んだ事がなかったのですが、去年ちょっとだけ扱う必要があり、少しだけ勉強したのを思い出したのと本を同僚から教えてもらってせっかく買ったので、年末はこの辺の内容を覚えていこうかなと思ってます。

参考

ja.wikipedia.org

.NET のスレッド同期に利用する基本的な処理(同期プリミティブ)について調べてみた

これは Sansan Advent Calendar 2019 の23日目の記事です。 adventar.org

最近 Qiita で書いてたけどはるか昔に自前でブログ作ってたの思い出したのでこちらに記載してみます。

先日業務でマルチスレッドで共有リソースを触るような処理を検討する必要があったのですが、スレッド同期とかやった事なかったので知識が中途半端だったので一回ちゃんと理解したいと感じたので調べてみました。 調べただけなのでちょっと内容的に薄いかもしれません。(チームの同僚から濃いの希望とか言われてたが勘弁...)

スレッドの同期はすべきではない

まず前提としてスレッドの同期は複数のスレッドが共有データに同時にアクセスする時にデータの破壊を防ぐために使われます。 しかしスレッドの同期は以下の理由から、そもそも可能ならやるべきではありません。

  1. 複雑になりミスを誘発しやすい 複数のスレッドから扱われるデータを全部把握しておく必要があります。 それらのデータを扱う処理をロック獲得と開放の処理で囲む必要がありますが、囲み方を間違えるだけでデータの破壊に繋がります。 またロック処理を正しく実装できている保証をする事は難しいです。タイミングの問題になるので、普通にテストしているだけだと再現しないケースも多く存在します。

  2. パフォーマンスの劣化に繋がる ロックの獲得と開放は、どのスレッドが最初にロックを取得するのか決定するために協調するためにCPU間の通信を行う必要があります。 この通信がパフォーマンスに悪影響を及ぼします。スレッドプールが獲得できないロックを獲得しようとすると新しいスレッドを作成しようとします。 スレッドの作成自体がメモリとパフォーマンス上高コストな処理です。またブロックされたスレッドが実行を再開する時にスレッドプールスレッドも一緒に動作します。 そのためCPUよりも多くのスレッドをスケジュールするようになりコンテキストスイッチが多発してパフォーマンスの劣化に繋がる可能性があります。

同期プリミティブ(Synchronization Primitives)

これらの前提がありながらもケースによってはスレッド同期の実装が必要になってくるケースがあるかと思います。 その場合に認識しておかなければならないのが同期に関する機能がどう動くのかという事です。これらの機能は同期プリミティブによって提供されます。 同期プリミティブとは並列コンピューティングの世界に置いて、クリティカルセクションで競合状態を起こす危険があるコンピューティングリソースを直列にするため利用できる、最も基本的なメカニズムの事を表します。(これは言語に関わらないコンピュータサイエンス用語) .NET の同期プリミティブにはユーザモードとカーネルモードの2種類が存在します。

  • ユーザモード
    • ユーザモードの方がスレッド協調のための特別なCPU命令を使用するのでカーネルモードよりもはるかに高速に動作します。
      • これは協調がハードウェアの内部で発生する事をさします。
      • ただしこれはOSがユーザモードの同期プリミティブでスレッドがブロックされた事を検出できない事も意味します。
        • ユーザモードの同期プリミティブでブロックされたスレッドプールスレッドはブロックされたとOSからはみなされないので、スレッドプールはブロックされているスレッドを置き換えるための新しいスレッドを生成しません。
        • スレッドの生成はメモリやパフォーマンス上高コストなのでこの方が望ましいです。
    • 一方でユーザモードで動作中のスレッドはシステムによって横取りされる可能性があります。(可能な限り高速に再スケジュールされるが)
      • これは何らかのリソースを獲得したいけど、できないスレッドはユーザーモードでスピン(可能になるまでループ)してCPUを無駄に大量浪費してしまう可能性があります。
  • カーネルモード
    • カーネルモードの同期プリミティブはOSによって提供されます。なので利用にはアプリケーションからOSのカーネルに実装されてる関数を呼ぶ必要があります。
    • スレッドが別のスレッドが保持するリソースを獲得するためにカーネルモードの同期プリミティブを使用するとOSはCPUを浪費しないようにスレッドをブロックし、スレッドがリソースにアクセス可能になったらOSはスレッドを再開させます。

基本的にはユーザモードの方が高速で動作するので、こちらのものを利用すべきです。 ただし保持している同期を開放しないとスレッドが永久にブロックされるようなケースでは以下の通りになります。 ユーザモードはスレッドをCPU上で永遠に動作させ続けることにならいライブロック状態にになります。この場合CPUとメモリの両方を浪費します。 カーネルモードの場合はスレッドが永久にブロックされるとデッドロック状態になり、無駄なスレッドの割り当てだけになるのでメモリのみ浪費になり、こちらの方がまだマシになります。

ユーザーモードの同期プリミティブ

.NET では以下のデータ型への読み書きが atomic であることを保証しています。 - Boolean - Char - Byte - Sbyte - Int16 - Int32 - IntPtr - UInt16 - UInt32 - UintPtr - Single - 参照型

一方 Int64 などの読み書きが atomic である事が保証されてない型では複数のスレッドで同時に行うと値が変わる可能性があります。

long x = 0x0123456789abcdef;

この値は別スレッドから読み取ると 0x0123456700000000 または 0x0000000089abcdef で取得される場合があります。 これは分裂読み取り(torn read)と呼ばれます。コンパイラやCPUの最適化のために読み取り、書き込みがいつ行われるかの保証はありません。 これらの Int64, UInt64 や Double に atomic な読み書きを強制するのが以下二つの同期コンストラクトになります。

  • Volatile
    • 単純なデータ型への atomic な読み取り または 書き込みを実行する
  • Interlocked
    • 単純なデータ型への atomic な読み取り 及び 書き込みを実行する

Volatile

.NETはコンパイラがILに変換し、JITコンパイラが実行時にそれをネイティブのCPU命令にしてCPUがそれを実行されます。 この変換処理毎に最適化が働くようになっています。そのため以下のコードが想定通りに動かない可能性があります。 例えば以下のケースではコンパイラ_stopWorker が true/false のいずれかと認識し Worker メソッド内で変更されないと認識してしまいます。 そのためコンパイラは最初に _stopWorker をメソッドの最初にチェックするコードを生成する可能性がります。 (特にビルド時にx86でoptimizeスイッチをonにした場合はx64 よりも成熟した最適化が走るので積極的に最適化を行おうとする傾向にあるそうです。)

class static StrangeBehavior 
{
    private static bool _stopWorker = false;

    public static void Main()
    {
        var thread = new Thread(Worker);

        thread.Start();

        Thread.Sleep(500);

        _stopWorker = true;

        thread.Join();
    }

    private static void Worker()
    {
        var x = 0;
        while(!_stopWorker)
        {
            x++;
        }
    }
}

このコードを修正するには volatile を使います。

private static volatile bool _stopWorker = false;

volatile をfieldの宣言に付けることで最適化の対象とされなくなります。 しかし volatile で宣言された field の値を参照渡しすることをサポートしてない上に、最適化が外れるのでパフォーマンスが悪化する可能性があります。 volatile は System.Threading.Volatile の Read/Write を共有リソースの読み書き時に利用する事で同様の事ができます。

Interlocked

Volatile は Read と Write によって atomic な読み取りか書き込みを提供してました。 一方で System.Threading.Interlocked は読み書きを atomic な操作として提供します。 通常コンピュータ上で int の increment/decrement は以下の3つの操作となります。 1. インスタンス変数からレジスタに値をロードする 2. 値を increment/decrement する 3. 値をインスタンスに保存する これを Interlocked.Increment/Decrement を利用しない場合は以下のことが発生する可能性があります。 最初のスレッドが 1. 2. を実行 => 別スレッドが1.2.3.の全部を実行 => 最初のスレッドが操作を再開し値を上書き => 別スレッドの操作は無かった事に。 Interlocked のメソッドはフルメモリフェンスです。つまり Interlockedのメソッドの呼び出しより前の変数の書き込みは Interlocked のメソッドよりも前に行われ、 Interlocked のメソッドより後の変数の読み取りは Interlocked のメソッドよりも後に実行される事は保証されます。

カーネルモードの同期プリミティブ

基本的にカーネルモードの同期プリミティブはOS自身の協調が必要になるためユーザモードのものより低速で動作します。 またカーネルのオブジェクトに対する関数呼び出しがマネージコードからネイティブのユーザモードコード、カーネルモードコードの遷移、そして終了時には逆の遷移を引き起こします。これはCPUを大量に消費する高コストな操作なのでパフォーマンスの悪化を招く場合があります。 ただしカーネルモードの同期プリミティブには以下のメリットがあります。 - リソースの競合が発生した場合にOSが敗者のスレッドがスピンしてCPUを浪費しないようにブロックする - ネイティブスレッドとマネージスレッドを互いに同期できる - 同じマシン上の異なるプロセスで実行中のスレッドを同期できる - アクセス権のないアカウントからのアクセスを防ぐセキュリティ設定が適用できる - スレッドを全ての、またはいずれかのカーネルモード同期プリミティブが利用可能に設定されるまでブロックできる - タイムアウト値が設定でき、スレッドをブロックできる。スレッドが一定時間リソースにアクセスできない場合ブロックを解除して別タスクを実行できる

カーネルモードの同期プリミティブには大きく分けて二つあり以下の二種類が存在します (Mutexはこれら二つの上で実装なので分類からは除外) - Event - Semaphore

これらは WaitHandle と呼ばれるOSのカーネルオブジェクトをラップしただけのシンプルな抽象クラスを継承しています。 この継承階層と概要は以下の通りとなります。 - WaitHadle - Mutex - プロセス間の同期に利用できる。アクセスを許可できるスレッドは1つのみ。 - Semaphore - リソースやリソースプールへアクセスできるスレッド数を制限できる。 - EventWaitHandle - スレッドの同期イベント。シグナルによって別スレッドに lock 可能になった状態を伝える。ローカルなイベントだけでなくプロセスを跨がる名前付きシステムイベントを扱える。 - AutoResetEvent - シグナルを送られた場合に自動でスレッドをブロック解除する同期イベント - ManualResetEvent - シグナルを送られてもスレッドを自動でブロック解除せず、手動でする必要がある同期イベント

Event

Event は簡単に言えばカーネルで維持される bool 変数で、 Event を待機するスレッドは Event が false になるとブロックされ、 true になるとブロックが解除されます。 Event には二種類あり、AutoResetEvent は true になる時カーネルは待機中の最初のスレッドをブロックを解除した後自動で false になり、ブロックされたスレッドは1つだけ起動されます。 ManualResetEvent は true になる時、自動的にリセットされないので待機中の全てのスレッドがブロック解除されます。この時コードで手動で Event を false に解除する必要があります。

Semaphore

Semaphore は簡単に言えばカーネルで維持される int32 の変数で、Semaphore を待機するスレッドはSemaphore が 0 の時ブロックされ、Semaphore が 0 よりも大きい場合に、ブロックが解除されます。 Semaphore を待機するスレッドのブロックが解除される時、Semaphore は自動的にカウントを 1 減らします。 AutoResetEvent と 最大値が 1 の Semaphore はほぼ同じ動きをしますが、違いとして AutoResetEvent は複数回 Set を読んでも 1 スレッドしかブロックを解除しませんが Semaphore は Release を複数回呼ぶと内部カウントを増加させ続けてしまい複数のスレッドのブロックを解除する事になります。

Mutex

Mutex は相互排他ロックを表現します。これは AutoResetEvent と似た動きをし、一度に待機中のスレッドの一つを開放します。

所感

実際に同期処理にこれらの同期プリミティブを利用する機会は実はそれほど多くはないかと思います。(Interlockedとかはよく見る気がするけど。) しかし実務においてはこれらの組みあわせによってできた同期処理群(Monitorや SemaphoreSlim, ReaderWriterLockSlim など)の利用はあるかと思います。 それらの挙動を理解するためにも一度は学習しておいた方がいいのでは無いでしょうか。

参考情報

第四回Ques 参加してきました

第四回Ques に参加してきました。

f:id:ea54595:20140422232305p:plain

Quesとは

会社の業務で遅刻したため前半の機械学習の自動化についてはほぼ終わり際のみですが。。
Mobageの自動化周りに話は面白かったです。
特にスマートフォン周りは業務で経験が無いため非常に参考になりました。

では以下メモ。

第一部.機械学習実装のテスト自動化

株式会社ALBERT システム開発部・コンサルティングシステム開発チーム
小宮 篤史さん

※途中から参加したためこれより前の話は不明....

機械学習のテストはホワイトボックステストを行う。

分岐だけでなく数値計算に気をつける。 例えばこういった計算。

doubel v;
v = 1 / 0.0; // Infinity
v = 0.0 / 0; // Nan
v = 1 + 0.000_000_000_000_000_09; //1.0
Math.exp(800); // Infinity
Math.log(0); //Negative infinity
Math.log(-1); //Nan

機械学習のテストは業務システムとは異なる知識を求められる。
QAの知識を十分には活用することはできる。

精度を求めるにはアルゴリズムの分析レベルではロバストかどうかは求める。

Q&A

確実なベースラインを作るのは難しいのでは?

ベースラインではパーセプトロン分析を使う。
パーセプトロンが正確なものかの検証をキチンと分析のフェーズでする。
テスト自動化はその後のフェーズ。

どこまで機械学習でできるの?

スパムか正解メールかの分類は人間が行うしかない。(アカデミックでは研究されてるかも)
それと同じ結果になるかをテストする。

統計サイドのエンジニアが意識すべき観点は?

関数の帰ってくる値を認識して欲しい。上記のMath.expなど。
あとはパフォーマンスを意識して欲しい。


第二部.Mobageオープンプラットフォームでのテスト自動化

DeNA システム本部 品質管理部 SWETグループ
中川 勝樹さん
twitter @ikasam_a
github github@masaki

Mobageオープンプラットフォームとは

海外にもプラットフォームを展開。

SWETって何?

DeNAのテスト専属チームのこと。
当初3人から20人へ拡大中。

立ち上げ背景

  • オープンラットフォームのグローバル展開
  • 大規模システムの拡張とリファクタリング
  • デリバリーのスピードを落とせない
  • 検証の属人性の解消

➡ テスト専門チームの立ち上げ

Mission
  • End to End テストを確率する
  • テストを徹底的にに自動化する
  • テストしやすい環境を提供する

    • テストしやすい環境とは?

      • 単体テストのREDが消えない問題 QCDの優先順位でREDのまま
      • リリーススピード
      • CI環境の整備
何故独立チームなのか?
  • 横串チームによる戦略的な他プロダクトへのテストノウハウを横展開するため

そもそもSWETってどういう意味?

Software Engneer In Test
Missionは Quality Assurance

他の会社の読み方違うけど役割は同じ

company Team Name
Google SET Software Engneer In Test
MicroSost SDET Software Development/Design in Engneer Test
DeNA SWET SoftWare Engneer In Test
SETの役割
TE(Test Engneer)の役割
  • テストファースト
  • テストを自動化していく
  • テスト結果に関わる
  • テスト実行をドライブする

TEと違いSETは開発者にフォーカスしている

  • 個々の質を上げる
  • 開発者のテストをしやすくるする
Developer Productivity

技術基盤など開発の生産性向上を行う

SWET = SETの役割 + TEの役割

SETはテストに絞ったDeveloper Productivity向上のためのチーム
テスト対象の開発もできるぐらい技術と仕様を理解できる

プラットフォームテスト自動化戦略

対象領域 1. WebAPI 2. Web Application 3. Mobile Web 4. Client SDK

意識していること

  • 適切なシステム分割をする
    • システムビッグバンを避ける
  • サーバで完結できるところはサーバで
    • WebAPIの機能テストはクライアント無しで
    • ブラウザを使う場合は実記で

スマートフォンテスト自動化

テスト対象によってテスト方法も異なる

1.WebAPI, Webアプリ

ブラウザの自動化技術をそのまま使う

Capabilityでドライバを選択 ブラウザによってDesiredCapabilityを選択

DesiredCapabilities capability=DesiredCapabilities.internetExplorer();
capability.setCapability(
              InternetExplorerDriver.INTRODUCE_FLAKINESS_BY_
IGNORING_SECURITY_DOMAINS, true);
WebDriver webdriver = new InternetExplorerDriver(capability);
2.ネイティブアプリ、SDK
  • アプリの操作を自動化する
  • 外部からプロセスにアタッチして操作
  • テスト用ライブラリを埋め込んで操作
    • Calabash
    • 自分たちでコードを弄れる場合のみ
    • ただしアプリ改変になるため一部にするなど考える必要がある

最近はAppiumを使って直接プロダクトコードには手を加えない

SDKでは
  • ライブラリを組み込んだテストアプリを作成
  • ライブラリの機能を網羅できるようにつくる
  • テストアプリの操作を自動化
  • あとはネイティブアプリと同様

ブラウザテストはRubyでやっているのでそれを流用している

httpのアサーションなどブラウザ単体ではできない部分のため
セッション情報を渡して途中からMechanizeに切り替えてテストをしている

Q&A

プロダクトの改修などとの関わり方は?

SWETチームから各プロダクトチームに1名派遣されている SWET同士で情報交換をしている

テストだけだとモチベートしにくいのでは?

あくまでもプロダクトを出す事が目的。関わり方が違うだけと話している。

テストコードのメンテナンスはどうしている?特にGUIは変更が多いが?

テストに強い変更をする。 テスタビリティの高い設計をメンバーにインプットしている。 設計レビュー、コードレビューもしている。

Appiumが動かないAndroidはどうする?

Selendroidを組み合わせて使う。書き方が変わる部分はフレームワークを開発して対処。

Rubyでテストしている理由は?

自分が得意なのとPerlSeleniumの公式ドライバが無いから。

Lean UXが拓く最適なデザイン メモ

以下先日のセミナーのメモ書きです。自身の備忘録用に残しておきます。

Lean Startupの実践と難しさについて

  • 工藤博樹さん
  • Lean Startupのワークショップ啓蒙団体

    どうやってやってるか

  • Javalin Boradを利用して、やりたい事が刺さる顧客を考える。以下の要素をできるだけ具体的に埋めていく。例えば顧客は学生でもどういった学生なのかなど。
  • 考えるのは顧客は誰か?
  • 課題は何か?
  • 前提は何か?
  • 判断基準は何か?

  • ここで実際に外に出て顧客になりそうな人にインタビューして検証する。
    「もっと顧客を細分化して考えるべき」「海外では受けたけど日本では違った。。」など。このプロセスを繰り返して「顧客は誰か」「どんな課題があるのか」を明確化していく。

MeryBizでのLean Startupの実践

  • MeryBizとは?

    • 中小企業向けの経理のアウトソーシングサービス。
    • レシートを送ってもらってそれをデータにして返すサービスを行っている。
  • 独立した当初は何をやるか決まってなかったが、もともと自分では事務作業に痛みを感じていた。

  • そこで「これは課題じゃないか」と認識して事務作業のアウトソーシングサービスを始めた。
  • 最初は経理も予約など事務作業を何でもやろうとして、サービスがぼやけてしまい失敗した。
  • Webのページも経理系のサービスだったので固いイメージを持っていたのでそれに反して柔らかいイメージにしていた。しかし顧客が経理に求めているのは「安心」だった。なので固めのページに修正した。仮説のずれを検証して修正のサイクルをまわした。

LeanUXのサイクル

  1. 前提の宣言
  2. MVPの作成
  3. 実験の実行
  4. フィードバックとリサーチ

実際に難しかった事

  1. LeanUXをメンバーの共通言語にすること。
  2. メンバーが物理的に離れていたので認識が食い違う事が多かったこと。

LeanUX Designing Calture 組織文化をデザインする

IT is a rapidly changing business

  • IT業界の変化は早い。 流れが速いからこそ、流行に惑わされずに、本質を見極める必要がある。
    米Yahooのメリッサマイヤーがそれを実践できていると考えている。
    「We need to be one Yahoo! and that starts whith physically being together!」
    在宅勤務をやめさせて物理的な距離を近くした。
    これによって人の意見を交換させて新しいアイデアに結びつけれるようにした。
  • 企業の本質は「文化」である。

Lean UX Mind Set

  1. 「どのようにつくるか」ではなく「どのようなもの」を作るか。
  2. サービスやものづくりにおいて手段が目的化してしまう。
  3. 無駄をなくす事が目的ではない。結果的に無駄が無くなる。
  4. Build Mejar Run だがBuildからでは無く逆から考える。
  5. 「問題解決」よりも「問題の発見と定義」に重きをおく。

  6. 正しい問題を解決しているのかを重視する。

    • 例えば医療系の会社で風薬を作った場合だと以下の事を考えているか?
      • 「この薬が解決する症状ってなに?」
      • 「その解決を望んでいるどこにいる?」
  7. 「足の引っ張り合い」から「助け合い」文化の醸成

  8. Transparent「透明性」 

  9. Demystify「打ち明ける事」できること困っている事をお互いにうちあける
  10. Cross-functional Collaboration チームから独立して働く贅沢は許されない。

Lean UX is an Engine of Learning

    • Lean UXは組織の学びのエンジンである。学びは二種類ある。
  1. ユーザからの学び

  2. メンバーからの学び

  3. 前提を明らかにする段階でお互いの認識を理解して学べる。
    お互いの職能が異なると視点も異なるが、それをお互いに学び合う。

    C-P-S hypothesis

  4. Problem(課題)
  5. Custamer(顧客)
  6. Solution(解決法)

まずこの3点について仮説を埋める。
埋めれなかったら理解が足らないかもしれないので学びにいく。
実践してみて仮説と異なったら、どの軸かをピボットする。

No more of this

  • 考えていたものと違うものができあがってしまったという状況はもう見たくない。
  • もっとコミュニケーションを綿密にする必要がある。
  • 作る工程はハッカソンの方が一般企業より上だと感じた。10時間でアプリのプロトタイプが作成できた。こういったハッカソンがLeanUXを学ぶにはいいと思う。 組織図に惑わされないで本来のものづくり、事作りに集中する。

すばらしいUXはどこから生まれる?

松井田彰さん

  • 教育ベンチャーednity UXデザイナー
  • ednity 学習環境のリデザインを掲げる = education + community

    LeanUXの実践

    5名のスタートアップでるednityでは下記のような流れで行っている。

  • デザイン会議

  • どのようなシーンで使われる?どのような体験がふさわしいか?
  • ペーパープロトタイピング
  • ユーザービリティ確認。すぐにできるのがメリット。
  • デザイナー、エンジニアはそれぞれデザインとモックを作成。
  • 各作成物を合体させる

LeanUXから学んだこと

メンバー全員で机を並べて協力してやった。
ペーパープロトタイピングで素早いフィードバックができた。

  1. 思考プロセスの共有 実際にあると解り易い
  2. 多くの創発性 アイデアが色々でてくる
  3. チーム全員が責任をもつ チームがUXに対して責任を持てる
  4. プロダクトの質の向上 メンバーで遣る事で理想とのギャップが減った
  5. 無駄な中間制作物の排除 ワイヤーフレームとかカンプとかもう使わない。

場をつくる

会議の場に以下のような事をしてリラックスして話やすい環境を作る。

創造的なチームからUXは生まれる

そのために各メンバーは適切なふるまいをする必要がある

LeanUXで変わる group discussion

  • LeanUXを導入しようとしてもコンフリクトが起きるけど?

    • 非デザイナーにデザイン思考を共有できるような環境、意見が生まれ易い環境をつくっていく。
    • ファシリテーションが必要。
  • ファシリテーションでの工夫は?

    • PMが指示してやらせて確認だと上手く行かなかった。。デザイナーに権限を渡した。他のメンバーが自発的に動けるようにつんつんする」
    • メンバーの意見を言い易くする。➡「スクラムと同じ?」
  • 大きいチームから小さいチームでの変わった事は?

    • イデアが出易くなった。廊下にポストイットに成果物やアイデアを張っていくとか。 物理的な雰囲気作りは必要。つんつんする役割の人が必要。
  • UxDの考え方の導入

  • ワークショップ
  • 研修
  • プロジェクトに導入してファシリテートする

    • 目的、企業規模によって異なる。
  • UxDのアプローチで難しいのは投資価値をどうやって認識してもらうか。

    • ユーザー中心って本当に意味があるのか?ROIをどうやって出しているのか?
    • まず課題がある。例えば問い合わせページに人がこないなど。それを解決する手段として実践してみて早くから効果がでた。
  • LeanUXは文化だと思っているのでROIは違和感がある。後から理屈付けはできるけど。。

    • でも組織にいると導入に説明が必要。
    • チームなどで実験的にやってみて実際やってみての成果(コミュニケーションコスト削減、無駄な成果物など)を伝えてみる。
    • 目的ではないけど、結果論として無駄が減る。
    • 無駄が無くなった理由はこういった文化がチームにあったからって説明ができる。
    • 部署レベルからやる。
    • BalanceTeamConference で言われているのは
      1. ビジネス決定できる人
    • . デザイナー
    • . エンジニア
      3人チームがバランスとれる。わざわざ3人のチームにするのは意思決定を早くし、リスクを取れるチームを作るため。3人は握手三回。4人だと六回。つまり4人目以降はコミュニケーションコストが増える。
  • 文化っていってるけどとは?
    • 企業における生活そのもの。そこの見直し。
    • 効率性の維持によって失ったものがある事に気づく。
  • Remoteでのチームビルディングって難しいのでは?
    • 興味の周りに人が集まるもの。なのでお互いに興味を持つ。「デザイナーが何やっているのか?」「エンジニアが何やっているのか?」お互いに興味を持っていれば距離は関係ないものになるのでは。

LeanUX刊行記念セミナーにいってきました。

Lean UX ―リーン思考によるユーザエクスペリエンス・デザイン (THE LEAN SERIES)

Lean UX ―リーン思考によるユーザエクスペリエンス・デザイン (THE LEAN SERIES)

先日、六本木のGreeセミナールームにて行われたLeanUX刊行記念セミナーに行ってきました。
もともと本業ではBtoBのシステム開発を行っており、あまりUXが重視されると実感できる仕事ではないのですが、これからもそうだとは限らない(SIer自体の市場が縮小しておりビジネスモデルの変換が求められていたり、自身のキャリアがずっと同じとも限らない)ため一個人の開発者としては、認識にいれておくべき概念だと思い参加してきました。

LeanUXとは

Lean UX とはその名のとおり、Lean Startupのコンセプトを従来のユーザエクスペリエンスデザインに取り入れ、アジャイルなチームマネージメントをも可能にする手法体系です。 Lean UXーリーン思考によるユーザエクスペリエンス・デザイン

1.新しい手法や考え方の導入について

LeanUXについては単語だけ聞いた事があるレベルで参加してきた思ったのですが、 こういった明確なROIを示す事が難しい手法を組織に導入する際の話が一番印象に残りました。

監訳者の坂田さんがおっしゃるには「LeanUXは文化である。文化にROIとかはおかしい」という話でしたが、実際文化でも周りに納得してやるためには効果を示さなければなりません。

LeanUXは無駄を目指したものではないが結果として無駄が無くなる事で周りを説得する材料にできるという事が語られていました。

つまりどこかのチームレベルで実践してみて、その効能を見せて説得すればいいという話です。 これはアジャイルやTDDなどの開発手法の導入にも応用できる話であり参考になりました。

2.顧客に対して考え方をフォーカスする

もうひとつ話を聞いていて印象にのこったポイントは顧客について考える姿勢です。
1. 顧客は誰か? 2. その顧客が持つ課題は何か? この二点にフォーカスする事の大切さが語られていました。これは先日聞いたマーケティングの考え方でも主流な考え方でもあります。

現在BtoBのシステム開発に携わっていますが、顧客が遠いケース(ex..システム部門➡実際使うエンドユーザのように実際使うユーザとは直接のやりとりを行わない)が多いので、この視点が薄くなってしまいがちです。
なのでは常日頃から意識すべき点のように感じました。どのように意識すべきかというとシステムの要件を定義する際にその利用者が誰で、どのように使用するのか、またそのシステムを使う事で直接顧客企業の更にまた顧客にどんなバリューを提供てきるのかといった背景を念頭に入れている必要があると思います。

以上セミナーで考えさせられた事でした。
まだLeanUXについては本を会場で買ったばかりなのでこれから学んでいこうと思います。

Capistrano3でrailsをdeployしてみる

Capistrano3でrailsをdeployしてみる

CapistranoオープンソースRuby製ソフトウェアデプロイメントツールです。
複数のサーバへのソフトウェアのデプロイメントを自動化する事ができます。 railsを対象によく用いられますが、railsなどのフレームワークや言語に限らずデプロイする事ができます。
現在最新版は3.0.1となります。(2014/1/12現在)
なおCapistranoは2から3への変更時にパラメータの持ち方などが変わり互換性がなくなっています。今回は3以降の記述で行います。
公式ページ

今回の環境
CentOS6.4
Ruby 2.1
rbenv

1.インストール

gemを使ってcapistranoをインストールします。 rootでない場合はsudoも付けてください。
railsじゃない場合はcapistrano-railsは必要ありません。

gem install capistrano
gem install capistrano-rails
gem install capistrano-rbenv #またはapistrano-rvm

bundlerを使っている場合は下記の記述をGemfileに追記してbundler installを実行してください。

gem 'capistrano', '~> 3.0.1'
gem 'capistrano-rails'
gem 'capistrano-bundler'

2.設定ファイル作成

設定ファイルを作成します。railsのルートディレクトリで下記コマンドを実行してください。

cap install

これで下記のファイルが作成されます。

railsルート
├─  Capfile
├─  config
│ ├─  deploy
│ │ ├─production.rb
│ │ └─staging.rb
│ └─deploy.rb
└─  lib
    └─capistrano
        └─tasks

3.Capfile記述

まずCapfileを記述します。
初期ではコメントアウトがあるので必要なものをコメントアウトから外します。

# Load DSL and Setup Up Stages
require 'capistrano/setup'

# Includes default deployment tasks
require 'capistrano/deploy'

# Includes tasks from other gems included in your Gemfile
#
# For documentation on these, see for example:
#
#   https://github.com/capistrano/rvm
#   https://github.com/capistrano/rbenv
#   https://github.com/capistrano/chruby
#   https://github.com/capistrano/bundler
#   https://github.com/capistrano/rails/tree/master/assets
#   https://github.com/capistrano/rails/tree/master/migrations
#
# require 'capistrano/rvm' #rvmならこちらを外す
require 'capistrano/rbenv' #rbenvならこちらを外す
set :rbenv_type, :system # or :system, depends on your rbenv setup
set :rbenv_ruby, '2.1.0' #rubyのバージョンを指定
# require 'capistrano/chruby'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'

# Loads custom tasks from `lib/capistrano/tasks' if you have any defined.
Dir.glob('lib/capistrano/tasks/*.cap').each { |r| import r }

4.deploy/{環境}.rb記述

deployフォルダ配下にproduction.rbやstagingなどが初期ではあります。
追加したければdevelopment.rbなどを追加します。

set :stage, :production #環境名
#各サーバの役割を記述
role :app, %w{deploy@example.com} #アプリケーションサーバ
role :web, %w{deploy@example.com} #webサーバ
role :db,  %w{deploy@example.com} #DBサーバ
#サーバ情報記述
server 'example.com', #サーバ名
user: 'deploy', #実行ユーザ
roles: %w{web app db}, # サーバの役割
ssh_options: {
    keys: %w(~/.ssh/xxxxx/private_key)
    auth_methods: %w(publickey) # 認証方法 passwordも可能
    #password: 'xxxxx' #password指定
}

4.外部ファイルにタスクを記述する

deploy.rbの外部ファイルにタスクを記述する事ができます。
lib/capistrano/tasks配下にXXXX.capファイルを作成します。 今回はunicornの起動タスクunicorn.rbとして記述します。

#unicorn.rb
namespace :unicorn do
  task :environment do
    set :unicorn_pid, "#{shared_path}/tmp/pids/unicorn.pid"
    set :unicorn_config, "#{current_path}/config/unicorn/#{fetch(:rails_env)}/unicorn.rb"
  end

  def start_unicorn
    within current_path do
      execute :bundle, :exec, :unicorn_rails, "-c #{fetch(:unicorn_config)} -E #{fetch(:rails_env)} -p 8080 -D"
    end
  end

  def stop_unicorn
    execute :kill, "-s QUIT $(< #{fetch(:unicorn_pid)})"
  end

  def reload_unicorn
    execute :kill, "-s USR2 $(< #{fetch(:unicorn_pid)})"
  end

  def force_stop_unicorn
    execute :kill, "$(< #{fetch(:unicorn_pid)})"
  end

  desc "Start unicorn server"
  task :start => :environment do
    on roles(:app) do
      start_unicorn
    end
  end

  desc "Stop unicorn server gracefully"
  task :stop => :environment do
    on roles(:app) do
      stop_unicorn
    end
  end

  desc "Restart unicorn server gracefully"
  task :restart => :environment do
    on roles(:app) do
      if test("[ -f #{fetch(:unicorn_pid)} ]")
        reload_unicorn
      else
        start_unicorn
      end
    end
  end

  desc "Stop unicorn server immediately"
  task :force_stop => :environment do
    on roles(:app) do
      force_stop_unicorn
    end
  end
end

5.deploy.rb記述

deploy.rbにデプロイ時のタスクを記述していきます。
ここでは各環境共通の処理、変数を記述します。 set :対象変数で変数に値を設定します。この値はfetch :対象変数で使用する事ができます。

#アプリケーション名
set :application,'test'
#レポジトリURL
set :repo_url, 'git@bitbucket.org:xxxxxx/test.git'
#対象ブランチ masterに固定
set :branch, 'master'
#デプロイ先ディレクトリ フルパスで指定
set :deploy_to, '/var/www/test'
#バージョン管理方法 subverion, git, mercurial, cvs, bzrなど
set :scm, :git
#情報レベル info or debug
set :log_level, :debug
#sudoに必要 これをtrueにするとssh -tで実行される
set :pty, true
#sharedに入るものを指定
set :linked_dirs, %w{bin log tmp/pids tmp/cache tmp/sockets bundle public/system public/assets}
#capistrano用bundleするのに必要
set :default_env, { path: "/usr/local/rbenv/shims:/usr/local/rbenv/bin:$PATH" }
#5回分のreleasesを保持する
set :keep_releases, 5 

#タスク定義
namespace :deploy do #タスクnamespace
    #desc 'タスク説明'
    #task :restart do #タスク定義 
        #ここにタスク処理内容を記述
    #end
  #after :finishing, 'deploy:cleanup' #task実行タイミングを指定できます。詳細は下記
  #http://capistranorb.com/documentation/getting-started/flow/
    #サンプルにunicorn再起動タスク
    desc 'Restart application'
    task :restart do
      invoke 'unicorn:restart' #lib/capustrano/tasks/unicorn.cap内処理を実行
    end
  after :finishing, 'deploy:cleanup'
end

end

6.実行

実行します。

cap <対象環境名> deploy

これで対象サーバに対してデプロイメントが実行されます。 Capistranoは実行すると下記のような配置がされます。

railsルート
├─current #releases内最新のシンボリックリンク
├─  releases #配下に現在時刻に基づく名前のサブディレクトリが作成され最新コードがチェックアウトされる
└─  shared #リリース間で共用するファイルを置いておく

Linux初心者がさくらVPSでJenkinsを一から構築してみました(後編 Jenkins構築編)

Jenkins下準備

Jenkinsに取りかかる前に下準備としてJavaApacheを入れます。 Javaをインストールします。

yum list \*java-1\*
yum install java-1.7.0-openjdk java-1.7.0-openjdk-devel

Apacheをインストールします。

yum -y install httpd

次にsudo vim /etc/httpd/conf/httpd.confを開き編集します。

#ServerTokens OS
ServerTokens Prod

#KeepAlive Off
KeepAlive On

#ServerAdmin root@localhost
ServerAdmin root@example.com

#ServerName www.example.com:80
ServerName www.example.com:80

#ServerSignature On
ServerSignature Off

TraceEnable Off

apacheを起動し、ついでにサーバ起動時に自動で起動するようにします。

sudo service httpd start #apache起動
sudo chkconfig httpd on #起動時設定

いよいよJenkinsをインストールします。

wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo
rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key
yum install jenkins

vim /etc/sysconfig/jenkinsでjenkinsの設定をします。Jenkinsのポートは初期だと8080です。被らない様に設定しましょう。

JENKINS_PORT="[任意のポート番号]"
JENKINS_ARGS="-prefix=/"

Apacheの設定をします。 vim /etc/httpd/conf/httpd.confで設定ファイルを開きます。末尾に下記を加えます。

<VirtualHost jenkins.[IPアドレス]:80>
    ServerAdmin root@localhost
    ServerName jenkins.[IPアドレス]
    <Location />
        ProxyPass http://localhost:[任意のポート番号]/
        ProxyPassReverse  http://localhost:[任意のポート番号]/
    </Location>
</VirtualHost>

ApacheとJenkinsを起動します。

sudo service jenkins start
sudo service httpd start

これでブラウザからアクセスできます。