Разрешение ровно одному потоку проходить / не заставлять потоки ждать без необходимости

Прежде всего, позвольте мне объяснить ситуацию...

У меня есть класс, который имеет 2 свойства: DataA и DataB; не имеет значения, кто они, важно то, что каждый может быть рассчитан из другого. Я нахожусь в многопоточной среде и хочу, чтобы DataA / DataB вычислялись, когда и если это необходимо (это не всегда тот случай, когда оба будут доступны). Моей первой мыслью было что-то вроде...

public SomeDataObject DataA
{
    get
    {
        if (dataAisAvailable)
        {
            return dataA;
        }
        else
        {
            if (dataBisAvailable)
            {
                lock (dataACalcLock)
                {
                    // Don't want other threads recalculating dataA
                    if (dataAisAvailable)
                    {
                        return dataA;
                    }

                    ////////////////////////////////
                    // Calculate dataA from dataB //
                    ////////////////////////////////

                    dataAisAvailable = true;
                    return dataA;
                }
            }
            else
            {
                return null;
            }
        }
    }
}

С этим шаблоном, предполагая, что dataB действительно доступен, первый поток, прибывающий на сцену (давайте назовем его thread1), входит и вычисляет dataA, пока все хорошо... Однако любые потоки, которые ожидали, что dataA будут посчитал теперь приходится обращаться к нему один за другим... не очень эффективно имхо. Я хотел бы, чтобы эти потоки ожидали, пока thread1 завершит вычисление dataA, а затем все пойдут на это, как будто нет блокировки.

Возможно, у меня были другие мысли о ManualResetEvents, но я не уверен, как я могу безопасно гарантировать, что один и ровно один поток проходит через вычисления.

Я надеюсь, что я объяснил это ясно, хотя я не уверен, что у меня есть. Рад уточнить, хотя...

РЕДАКТИРОВАТЬ: мой плохой, я на.NET 4.0. И это в Silverlight...

4 ответа

Решение

Вдохновленный комментарием к моему вопросу, который был впоследствии удален, я проверил Lazy<> и пошел с чем-то похожим на...

    static SomeDataObject DefaultData;

    private Lazy<SomeDataObject> dataA = new Lazy<SomeDataObject>(() => DefaultData, LazyThreadSafetyMode.ExecutionAndPublication);
    private Lazy<SomeDataObject> dataB = new Lazy<SomeDataObject>(() => DefaultData, LazyThreadSafetyMode.ExecutionAndPublication);

    public SomeDataObject DataA
    {
        get
        {
            return dataA.Value;
        }
        set
        {
            dataA = new Lazy<SomeDataObject>(() => value, LazyThreadSafetyMode.ExecutionAndPublication);
            dataB = new Lazy<SomeDataObject>(GetDataB, LazyThreadSafetyMode.ExecutionAndPublication);
        }
    }
    public SomeDataObject DataB
    {
        get
        {
            return dataB.Value;
        }
        set
        {
            dataB = new Lazy<SomeDataObject>(() => value, LazyThreadSafetyMode.ExecutionAndPublication);
            dataA = new Lazy<SomeDataObject>(GetDataA, LazyThreadSafetyMode.ExecutionAndPublication);
        }
    }

    private SomeDataObject GetDataA()
    {
        if (DefaultData == dataB.Value)
        {
            return null;
        }

        ////////////////////////////////
        // Calculate dataA from dataB //
        // and return it.             //
        ////////////////////////////////
    }
    private SomeDataObject GetDataB()
    {
        if (DefaultData == dataA.Value)
        {
            return null;
        }

        ////////////////////////////////
        // Calculate dataA from dataB //
        // and return it.             //
        ////////////////////////////////
    }

К сожалению, ReaderWriterSlimLock не был доступен в Silverlight, в противном случае он выглядел многообещающим. Немного сравнительного анализа позволило предположить, что все вышеперечисленное намного быстрее (не говоря уже о том, чтобы делать больше, чем я хотел), чем все, что я написал сам.

