Елена Булыгина предлагает Вам запомнить сайт «Ленусик»
Вы хотите запомнить сайт «Ленусик»?
Да Нет
×
Прогноз погоды

Основная статья: Multithreading

Python в три ручья (часть 2). Блокировки

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

Потоки стремятся к ресурсам, с которыми должны работать. И когда к одному и тому же ресурсу обращается несколько потоков, возникает конфликт. Как его предотвратить?

Потоки нельзя в любой момент напрямую остановить или завершить: на то они и потоки. Но можно на их пути поставить дамбу — блокировку. Она пропустит только один поток, а остальные временно удержит. Так вы исключите конфликт.

Когда поток A выполняет операцию с общими ресурсами, а поток Б не может вмешаться в нее до завершения — говорят, что такая операция атомарна. Залогом потокобезопасности как раз выступает атомарность — непрерывность, неделимость операции.

Простая блокировка в Python

Взаимоисключение (mutual exception, кратко — mutex) — простейшая блокировка, которая на время работы потока с ресурсом закрывает последний от других обращений. Реализуют это с помощью класса Lock.

import threading
mutex = threading.Lock()

Мы создали блокировку с именем mutex, но могли бы назвать её lock или иначе. Теперь её можно ставить и снимать методами .acquire() и .release():

resource = 0

def thread_safe_function():
global resource
    for i in range(1000000):
        mutex.acquire()
        # Делаем что-то с переменной resource
        mutex.release()

Обратите внимание: обойти простую блокировку не может даже поток, который её активировал. Он будет заблокирован, если попытается повторно захватить ресурс, который удерживает.

С блокировками и без. Пример–сравнение

Что происходит, когда два потока бьются за ресурсы, и как при этом сохранить целостность данных? Разберёмся на практике.

Возьмём простейшие операции инкремента и декремента (увеличения и уменьшения числа). В роли общих ресурсов выступят глобальные числовые переменные: назовём их protected_resource и unprotected_resource. К каждой обратятся по два потока: один будет в цикле увеличивать значение с 0 до 50 000, другой — уменьшать до 0. Первую переменную обработаем с блокировками, а вторую — без.

import threading

protected_resource = 0
unprotected_resource = 0

NUM = 50000
mutex = threading.Lock()

# Потокобезопасный инкремент
def safe_plus():
    global protected_resource
    for i in range(NUM):
        # Ставим блокировку
        mutex.acquire()
        protected_resource += 1
        mutex.release()

# Потокобезопасный декремент
def safe_minus():
    global protected_resource
    for i in range(NUM):
        mutex.acquire()
        protected_resource -= 1
        mutex.release()

# То же, но без блокировки
def risky_plus():
    global unprotected_resource
    for i in range(NUM):
        unprotected_resource += 1

def risky_minus():
    global unprotected_resource
    for i in range(NUM):
        unprotected_resource -= 1

В названия потокобезопасных функций мы поставили префикс safe_, а небезопасных — risky_.

Создадим 4 потока, которые будут выполнять функции с блокировками и без:

thread1 = threading.Thread(target = safe_plus)
thread2 = threading.Thread(target = safe_minus)
thread3 = threading.Thread(target = risky_plus)
thread4 = threading.Thread(target = risky_minus)
thread1.start()
thread2.start()
thread3.start()
thread4.start()
thread1.join()
thread2.join()
thread3.join()
thread4.join()
print ("Результат при работе с блокировкой %s" % protected_resource)
print ("Результат без блокировки %s" % unprotected_resource)

Запускаем код несколько раз подряд и видим, что полученное без блокировки значение меняется случайным образом. При использовании блокировки всё работает последовательно: сначала значение растёт, затем — уменьшается, и в итоге получаем 0. А потоки thread3 и thread4 работают без блокировки и наперебой обращаются к глобальной переменной. Каждый выполняет столько операций своего цикла, сколько успевает за время активности. Поэтому при каждом запуске получаем случайные числа.

Как избежать взаимных блокировок?

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

Если есть «захват» мьютекса, ничто не должно помешать последующему «высвобождению». Это значит, что release() должен срабатывать, как только блокировка становится не нужна.

Пишите код так, чтобы блокировки снимались, даже если функция выбрасывает исключение и завершает работу нештатно. Подстраховаться можно с помощью конструкции try-except-finally:

