wiprog

C# とか,数学とか,社会とか.

async な lock をしよう

Qiita から移行

--

C# では,非同期なメソッドでは lock が使えません.この記事ではそれでも lock したいときはどうするのっていうお話をします.

たとえば,こんなふうにダブルチェックロッキングしたいとしますね.

// これが複数のスレッドから非同期に呼ばれる
private static async ValueTask 条件満たしたらなんかするAsync()
{
    if (条件)
    {
        lock (_lockObj) 
        {
            if (条件)
            {
                await なんかすごく重いIOバウンドなやつAsync();
            }
        }
    }
}

lock ステートメントは非同期メソッド内で使えないので,実際にはこのコードはコンパイルが通りません. じゃあどうしようか,といって,一番ラクな逃げ道は同期にしちゃうことです.カンタンカンタン.

// これが複数のスレッドから非同期に呼ばれる
private static void 条件満たしたらなんかする()
{
    if (条件)
    {
        lock (_lockObj) 
        {
            if (条件)
            {
                なんかすごく重いIOバウンドなやつAsync().Wait();
            }
        }
    }
}

Task だって Wait しちゃえばただの同期,これはちゃんと動きます.でもここで「いやなんのための async なんだよ」ってなりますよね. lock したいだけなのに,そのために非同期の恩恵を捨て去る,そんなことやっちゃダメです.

じゃあ最新技術をこね回して難しいコード書くのかっていうとそんなことはなく,むしろ全く逆で古くからある技術を使います.そう,セマフォ です. セマフォっていうと OS の機能で,プロセス間の資源のアクセス制御に使うイメージですが,.NET にはプロセス内で利用するための SemaphoreSlim クラスがあります. セマフォといっても結局待たなきゃいけないでしょって話なんですが, SemaphoreSlim クラスには非同期で待てる WaitAsync() メソッドがあるわけです. これをつかうと完全に非同期な lock が実現できるわけですね. これはもうパターンが固定なので,ちょっと汎用的に AsyncLock なんていうクラスを作っておくとどこでも使えます. やってることもとてもカンタンなので,作り方さえ覚えてしまえばとっさのときにも書けます.

/// <summary>
/// async な文脈での lock を提供します.
/// Lock 開放のために,必ず処理の完了後に LockAsync が生成した IDisposable を Dispose してください.
/// </summary>
public sealed class AsyncLock
{
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

    public async Task<IDisposable> LockAsync()
    {
        await _semaphore.WaitAsync();
        return new Handler(_semaphore);
    }

    private sealed class Handler : IDisposable
    {
        private readonly SemaphoreSlim _semaphore;
        private bool _disposed = false;

        public Handler(SemaphoreSlim semaphore)
        {
            _semaphore = semaphore;
        }

        public void Dispose()
        {
            if (!_disposed)
            {
                _semaphore.Release();
                _disposed = true;
            }
        }
    }
}

面倒な人はこれをコピペでいいです.使うときは, lock 構文の代わりに using 構文を使います.これによって, semaphore の管理を忘れて lock という意味を持たせた見た目のコードを書けます.

たとえば,最初の例を書いてみるとこんな感じです.

private static readonly s_lock = new AsyncLock();
private static async Task 条件満たしたらなんかするAsync()
{
    if (条件)
    {
        using (await s_lock.LockAsync()) 
        {
            if (条件)
            {
                await なんかすごく重いIOバウンドなやつAsync();
            }
        }
    }
}

lock が using に変わっただけで,あとはあまり変わりません.でもこれはコンパイルも通るし,ちゃんと非同期でパフォーマンスよく動きます.

というわけで,いままで .Wait しちゃってた方,今日からは await しましょう!

Span<T> のつかいみち

Qiita から移行 (2019/12/10 投稿)

--

これは、 C# Advent Calendar 2019 の 10 日目の記事です(遅刻すみません!)。 前の記事は、 @Xeltica さんの C# 用ゲームエンジンを自作した話 です。

.NET Core 2.1 で使えるようになってしばらくたった Span<T> ですが、まだ使えてないよ〜って C#er のみなさんもまだまだいらっしゃると思うので、ぼくが書いたコードを晒していきます。 もっと速くなるよ、とかあれば教えてください!

文字列系

ここでのコツは、 Span<char>#ToString() をうまく使うことと、 stackalloc でスタック領域を活用することです。 ヒープアロケーションを極力避けることで、高速に動作させます。