Это звучит очень похоже на сценарий, для которого ReaderWriterLockSlim был изобретен (разрешено несколько одновременных программ чтения, в то время как разрешено писать только одному потоку за раз).

Это может выглядеть примерно так (не проверял этот код должным образом, поэтому убедитесь, что он работает так, как вы хотите, если решите его использовать):

private ReaderWriterLockSlim dataLock = new ReaderWriterLockSlim();

public SomeDataObject DataA
{
    get
    {
        if (dataAisAvailable)
        {
            return dataA;
        }

        dataLock.EnterReadLock();

        try
        {
            if (dataBisAvailable)
            {
                dataLock.EnterUpgradeableReadLock();

                try
                {
                    // Don't want other threads recalculating dataA
                    if (dataAisAvailable)
                    {
                        return dataA;
                    }

                    dataLock.EnterWriteLock();
                    try
                    {
                        ////////////////////////////////
                        // Calculate dataA from dataB //
                        ////////////////////////////////

                        dataAisAvailable = true;
                    }
                    finally
                    {
                        dataLock.ExitWriteLock();
                    }

                    return dataA;
                }
                finally
                {
                    dataLock.ExitUpgradeableReadLock();
                }
            }
            else
            {
                return null;
            }
        }
        finally
        {
            dataLock.EnterReadLock();
        }
    }
}

Совет / плагин: если вы хотите немного сократить количество добавляемых конструкций try/finally, вы можете обернуть некоторые из них в методы расширения ( как представлено в моем блоге) или, возможно, даже чище, обернув их в IDisposable прокси ( как предложено Джошем Перри).

Предложение:

if (dataAisAvailable)
        {
             //Wait for AutoResetEvent here, perhaps add a timeout and when it expires, you can return the current dataA, so threads don't wait forever. 
            return dataA;
        }
        else
        {
            if (dataBisAvailable)
            {
                lock (dataACalcLock)
                {
                    // Don't want other threads recalculating dataA
                    if (dataAisAvailable)
                    {
                        return dataA;
                    }

                    ////////////////////////////////
                    // Calculate dataA from dataB //
                    ////////////////////////////////

                    dataAisAvailable = true;
                     //Set AutoResetEvent to signalled so waiting threads can get to DataA.
                    return dataA;
                }
            }

Чтобы быть уверенным - что вы хотите сделать, это избежать пересчета A и B, избегая блокировок при доступе к ним после первого вычисления, да?

Если поток читает dataAisAvailable и находит его истинным, то проблем нет - поток может использовать A. Если он читает dataAisAvailable и находит его ложным, возникает проблема, и ему необходимо получить эксклюзивную блокировку, чтобы гарантировать, что dataAisAvailable по-прежнему ложь и посчитай если так. Я думаю, что критический раздел подойдет. Если для вычисления A/B требуются возрасты, это приведет к блокировке ядра, но это произойдет только в том случае, если поток обнаружит одно из логических значений false.

Я думаю, что вам это сойдет с рук, потому что булевы значения всегда идут "в одну сторону", от ложного к истинному, и поэтому вы можете уйти с помощью простой булевой проверки (я думаю).

Ваш "поток 1" найдет dataAisAvailable false и попытается получить блокировку. Если это удается, он снова проверяет dataAisAvailable внутри блокировки и вычисляет его или нет. Затем он выходит из блокировки, возвращая данные A. Если "поток 2" входит первым, между потоком 1, проверяющим dataAisAvailable, и потоком 1, получающим блокировку, вычисляет dataA и выходит из блокировки, поток 1 затем входит в блокировку, находя, что dataAisAvailable теперь имеет значение true, и поэтому просто выйдите из блокировки с dataA,

Тогда потоки 2-N всегда будут видеть dataAisAvailable как true и получать A, даже не пытаясь получить блокировку.

Ргдс, Мартин

Другие вопросы по тегам