try:
    mutex.acquire()
    # Ваш код...

except SomethingGoesWrong:
    # Обрабатываем исключения

finally:
    # Ещё код
    mutex.release()

Другие инструменты синхронизации в Python

До сих пор мы работали только с простой блокировкой Lock, но распределять доступ к общим ресурсам можно разными средствами.

Семафоры (Semaphore)

Семафор — это связка из блокировки и счётчика потоков. Если заданное число потоков уже работает с ресурсам, лишние будут блокироваться. Это удобно, чтобы ограничить число подключений к сети или одновременно авторизованных пользователей программы.

Значение счётчика уменьшается с каждым новым вызовом acquire(), то есть с подключением к ресурсу новых потоков. Когда ресурс высвобождается, значение возрастает. При нулевом значении счётчика работа потока останавливается, пока другой поток не вызовет метод release(). По умолчанию значение счётчика равно 1.

s = Semaphore(5)
# В скобках при необходимости указывают стартовое значение счётчика

Можно создать «ограниченный семафор» конструктором BoundedSemaphore().

События (Event)

Событие — сигнал от одного потока другим. Если событие возникло — ставят флаг методом .set(), а после обработки события — снимают с помощью .clear(). Пока флага нет, ресурс заблокирован. Ждать события могут один или несколько потоков. Важную роль играет wait(): если флаг установлен, этот метод спокойно отдаёт управление ресурсом; если нет — блокирует его на заданное время или до установки флага одним из потоков.

e = threading.Event()

def event_manager():
    # Ждём, когда кто-нибудь захватит флаг
    e.wait()
    ...
    # Ставим флаг
    e.set()

    # Работаем с ресурсом
        ...
   # Снимаем флаг и ждём нового
   e.clear()

Если нужно задать время ожидания, его пишут в секундах, в виде числа с плавающей запятой. Например: e.wait(3,0).

Метод is_set() проверяет, активно ли событие. Важно следить, чтобы события попадали в поле зрения потоков-потребителей сразу после появления. Иначе работа зависящих от события потоков нарушится.

Рекурсивная блокировка (RLock)

Такая блокировка позволяет одному потоку захватывать ресурс несколько раз, но блокирует все остальные потоки. Это полезно, когда вы используете вложенные функции, каждая из которых тоже применяет блокировку. Число вложенных .acquire() и .release() не даст интерпретатору запутаться, сколько раз поток имеет право захватывать ресурс, а когда блокировку надо снять полностью. Механизм основан на классе RLock:

import threading, random

counter = 0
re_mutex = threading.RLock()

def step_one():
    global counter
    re_mutex.acquire()
    counter = random.randint(1,100)
    print("Random number %s" % counter)
    re_mutex.release()

def step_two():
    global counter
    re_mutex.acquire()
    counter *= 2
    print("Doubled = %s" % counter)
    re_mutex.release()
   
def walkthrough():
    re_mutex.acquire()
    try:
        step_one()
        step_two()
    finally:
        re_mutex.release()

t = threading.Thread(target = walkthrough)
t2 = threading.Thread(target = walkthrough)

t.start()
t2.start()
t.join()
t2.join()

Запустите это и проверьте результат: арифметика должна быть верна.

Теперь попробуйте убрать блокировку внутри walkthrough:

def walkthrough():
        step_one()
        step_two()

Ещё раз запустите код — порядок действий нарушится. Программа умножит на 2 только второе случайное число, а затем удвоит полученное произведение.

Переменные состояния (Condition)

Переменная состояния — усложнённый вариант события (Event). Через Condition на ресурс ставят блокировку нужного типа, и она работает, пока не произойдёт ожидаемое потоками изменение. Как только это случается, один или несколько потоков разблокируются. Оповестить потоки о событии можно методами:

  • notify() — для одного потока;
  • notifyAll() — для всех ожидающих потоков.

Это выглядит так:

# Создаём рекурсивную блокировку
mutex = threading.RLock()

# Создаём переменную состояния и связываем с блокировкой
cond = threading.Condition(mutex)

# Поток-потребитель ждёт свободного ресурса и захватывает его
def consumer():
    while True:
            cond.acquire()
            while not resourse_free():
            cond.wait()
            get_free_resource()
            cond.release()

