Рубрики
Без рубрики

Многопоточность без блокировок

Просто потому, что вы разделяете структуры данных между потоками, это не значит, что вы должны использовать блокировки. Над… С тегами csharp, java, cpp, computer science.

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

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

ПРЕДУПРЕЖДЕНИЕ: Я перестал использовать эти шаблоны, потому что их легко неправильно понять и неправильно использовать, что может привести к случайным труднодиагностируемым ошибкам. Если вы используете эти шаблоны, вы не помогаете следующему разработчику лучше понять код или избежать ошибок. Вы были предупреждены.

Ссылка на неизменяемый объект

Мы знаем, что в многопоточной среде изменение общего объекта без блокировок – плохая идея. Но как насчет его замены?

Например, рассмотрим этот код. Я буду использовать C#, но то же самое применимо к Java, C++ или другому традиционному языку с потоками.

public class FunWithThreads {
    private Thing thing;

    public void InThreadOne() {
        this.thing = new Thing()
    }

    public Result InThreadTwo() {
        var localThing = this.thing;
        var result1 = SomeCalculation1(localThing);
        var result2 = SomeCalculation2(localThing);
        var finalResult = DoMoreWork(localThing, result1, result2);
        return result;
    }
}

Предположим, что множество потоков одновременно вызывают В первом потоке() , в то время как множество других потоков одновременно вызывают Во втором потоке() . Получим ли мы когда-нибудь неправильный результат, условия гонки или конфликт?

Не совсем. Во втором потоке() всегда ссылается на согласованную версию this.thing , потому что она помещает ссылку на нее в локальную переменную и с этого момента использует только локальную ссылку. Если вычисления займут слишком много времени, то, возможно, мы получим немного устаревший результат, но этот результат всегда будет последовательным.

Где это полезно : Я обнаружил, что этот подход полезен всякий раз, когда this.thing представляет собой большую предварительно вычисленную структуру данных, которая обновляется нечасто, но часто читается. В основном я использовал его для кэширования некоторых данных для внутренних приложений на рынках капитала, чтобы сделать приложение более удобным для пользователя.

Подводные камни : Действительно важно убедиться, что this.thing упоминается только один раз в начале метода, иначе мы вернемся к условиям гонки. Также очень важно, чтобы this.thing никогда не изменялся после его назначения. Это означает множество описательных комментариев, очень тщательный анализ кода и уверенность в том, что каждый разработчик, который когда-либо просматривал этот код, знает все подводные камни. В более крупной кодовой базе кто-то (возможно, даже вы!) все равно может найти способ где-то ошибиться.

Сложные атомарные операции

Причина, по которой приведенный выше код работает, заключается в том, что в C # (и в большинстве других языков) назначение ссылок является атомарной операцией, т.Е. Во время ее выполнения другой поток не может вмешаться на полпути и сделать что-то еще.

Современные процессоры имеют несколько других интересных атомарных операций, таких как compare и swap , реализованных в C# как Interlocked. Сравните Exchange() , а в Java как AtomicInteger.compareAndSet() . Эта функция принимает три аргумента и выполняет следующее в качестве атомарной операции:

  1. Сравнивает первый и третий аргументы, затем
  2. Если они равны, устанавливает первый аргумент равным второму аргументу, или
  3. Если они не равны, ничего не делает.

Один из случаев, когда это может быть использовано, – это попытка ограничить запросы на дорогостоящую операцию с ограниченным ресурсом. Например, этот код отбросит любые запросы, если дорогостоящая операция уже запущена:

// Let's pretend this class is a singleton.
public class ExpensiveOperationLimiter {
    private int isProcessing = 0; 

    public void RequestExpensiveOperation() {
        // If isProcessing == 1, this does nothing and returns 1.
        // If isProcessing == 0, this sets isProcessing to 1 and returns 0. 
        var wasProcessing = Interlocked.CompareExchange(ref isProcessing, 1, 0);

        if (!wasProcessing) {
            // Only one invocation will be running at a time.
            DoExpensiveOperation()
        }

        isProcessing = 0;
    }

    private void DoExpensiveOperation() {
        // ...
    }
}

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

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

Где это полезно : Я использовал этот тип кода в ситуациях, когда события могут случайным образом запускать обновление кэша сервера приложений из базы данных или из другой системы.

Подводные камни : Как правило, я бы счел это микрооптимизацией и не стоящей затраченных усилий. Многие разработчики понимают или слышали о блокировках, и им легче следовать им, чем любому коду, который включает сравнение и установку. Как и прежде, используя это, вы рискуете, что кто-то (включая вас!) в конечном итоге совершит ошибку.

Лучший способ избежать блокировок

Не пишите многопоточный код!

Бесстыдная пробка

Проверьте Измерьте , тайм-трекер, который каждые 10 минут спрашивает вас, что вы делаете.

Оригинал: “https://dev.to/iourikhramtsov/multi-threading-without-locks-fgb”