byte[] -> 16 進数 への変換

byte 配列を 16 進数の文字列に変換する拡張メソッドです。

/// <summary>
/// 16 進数の文字列に変換します
/// </summary>
public static string ToHexString(this byte[] source, bool upperCase = false)
{
    Span<char> buffer = stackalloc char[source.Length * 2];
    source.WriteAsHexChars(buffer, out _, upperCase);
    return buffer.ToString();
}

/// <summary>
/// バイト列を 16 進数の列として書き込みます
/// </summary>
public static void WriteAsHexChars(this ReadOnlySpan<byte> bytes, Span<char> dest, out int charsWritten,
    bool upperCase = false)
{
    charsWritten = 0;
    
    foreach (byte b in bytes)
    {
        b.WriteAsHexChars(dest.Slice(charsWritten), out var count, upperCase);
        charsWritten += count;
    }
}

/// <summary>
/// バイトを 16 進数として書き込みます
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteAsHexChars(this byte b, Span<char> dest, out int charsWritten, bool upperCase = false)
{
    ReadOnlySpan<char> source = upperCase ? s_byteToHexUpper[b] : s_byteToHexLower[b];
    source.CopyTo(dest);
    charsWritten = source.Length; // 2 固定のはず
}

// byte -> char[] に変換するための 配列
private static readonly char[][] s_byteToHexLower =
    Enumerable.Range(0, 256).Select(x => ((byte) x).ToString("x2").ToCharArray()).ToArray();

private static readonly char[][] s_byteToHexUpper =
    Enumerable.Range(0, 256).Select(x => ((byte) x).ToString("X2").ToCharArray()).ToArray();

snake_case -> PascalCase への変換

スネークケースからパスカルケースへの変換をします。これは他の変換 (例: Pascal -> snake) にも応用できます。

public static string SnakeCaseToPascalCase(this string snake)
{
    ReadOnlySpan<char> snakeSpan = snake;
    Span<char> buffer = stackalloc char[snakeSpan.Length];

    int bufferPos = 0;
    bool toUpper = true;
    for (var i = 0; i < snake.Length; i++)
    {
        var target = snakeSpan[i];

        if (target == '_')
        {
            toUpper = true;
        }
        else
        {
            buffer[bufferPos++] = toUpper ? char.ToUpper(target) : target;
            toUpper = false;
        }
    }

    return buffer.Slice(0, bufferPos).ToString();
}

MD5 ハッシュの計算

MD5 ハッシュの計算にも Span が活用できます。 MD5CryptoServiceProvider はすこしラップしてあげると使いやすくなりますが、ここでも Span を活用します。 Span のコツは、できるだけ最後まで Span で扱うこと (= スタックを最大限に活用)、だと思ってます。 さっきの byte[] -> 16 進への変換も利用して、16 進数の書き込みまで Span だけでやってます。

/// <summary>
/// MD5 ヘルパ
/// </summary>
public static class Md5HashHelper
{
    // md5 インスタンスキャッシュ
    // このインスタンスはメソッド呼び出しによって内部状態が遷移するため
    // かならず内部状態の初期化を行うこと。
    // 呼び出し後に自動的に内部状態が初期化されるメソッドは ComputeHash, TryComputeHash のみです。
    [ThreadStatic] private static MD5CryptoServiceProvider t_md5 = null;

    /// <summary>
    /// MD5 ハッシュのバイト長
    /// </summary>
    public const int HashBytesLength = 16;

    /// <summary>
    /// MD5 ハッシュの十六進表記の文字数
    /// </summary>
    public const int HashHexStringLength = 32;
    
    /// <summary>
    /// キャッシュを使用するか
    /// </summary>
    /// <remarks>
    /// true の場合、 MD5CryptoServiceProvider のインスタンスをスレッドごとにキャッシュして使用します。
    /// false の場合は毎回新しい MD5CryptoServiceProvider のインスタンスを使用します。
    /// </remarks>
    public static bool UseCache { get; set; } = true;

    /// <summary>
    /// 指定したバイト配列のハッシュ値を計算します。
    /// </summary>
    public static byte[] ComputeHash(byte[] buffer)
    {
        return ComputeHash(buffer, 0, buffer.Length);
    }