# Поток-производитель разблокирует ресурс и уведомляет об этом потребителя
def producer():
    while True:
            cond.acquire()
            unblock_resource()
            # Сигналим потоку: "Налетай на новые данные!"
            cond.notify()
            cond.release()

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

cond = threading.Condition(mutex)
another_cond = threading.Condition(mutex)

Компактные блокировки с with

При множестве участков с блокировками каждый раз прописывать «захват» и «высвобождение» утомительно. Сократить код поможет конструкция с оператором with. Она использует менеджер контекста, который позволяет сначала подготовить приложение к выполнению фрагмента кода, а затем гарантированно освободить задействованные ресурсы.

Чтобы понять дальнейший материал, кратко разберем работу with, хотя это и не про блокировки. У класса, который мы собираемся использовать с with, должно быть два метода:

  • «Предисловие» — метод __enter__(). Здесь можно ставить блокировку и  прописывать другие настройки;

  • «Послесловие» — метод __exit__(). Он срабатывает, когда все инструкции выполнены или работа блока прервана. Здесь можно снять блокировку и/или предусмотреть реакцию на исключения, которые могут быть выброшены.

Удача! У нашего целевого класса Lock эти два метода уже прописаны. Поэтому любой экземпляр объекта Lock можно использовать с with без дополнительных настроек.

Отредактируем функцию из примера с инкрементом. Поставим блокировку, которая сама снимется, как только управляющий поток выйдет за пределы with-блока:

def safe_plus():
    global protected_resource
    for i in range(NUM):
        with mutex:
 protected_resource += 1

# И никаких acquire-release!
Пройти обучение

8 июн 18, 21:33
0 0
Статистика 1
Показы: 1 Охват: 0 Прочтений: 0

Как избежать тупиковых блокировок в Java

В прошлой статье мы обсуждали многопоточность. В этот раз поговорим о главной проблеме многопоточных приложений — тупиковых взаимных блокировках, известных как deadlocks. Такие блокировки возникают, когда минимум два потока одновременно пытаются работать с общими ресурсами и ограничить доступ к этим ресурсам друг для друга. При этом часто создаётся ситуация, когда ни один поток не может ни получить нужный ресурс, ни освободить занимаемый. Для блокировки «конкурентов» поток может использовать mutex, критическую секцию или семафор.

Как происходит блокировка

Два потока работают с общими ресурсами.

Поток 1 захватывает Ресурс 1 и начинает операции с ним.

Поток 2 последовательно захватывает Ресурс 2 и Ресурс 1.

Поток 2 не получает доступа к Ресурсу 1 и в ступоре ждёт, когда тот освободится.

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

Как это выглядит в коде:

public class DeadlockTest {  
 public static void main(String[] args) {  
   final String res1 = "my sample text";  
   final String res2 = "some other text";  

   // Пусть поток P1 навесит замок на ресурс res1, а затем на res2
   Thread P1 = new Thread() {  
     public void run() {  
         synchronized (res1) {  
          System.out.println("Поток 1 навесил замок на Ресурс 1");  
          try { Thread.sleep(100);} catch (Exception e) {}  

          synchronized (res2) {  
           System.out.println("Поток 1 навесил замок на Ресурс 2");  
          }
        }
     }
   }

   // Поток P2 последовательно пытается запереть доступ к res2 и res1
   Thread P2 = new Thread() {  
     public void run() {  
       synchronized (res2) {  
         System.out.println("Поток 2 навесил замок на Ресурс 2");  
         try { Thread.sleep(100);} catch (Exception e) {}  
         synchronized (res1) {  
           System.out.println("Поток 2 навесил замок на Ресурс 1");  
         }
       }
     }
   }

   P1.start();  
   P2.start();  

 }
}  

Видимо-невидимо

Вторая проблема  — видимость данных. Если два потока работают с одной переменной, каждый из них хранит её копию в кэше процессора, на котором запущен. Изменения в одной копии не отражаются мгновенно в основной памяти и других копиях. Это ведёт к путанице: одни потоки работают с актуальным значением, другие  — с устаревшим.

Есть несколько способов уберечь Java-приложение от «падений» и «зависаний», связанных с противоречиями в работе потоков. Это механизмы synchronized и volatile, алгоритмы, реализованные в классах Java Concurrent, но главное  — забота о структуре вашего приложения.

Ключевое слово volatile в Java

Модификатор volatile используют, когда нужно:

  • обеспечить видимость данных  — убедиться, что при обращении к переменной любой поток получит её последнее записанное значение;
  • исключить кэширование значений переменной и хранить их только в основной памяти.

Как только один поток записал что-то в volatile-переменную, значение идёт прямо в общую память и тут же доступно остальным потокам:

class CarSharingBase
{
  static volatile int your_car_ID = 3222233;
}

Но учтите, что модификатор volatile никак не ограничивает одновременный доступ к данным. А значит, в работу одного потока с полем может вмешаться другой поток. Вот что будет, если два потока одновременно получат доступ к операции увеличения на единицу (i++):

int i = 0;

Поток 1: читает переменную (0)

Поток 1: прибавляет единицу

Поток 2: читает переменную (0)

Поток 1: записывает значение (1)

Поток 2: прибавляет единицу

Поток 2: записывает значение (1)

Если бы два потока не мешали друг другу, а работали последовательно, мы получили бы на выходе значение «2», но вместо этого  видим единицу. Чтобы такого не происходило, нужно обеспечить атомарность операции. Атомарными называют операции, которые могут быть выполнены только полностью. Если они не выполняются полностью, они не выполняются вообще, но прервать их невозможно.

В примере с увеличением на единицу мы видим сразу три действия:  чтение, сложение, запись. Чтение и запись — операции атомарные, но между ними могут вклиниться действия другого потока. Поэтому составная операция инкремента (i++) полностью атомарной не является.

Обратите внимание: с volatile-переменной возможны как атомарные, так и неатомарные операции. Ключевое слово volatile позволяет сделать так, чтобы все потоки читали одно и то же из основной памяти, но не более того.

Простейший способ гарантировать атомарность  — выстроить потоки в очередь за ресурсами с помощью механизма synchronized. Представьте, что на электронный счёт одновременно переводят деньги два клиента. Уж лучше попросить одного из них немного подождать, чем допустить ошибки в денежных расчетах.

Ключевое слово synchronized

Модификатор synchronized исключает доступ второго и последующих потоков к данным, с которыми уже работает один поток. Это ключевое слово используют только для методов и произвольных блоков кода.

Используйте synchronized, чтобы:

  • обеспечить доступ только одного потока к методу или блоку единовременно;
  • обеспечить каждому работающему с ресурсами потоку видимость изменений, внесённых предыдущим потоком;
  • гарантировать, что операции внутри блока или метода будут выполнены полностью, либо не выполнены вовсе.

Метод может быть статическим или нет  — без разницы. Но синхронизация влияет на видимость данных в памяти. В прошлой статье мы говорили о взаимном исключении (mutex’e). С его помощью synchronized ограничивает доступ к данным. Образно говоря, это замок, с помощью которого поток запирается наедине с объектом, чтобы никто не мешал работать. Обратите внимание: замок запирают до начала работы. То есть проверка, не заняты ли ресурсы кем-то другим, происходит на входе в synchronized-блок или метод.

public class SynchronizeThis {
    private int syn_result;
    public synchronized int methodGet() {
        return syn_result;
    }
}

Разблокировка же ресурсов происходит на выходе. Поэтому атомарность операций гарантирована.

Данные, которые изменились внутри метода или блока sychronized, находятся в кэше поверх основной памяти и видны следующему потоку, к которому перешёл мьютекс.

Подсказки по блокирующей синхронизации

Главное при работе с synchronized  — правильно выбрать объект, по которому будет происходить проверка доступности ресурсов. Если вам нужна синхронизация на входе в метод, учитывайте, принадлежит этот метод классу или объекту. Вход в статичный метод блокируют по объекту класса, а в абстрактный метод  — по this. Если по ошибке заблокировать метод класса по this, доступ к ресурсам останется открыт для всех.

Никогда не используйте synchronized в конструкторе  — получите ошибку компиляции. А ещё остерегайтесь «матрёшек», когда синхронизированные методы одного класса вызывают внутри себя синхронизированные методы других классов.