    /// <summary>
    /// 指定したバイト配列の指定した領域のハッシュ値を計算します。
    /// </summary>
    public static byte[] ComputeHash(byte[] buffer, int offset, int count)
    {
        var md5 = RentUnsafe();
        var result = md5.ComputeHash(buffer, offset, count);
        ReturnUnsafe(md5);
        return result;
    }

    /// <summary>
    /// 指定した Stream オブジェクトのハッシュ値を計算します
    /// </summary>
    public static byte[] ComputeHash(Stream inputStream)
    {
        var md5 = RentUnsafe();
        var result = md5.ComputeHash(inputStream);
        ReturnUnsafe(md5);
        return result;
    }

    /// <summary>
    /// 入力バイト列のハッシュ値を計算し、出力バイト列にコピーします。
    /// </summary>
    public static bool TryComputeHash(ReadOnlySpan<byte> source, Span<byte> destination, out int bytesWritten)
    {
        var md5 = RentUnsafe();
        var result = md5.TryComputeHash(source, destination, out bytesWritten);
        ReturnUnsafe(md5);
        return result;
    }

    /// <summary>
    /// 入力バイト列のハッシュ値を計算し、16 進数表記で出力文字列にコピーします
    /// </summary>
    public static bool TryComputeHash(ReadOnlySpan<byte> source, Span<char> destination, out int charsWritten, bool upperCase = false)
    {
        Span<byte> buffer = stackalloc byte[16];
        if (TryComputeHash(source, buffer, out var bytesWritten))
        {
            ((ReadOnlySpan<byte>) buffer).WriteAsHexChars(destination, out charsWritten, upperCase);
            return true;
        }
        else
        {
            charsWritten = 0;
            return false;
        }
    }
    
    /// <summary>
    /// MD5 インスタンスを借用します。
    /// </summary>
    private static MD5CryptoServiceProvider RentUnsafe()
    {
        if (!UseCache || t_md5 == null)
        {
            return new MD5CryptoServiceProvider();
        }
        else
        {
            var md5 = t_md5;
            t_md5 = null;
            return md5;
        }
    }

    /// <summary>
    /// MD5 インスタンスをキャッシュに返却します。
    /// ※返却前に内部状態が汚染されたインスタンスを返却しないこと。
    /// </summary>
    private static void ReturnUnsafe(MD5CryptoServiceProvider md5)
    {
        if (UseCache && t_md5 == null && md5 != null)
        {
            t_md5 = md5;
        }
    }
}

Utf8Json のカスタムフォーマッタ

JSON のフォーマッタを書くときも Span を活用できます。

Enum を CamelCase にするフォーマッタ

PascalCase な enum メンバを CamelCase でシリアライズしたいことがあって書いてみました。

public class CamelCaseEnumFormatter<T> : IJsonFormatter<T>
    where T : struct
{
    public void Serialize(ref JsonWriter writer, T value, IJsonFormatterResolver formatterResolver)
    {
        var str = value.ToString();
        Span<char> buffer = stackalloc char[str.Length];
        str.AsSpan().CopyTo(buffer);
        buffer[0] = char.ToLower(buffer[0]);
        writer.WriteString(buffer.ToString());
    }

    public T Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
    {
        var str = reader.ReadString();
        return Enum.Parse<T>(str, true);
    }
}

おまけ: StringBuilder

これは半分遊びですが、スタック上で複雑な文字列を構築していきたいときのための、StringBuilder の Span 実装です。

public ref struct SpanStringBuilder
{
    private Span<char> _buffer;

    private int _index;

    public SpanStringBuilder(Span<char> buffer)
    {
        _buffer = buffer;
        _index = 0;
    }

    public void Write(ReadOnlySpan<char> str)
    {
        str.CopyTo(_buffer.Slice(_index));
        _index += str.Length;
    }

    public void WriteLine(ReadOnlySpan<char> str)
    {
        Write(str);
        Write(Environment.NewLine);
    }

    public void Write(char c)
    {
        _buffer[_index++] = c;
    }

    public void Write(int value)
    {
        if (!value.TryFormat(_buffer.Slice(_index), out var charsWritten))
        {
            throw new OutOfMemoryException("buffer のサイズが足りません");
        }

        _index += charsWritten;
    }

    public Span<char> AsSpan() => _buffer.Slice(0, _index);

    public override string ToString() => AsSpan().ToString();
}

読書メモ: 起業の科学 #1 スタートアップにとっての「良いアイデア」とは

いかに課題にフォーカスするか

解決する課題の質を高めよ

「スタートアップの生死を分けるのは、 Product Market Fit (PMF, 市場で顧客から熱狂的に愛される製品のこと) を達成できるかできないかだ」 - Mark Andreessen, Founder of Andreessen Horowitz

いくら優れたプロダクトを生み出しても、市場に受け入れられなければ成長はできない。 PMF を達成するためにはまず、自分たちのビジネスアイデアが市場から求められているものなのかを検証すること。 スタートアップにおいては 課題の質 にフォーカスすることが最も重要である。 スタートアップのアイデアは、 課題ドリブン (課題ありき) ではなく、ソリューションドリブン技術ドリブン であることがあまりに多い。

「バリューのある仕事をしようと思えば、取り組むテーマは イシュー度解の質 が両方高くなければならない」 - 安宅和人, CSO of Yahoo

バリューのあるアイデアを見つけるには、「課題の質を上げてから、ソリューションの質を上げる」というパスしかない。 よって、スタートアップを始めるにあたって真っ先に注力すべきは、解決を目指す課題の質を向上させることである。

  • 今検討しているアイデアは、顧客にとって本当に痛みのある課題なのか?
  • このアイデアの妥当な代替案が、既に市場に存在していないか?

のように、様々な角度からアイデアの深堀りを繰り返していくことで、課題の質が上がる。 それができたら、その課題に対する解決策を検討し、磨きをかけていくことで、初めて価値のある「良いアイデア」に至る。

課題を軽視して大失敗

課題の質ではなく、ソリューションの質にこだわると、顧客にはほとんど使われずに終わる。

グーグルグラスや Apple Watch は膨大な資金力とブランド力を背景に、 PMF ではなく力技を選んだ。 しかし結局、ユーザの期待に応えることはできず、思うようには売れなかった。

課題を意識しないのは自殺行為

先の例が示しているのは、 Google や Apple のような実績や資金力のある彼らですら、課題の質を軽視して、プロダクトありき、ソリューションありきで考えると容易に失敗するということ。 資金も人材も知名度もないスタートアップが課題を軽視することは、ただの自殺行為である。

課題の質を決める 3 つの要素

課題の質は、ファウンダー(創業者)個人が次の 3 つの要素をどれだけ持つかに依存する。

  • 高い専門性
  • 業界(現場)の知識
  • 市場環境の変化 (PEST: Political, Economic, Social, Technological) に対する理解度

これらが極めて重要であり、ファウンダー個人に求められる。

自分ごとの課題を解決せよ

課題の質を高めるもう一つの要素は、ターゲットとする課題が「自分ごと」であるかどうか。 Airbnb (創業者が家賃を払えないという切迫した状況から生まれた) や、ダイソン (従来の紙パックの掃除機の吸い込みの弱さやパックの交換が面倒で大きな憤りを覚えた) は、自分が痛みを感じている具体的な課題から始まり、その課題を自分ならどう解決するかという順番でビジネスアイデアが形成されている。

魔法のランプクエスチョン (もし魔法のランプがあって、課題を解決するためにソリューションを出してくれるとしたら、どんなものがいいか?) から導き出される答えの中に、ダイヤの原石があるはず。 自分ごとの課題といっても必ずしも本人が当事者である必要はなく、周りの身近な人が抱えている課題でも、その課題の痛みをしっかり理解しているのであれば OK。

課題の発見は、まだ「始まり」

課題の発見は始まりにすぎない。重要なのはその後の磨きこみである。 このときに自分ごとの課題でなければ、本気で磨きこみができない。

なぜあなたが、それをするのか?

スタートアップでは、 第三者の課題 (自分がそこまで共感や思い入れがない他人の課題) を解決しようとすることは避けなければならない。 強い共感が持てない課題は自分ごとにならず、痛みの検証がどうしても表面的になり、結果的に真の課題にたどり着くことが困難になる。 また、説得力もないので周囲からの協力も得られにくい。

「YC のインタビューでは『誰がその製品を心の底から欲しがっているのか?』を聞く。ベストの答えは起業家自身であることで、次に良いのは、ターゲットユーザをものすごく理解しているのがわかる解答だ」 - Sam Altman, President of Y Combinator

ファウンダーの課題に人は集まる