Помните, что синхронизация требует ресурсов. При обработке большого массива данных вызов мьютексов становится особенно затратным. Чтобы гарантировать атомарность без синхронизации, используют классы Concurrent.

Атомарность с помощью Java Concurrent

Вернёмся к составной операции «чтение-изменение-запись». Когда нам нужно развести потоки по углам, но без мьютекса, можно использовать инструкцию «сравнение с обменом»  — compare and swap (CAS).

Для этого сначала заводят переменную, по значению которой можно понять, заняты ли ресурсы и, если да,  — кем. Например, пока ресурсы свободны, переменная хранит «-1», а если заняты — номер процессора, который с ними работает (0,1 и т.д.).

Поток приходит за свободными ресурсами, видит «-1»,  перезаписывает значение на номер процессора, на котором сам работает, выполняет действия. После завершения всех операций переменной возвращается значение «-1». Если  же поток на входе видит номер какого-то процессора, он получает отказ и не может выполнить намеченную операцию. Это простейший случай сравнения с обменом. Важно понимать, что поток, который получил отказ, не блокируется. Он может сообщить программе, что у него проблемы, и перейти к запасному плану действий.

Можно сказать, что это более интеллигентная форма взаимодействия между потоками. Они уже не бодаются за ресурс и не закрывают дверь перед носом оппонента, а обмениваются сообщениями в духе: «Хотелось бы поработать вот с этим»  — «Извините, оно пока занято. Не желаете ли кофе?».

На этом принципе построен целый ряд алгоритмов синхронизации, которые называют неблокирующими (non-blocking). Создание таких алгоритмов  — задача не для новичка. Но, к статью, в Java «из коробки» есть несколько готовых неблокирующих решений. Они собраны в пакете java.util.concurrent.

ConcurrentLinkedQueue

На русский название класса переводится как «параллельная очередь». Работает такая очередь по принципу First In First Out («Первым зашёл  — первым выйдешь»). Алгоритм основан на CAS, быстр и оптимизирован под работу со сборщиком мусора.

Если вы хотите создать очередь из объектов, а затем добавлять и удалять их в нужный момент, не нужно писать и синхронизировать методы вручную. Достаточно создать классы для потоков, производящих и потребляющих данные, а затем поставить эти потоки в очередь ConcurrentLinkedQueue.

В прошлой статье мы говорили, что в Java поток можно создать как экземпляр класса Thread или как отдельный класс с интерфейсом Runnable. Сейчас мы используем второй подход. Единственный метод интерфейса Runnable —  run(). Чтобы задать нужное поведение для потребителя и производителя, мы будем переопределять этот метод в каждом случае по-своему.

Поток-производитель:

public class ProducerThread implements Runnable {
    @Override
    public void run() {
        System.out.println("Генерируем сообщения в очередь");
        try {
            for (int i = 1; i <= 10; i++) {
                QueueTest.enqueueTask("Задача номер " + i);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

Поток-потребитель:

public class ConsumerThread implements Runnable {
    @Override
    public void run() {
        String task;
        System.out.println("Ждём задачи \n");
   //Пока есть задачи в очереди:
        while (QueueTest.isTaskHasBeenSet() || QueueTest.getQueue().size() > 0) {
            if ((task = QueueTest.getQueue().poll()) != null)
                System.out.println("Выполняю задачу : " + task);
            try {
                Thread.sleep(500);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

Обратите внимание, если очередь пуста, метод poll() вернёт значение null. Поэтому нам нужно было убедиться, что он возвращает что-то другое.

Чтобы узнавать, сколько всего элементов в очереди, у класса ConcurrentLinkedQueue есть метод size(). Он работает медленно, поэтому злоупотреблять им не стоит. При необходимости можно вывести весь список элементов очереди методом toArray(), но сейчас нам это не нужно.

Очередь, в которой будут работать потоки:

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

public class QueueTest {
    private static Queue<String> queue = null;
    private static boolean taskHasBeenSet = false;

    public static void main(String[] args) {
        queue = new ConcurrentLinkedQueue<String>();
// Создаём и запускаем потребителя и производителя
        Thread producer = new Thread(new ProducerThread());
        Thread consumer = new Thread(new ConsumerThread());

        producer.start();
        consumer.start();

        while (consumer.isAlive()) {
            try {
    // Оставим время на ожидание постановки задач
                Thread.sleep(1000); 
            } catch (InterruptedException e) {
                 // Выводим трейс вместе с текстом исключения
                e.printStackTrace();
            }
        }
   // Всё выполнено нормально, выходим.
        System.exit(0);
    }

    public static Queue<String> getQueue() {
        return queue;
    }

    // Добавляем задачи в очередь
    public static void enqueueTask(String task) {
        try {
            queue.add(task);
            System.out.println("Добавлена задача : " + task);
            Thread.sleep(200);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }

    public static boolean isTaskHasBeenSet() {
        return taskHasBeenSet;
    }
    
    public static void setTaskHasBeenSet(boolean taskHasBeenSet) {
        QueueTest.taskHasBeenSet = taskHasBeenSet;
    }
}

Запустите и посмотрите, как добавляются и выполняются задачи.

Атомарные классы

Пакет java.util.concurrent.atomic включает в себя классы для работы с:

  • примитивами  — AtomicBoolean, AtomicInteger, AtomicLong;
  • ссылочными типами   — AtomicReference;
  • массивами  — AtomicBooleanArray, AtomicIntegerArray, AtomicReferenceArray и др.;
  • аккумуляторами  — DoubleAccumulator, LongAccumulator;
  • обновлениями («апдейтерами»)  — AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater.

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

import java.util.concurrent.atomic.AtomicInteger;

class myThread extends Thread{
    public volatile AtomicInteger counter;
/* Для наглядности тестирования мы сделали поле волатильным.
   Данные не попадут в кэш и работу двух потоков будет легче отследить */

    myThread(AtomicInteger counter){
        this.counter = counter;
    }

    @Override
    public void run() {
        for(int i = 0; i < 1000; i++){
            counter.updateAndGet(n -> n + 2);
        }
// После работы счётчика в каждом потоке будем выводить значение:
        System.out.println(counter);
    }
}

Для операций над числами мы использовали метод updateAndGet() класса AtomicInteger. Этот метод увеличивает число на основе аргумента  — лямбда-выражения

Теперь посмотрим на всё это в действии. Создадим и запустим два потока myThread:

public class MultiCounter {
    public static void main(String[] args) {
        AtomicInteger myAtomicCounter = new AtomicInteger(0);

        // Создаём потоки
        myThread t1 = new myThread(myAtomicCounter);
        myThread t2 = new myThread(myAtomicCounter);

        t1.start();
        t2.start();
    }
}

В результате работы кода получим два значения: первое из них будет случайным числом в диапазоне от 2000 до 4000, а второе  — всегда 4000. Например, при первом запуске я получила результат:

2670

4000

Запустите код ещё раз и убедитесь, что меняется только первое значение. Второе число предсказуемо именно благодаря работе атомарного счётчика. Если бы мы использовали для счётчика обычный (неатомарный) Integer, второе число было бы случайным.

Блокирующие и неблокирующие алгоритмы в Java

Блокирующие алгоритмы парализуют работу потока  — навсегда или до момента, пока другой поток не выполнит нужное условие. Представьте, что поток A заблокирован и ждёт, когда поток B завершит операцию. Но тот не завершает, потому что заблокирован кем-то ещё. Случайно создать такую западню в приложении очень легко, а вот просчитать, когда она сработает  — трудно. Если без синхронизации можно обойтись, лучше обойдитесь — ради экономии нервов и ресурсов.

Неблокирующие алгоритмы тоже могут вести к проблемам, например, к live-lock. Это ситуация, когда несколько потоков буксуют:  продолжают работать, но без реального результата. Причиной может быть загруженность потоков сообщениями друг от друга. Чем больше сообщение, тем больше оно грузит память. Другая возможная причина  — неудачная реализация алгоритма, при которой в очереди возникает конфликт.  Поэтому начинающим лучше не мастерить велосипед, а сначала разобраться с чужими наработками.

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

Пройти обучение

12 апр 18, 12:14
0 0
Статистика 1
Показы: 1 Охват: 0 Прочтений: 0
Показаны все темы: 2

Последние комментарии

нет комментариев
Читать

Поиск по блогу

Люди

7 пользователям нравится сайт lena2018.mirtesen.ru