課題への強い共感や思いが、スタートアップのビジョン、ミッションに翻訳されていく。 プロダクトを市場に本格投入するまでのフェーズにおいて、ビジョンやミッションはスタートアップの最大の競合優位性になる。 ユーザ、起業メンバー、投資家はいずれもファウンダーが語るビジョンとミッションに惹かれて集まる。

スタートアップが歩む道のりが想像以上につらいことも、自分ごとの課題から始める必要がある理由である。 最初に立てた仮設はほぼ覆ってしまい、限りある資金がどんどん減っていく。 この時、ファウンダーが自分ごとの課題を持ち、将来的にこうあるべきだという強いビジョンやミッションを掲げていれば、高いレジリエンス(メンタルの回復力)の源になる。

課題に出会った原体験は何か

「自分ごとの課題になっているかどうか」を別の言い方で自問するなら、「その課題にストーリー(原体験)があるか」

誰が聞いても良いアイデアは避ける

アイデアは crazy か

誰が聞いても良いと思えるアイデアは、スタートアップにとっては選んではならないアイデアだ。 ネガティブなフィードバックが周囲から集まる状態を「マーケットが定義されていない状態」ととらえて、今のタイミングで事業を手掛けることがチャンスだと考える。

「一見アンセクシーだが、実はセクシーなアイデアを見つけることが決め手となる」 - George Kellerman, COO of Yamaha Motor Ventures & Laboratory Silicon Valley

例: 排せつ予測デバイス D Free

こうした crazy でアンセクシーなアイデアは、人に話すのが恥ずかしいはずだ。 人に話すのが恥ずかしい段階とは、その課題を言語化して説明するフレームワークを入手できていないために、人に伝えるのが困難だということ。 当然、その課題に目をつけている企業はまだ存在しないか、ごくわずかである。

一方、言語化して人に伝えられるような課題をターゲットにした場合は、既に課題が認識されており、妥当な代替案がある場合が多い。 このようなビジネスは投入できるリソースの勝負や価格勝負になるため、スタートアップが狙うのは賢明ではない。

「競争は負け犬がすることだ」 - Peter Thiel, Founder of PayPal

市場のシェアを激しく奪い合う消耗戦になると、リソースの多い大企業が圧倒的に有利になり、スタートアップに勝ち目はない。

大企業の意思決定の仕方

大企業が新規事業を始めるときは、取締役会でほとんどの役員が賛同しないと稟議の承認が下りない。 判断の際に役員が気にするのは課題の質などではなく、その事業がもたらす売り上げや利益の見通し、蓋然性、既存のコアビジネスと競合しないか、といった点である。 そのため、アンセクシーなアイデアには挑戦しにくい。

「スタートアップではハードなことをするほうが実は近道である。簡単な道を選ぶことは結果として遠回りになる」 - Sam Altman, President of Y Combinator, lecture in Stanford University

一見悪いアイデアが世界を変えた

crazy なアイデアで大成功を収めたスタートアップの代表格は Airbnb だ。 犯罪大国のアメリカで赤の他人の家に泊まる、他人を自宅に泊めるという行為は、まさにバッドアイデアそのものだった。

「多くの人が、 Airbnb はうまくいった最悪のビジネスアイデアだと言っている」 - Brian Chesky, CEO of Airbnb

Uber のように、だれもが当たり前だと思ってきたこと(タクシーを捕まえるために何分も手を挙げて待ち、英語が片言のドライバーに目的地を伝えるのも大変で、支払いは基本的に現金)に疑問を投げかけられるかどうかが、スタートアップが世界を変える存在になれるかどうかの最初の分かれ道になる。

他の人が知らない秘密を知っているか?

成功する人は、ほかの人が知らない秘密を知っている。 ここではインスタカート(スマホなどで注文すると近所のスーパーでの買い物を代行してくれるサービス)の例を見ていく。

インスタカートが大成功した理由

インスタカートは店舗と商品を選ぶと、「ショッパー」と呼ばれる一般人が、注文した人の代わりに店まで買い物に行き、 45 分以内に自宅まで届けるという仕組み。 創業者の Apoorva Mehta はアマゾンの物流システムを開発するエンジニアで、物流と小売りに関して高い専門性があり、市場の流れをつかんでいた。 しかし、彼自身が glossary shopping (食料品の買い物) は自分で行うもの、という通例に疑問を持った視点こそが重要。

インスタカートは経歴チェックやトレーニングプログラム、カスタマーレビューの仕組みを導入し、見知らぬ人に直接口委に入れる食料品の買い出しを頼んだり、住所を教えたりすることの不安を払拭した。 このサービスは全米に一気に広がった。

Mehta は Amazon 在職中に新規事業としてインスタカートのアイデアを提案したが、自社ビジネスと競合する奇抜な試みでしかないと却下された。 Amazon が膨大な資金を投資してきたインフラ (自社倉庫、外部の物流会社との契約) を否定することになるからだ。 この却下によって Amazon が現状のシステムでこのサービスに対応できないことが明確になり、Mehta はアマゾンを脅かすほどのビジネスチャンスがあると気付いた。

小売りとの win-win 関係

Amazon の台頭はスーパー等の小売店にとって脅威である(カスタマーの予算を奪い合う)が、インスタカートの場合は Win-Win の関係が成り立つ。これが Mehta が気付いた 秘密 である。

なぜ crazy なアイデアが求められるのか

crazy なアイデアが求められる背景には、 IT の進歩で、マーケットのパラダイムシフトが高速化していることがある。 2014 年ごろから、ユニコーン企業が続々と生まれるようになり、評価額の上昇カーブもすさまじいものになった。 この事実は、プロダクトやサービスの が短くなっていることも意味する。 これだけの速さだと、 First Mover (最初の市場参入会社) が出てきたのを後追いしても遅い場合があり、だからこそ、誰よりも先に PMF を達成することが重要である。 このときにベースになるのが、「一見すると悪いが、本当は良いアイデア」である。

イノベーションカーブの変化

従来のイノベーションカーブは、innovator (革新者) がいて、Early Adopter (新たに登場した商品、サービス、ライフスタイルなどを、比較的早期に受け入れ、それによって他の消費者・ユーザーへ大きな影響を与えるとされる利用者層) がいて、Chasm(深い裂け目、の意。Earyly Adopter と Early Majority の間の大きな溝) を超えるとようやく Early Majority (Early Adopter に追従する形で受容し始める利用者層)に到達する、といった話だった。 このイノベーションカーブであれば、ある程度の時間をかけてプロダクトを検証することができたが、スマホなどの普及による情報伝達スピードの加速で、このモデルは古くなっている。 現在は Trial Customer (新しもの好きの顧客) と Burst Majority (爆発的に広がる一般層) の 2 段階できわめて速く浸透する。 ユーザ数がある値を超えるのを待たず、一定数の Trial Customer のフィードバックをベースにプロダクトを磨きこむという新時代は、スタートアップにとっては好機ともいえる。

代替案のないアイデアを探せ

「新規事業を考えるときには無消費をターゲットにせよ」 - Clayton M. Christensen

無消費とは、顧客が何も持たない状態のこと。つまり、前例もなければ、既存の消費者もいない、代替案のない状態のこと。 こうした場所を発見して PMF を達成できれば、大きな成長を見込める。

ロイヤルティーループの劇的変化

Royalty loop とは、製品を知った人がそれを気に入り、ユーザとして定着して使い続けてくれるまでの流れを輪のような形で示したもの。

従来の Royalty loop は、 AIDMA(Attention - Interest - Desire - Memory - Action) モデルであり、ユーザは一つのプロダクトやサービスを選ぶために時間をかけて、それを使う価値があるかを検証していた。 最近はほとんどのサービスで、「最初の 1 カ月は無料」といったフリーミアムが定番化した。まず使ってみて、有用だと思ったら購入する。気軽に始め、使っていく中で本格的にユーザがそのサービスに定着する (他の代替案を捨てる) というあらたな Royalty loop が生まれた。

また、継続的に使用する顧客との接点が多いフリーミアムやサブスクリプションモデルの利点はユーザの囲い込みだけではなく、フィードバックを高速に集められるという点にもある。 それをもとにプロダクトを絶えず改善していくことで、ファンの量と質 (高い LTV、低い解約率) を増やしていくことができる。

スタートアップが避けるべき 7 つのアイデア

1. 誰が見ても、最初から良いアイデアに見えるもの

一見よさそうに見えるアイデアはすでに誰かが手掛け、たいていは失敗している。

2. ニッチすぎる

あまりにも市場がニッチすぎると、将来的な成長が見込めず、スケールしない。

3. 自分が欲しいものではなく、作れるものを作る

技術ドリブンではいけない。技術的に作れたから作っただけのプロダクトは、課題から生まれたものではないので市場に受け入れられない。

4. 根拠のない想像上の課題

クラウドファンディングなどを使うと、たまたま出したコンセプト動画が受けて必要以上の金額が集まり、後戻りできなくなるケースがある。 Customer Problem Fit を実現しないうちに、表面的に PMF を達成したような状態。 もしビジネスをスケールするつもりなら、どんな課題を解決できるプロダクトになっているかを検証すべき。

5. 分析から生まれたアイデア

一時期あふれかえってどれも失敗したグルーポンの模倣サービスは、市場を俯瞰して空いている部分を狙うというロジカルなトップダウンアプローチでビジネスを展開しただけで、スケールするストーリーや、ファウンダー自身の思いが欠けていた。

6. 激しい競争に切り込むアイデア

スタートアップは「競争を避けること」が戦略の第一義であると考えるべき。

7. 一言では表せないアイデア

「誰 (customer) の何 (課題) をどのように解決するか」を一言で表せないアイデアは磨きこみが足りない。

読書メモ: Peopleware #1 人材を活用する (1) - プロジェクト失敗の原因

トム・デマルコの名著、 Peopleware を読み始めたので自分の意見も交えて読書メモ。 本の要約と自分の意見が半々ぐらいな感じです。ここ数年の話題だったり、主語が「私」となっているものは私個人の意見です。

今日もどこかでトラブルが

筆者は 10 年間にわたり、500 以上の開発プロジェクトとその結果について調査した。 調査の結果、プロジェクトの 15 % が水泡に帰している(中止、延期、納入後に使用されない等)ことがわかり、また、大きなプロジェクトほど失敗する可能性が高いこともわかった。 25 人年以上を注ぎ込んだプロジェクトのうち、実に 25 % ものプロジェクトが完成しなかった。 プロジェクトが失敗した原因の圧倒的多数は、 単なる技術的な問題として片付けられないものばかり だった。

問題の本質

失敗の原因を関係者に尋ねると、異口同音に 政治的要因 と言われたが、この言葉はいい加減に使われていた。 実際には、意思疎通の問題、要因の問題、マネージャーや顧客への幻滅感、意欲の欠如、高い退職率等の 社会学的な問題 であった。 政治的な問題ではなく、プロジェクトとチームの社会学的問題として本質をとらえると、もっと取り組みやすくなるはずだ、というのが筆者の主張。

実際のところ、ソフトウェア開発上の問題の多くは、技術的というより社会学的なものである。

多くのマネージャは実際には技術だけに関心があるというようなマネジメントをしている。 最も大切なのは人間中心に考えることなのに、いつもないがしろにされている(これは企業の教育に原因があるとのこと)。

そういえば今となってはすっかり忘れ去られた 7pay 事件(事故?)のときも、失敗の表面的な原因は技術的にあまりにもお粗末な品質の成果物に見えたが、現場の実態を暴露する twitter での書き込みや、社長の記者会見における発言を見ていると本質はやはり社会学的な問題だったように思う。 普段からコンビニ経営において、一緒に働く仲間をを軽視した本部の姿勢が批判されていることと、この失敗は無関係ではないと思う。

ハイテクの幻影

多少なりとも最新技術に関係している人(これは日本でいうところの『エンジニア』を名乗る人たちに相当すると思う)は、 ハイテクの幻影 に取り憑かれている。 他人の研究成果を応用しているに過ぎないのに、自分がハイテクビジネスの旗手だと錯覚している(日本の twitter や Qiita を見ているとよくわかります)。 ソフトウェアは、多数のチームやプロジェクト、固く結束した作業グループで開発するので、ハイテクビジネスではなく人間関係ビジネスに携わっていると言える。 プロジェクトの成功は関係者の緊密な対人関係によって生まれ、失敗は疎遠な対人関係の結果である。

マネージャが技術的問題にうつつを抜かす理由は、重要だからではなく、単にやりやすいからだ。

ここで、私のような テックリード と呼ばれるポジションの人は特に気をつけたほうが良いと思う。 テックリードは技術的な面でもチームをリードする必要があるが、テックリードになれた人は技術的な面ではすでに強みを持っているはずなので、社会学的な問題のマネジメントに割く労力を意図的に増やしたほうが良い。 これは私も経験済みだが、どうしても取り掛かりやすく仕事をしている気分になれる技術的な問題にフォーカスしがちである。

C# で messagepack vs json 比較

Messagepack の良さを社内に布教するためにベンチマークを取ったので転載しておく。

個人的には MessagePack + LZ4 の Typeless がおすすめ。 messagepack は可読性が・・・と言われることが多いけど、 Typeless なシリアライズなら型情報がつくので、 dynamic にデシリアライズして json を吐くような小さいツールさえ作っておけば gzip 圧縮した json とそこまで使い勝手は変わらないのでは?と思う。 パフォーマンスの面では messagepack が有利。json + gzip はファイルサイズは小さくなったけど、パフォーマンスが許容できるかどうか。

ベンチマークに使ったコードは こちら

ファイルサイズ

json:                     137,248,217 bytes
json + gzip:              045,170,640 bytes
msgpack:                  084,959,431 bytes
msgpack + lz4:            061,381,468 bytes
msgpack (typeless):       083,949,384 bytes
msgpack (typeless) + lz4: 060,776,211 bytes

パフォーマンス

BenchmarkDotNet=v0.12.0, OS=ubuntu 19.04
Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.0.100
  [Host]   : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT
  ShortRun : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT

Job=ShortRun  IterationCount=3  LaunchCount=1  
WarmupCount=3  
|                 Method |       Mean |     Error |   StdDev | Ratio | RatioSD |     Gen 0 |     Gen 1 |     Gen 2 | Allocated | Completed Work Items | Lock Contentions |
|----------------------- |-----------:|----------:|---------:|------:|--------:|----------:|----------:|----------:|----------:|---------------------:|-----------------:|
|       Utf8JsonWithType | 1,001.4 ms | 337.02 ms | 18.47 ms |  1.00 |    0.00 | 1000.0000 | 1000.0000 |         - | 642.99 MB |               3.0000 |                - |
|     Utf8JsonWithTypeGz | 4,134.0 ms | 987.83 ms | 54.15 ms |  4.13 |    0.13 | 7000.0000 | 7000.0000 | 2000.0000 | 814.09 MB |               3.0000 |                - |
|       Utf8JsonTypeless |   984.2 ms |  50.38 ms |  2.76 ms |  0.98 |    0.02 | 1000.0000 | 1000.0000 |         - | 642.99 MB |               2.0000 |                - |
|     Utf8JsonTypelessGz | 4,194.6 ms | 777.21 ms | 42.60 ms |  4.19 |    0.04 | 7000.0000 | 7000.0000 | 2000.0000 | 814.08 MB |               3.0000 |                - |
|    MessagePackWithType |   390.3 ms | 231.84 ms | 12.71 ms |  0.39 |    0.02 |         - |         - |         - |  336.9 MB |               2.0000 |                - |
| MessagePackWithTypeLz4 |   537.4 ms |  66.94 ms |  3.67 ms |  0.54 |    0.01 | 2000.0000 | 2000.0000 |         - | 395.79 MB |               2.0000 |                - |
|    MessagePackTypeless |   368.1 ms |   2.59 ms |  0.14 ms |  0.37 |    0.01 |         - |         - |         - | 335.94 MB |               2.0000 |                - |
| MessagePackTypelessLz4 |   515.2 ms |  75.83 ms |  4.16 ms |  0.51 |    0.01 | 2000.0000 | 2000.0000 |         - | 394.22 MB |               2.0000 |                - |
// * Legends *
  Mean                 : Arithmetic mean of all measurements
  Error                : Half of 99.9% confidence interval
  StdDev               : Standard deviation of all measurements
  Ratio                : Mean of the ratio distribution ([Current]/[Baseline])
  RatioSD              : Standard deviation of the ratio distribution ([Current]/[Baseline])
  Gen 0                : GC Generation 0 collects per 1000 operations
  Gen 1                : GC Generation 1 collects per 1000 operations
  Gen 2                : GC Generation 2 collects per 1000 operations
  Allocated            : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
  Completed Work Items : The number of work items that have been processed in ThreadPool (per single operation)
  Lock Contentions     : The number of times there was contention upon trying to take a Monitor's lock (per single operation)
  1 ms                 : 1 Millisecond (0.001 sec)