Статьи

Многопоточность — страх, неуверенность и сомнение

Прочитайте это: « Как объяснить, почему многопоточность сложна ».

Нам нужно поговорить. Это не так сложно.

Многопоточность сложна, только если вы делаете это плохо. Существует почти бесконечное количество способов сделать это плохо. Многие журналы и блоггеры решили, что многопоточность является следующей большой вещью (NBT ™). Для этого нам нужна новая, модная, дорогая языковая и библиотечная поддержка, и она нам нужна прямо сейчас.

Параллельные вычисления — это секрет следования закону Мура. Все эти дополнительные ядра останутся неиспользованными, если мы не сможем писать многопоточные приложения. И мы не можем писать многопоточные приложения, потому что — ну, есть много причин, разделенных между невежеством и высокомерием. Все это можно решить, бросая деньги за инструментами. Правильно?

высокомерие

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

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

Невежество

Лучший вид блокировки — это не мьютекс или семафор. Это, конечно, не СУБД (но, как известно, многие организации используют СУБД в качестве большой, медленной, сложной и дорогой очереди сообщений).

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

Очередь (читается с официальным «get») означает, что чтения не случайно игнорируются и перемещаются во время оптимизации. Кроме того, создание сложного объекта может быть выполнено одним потоком, который получает фрагменты данных из очереди, совместно используемой несколькими авторами. Нет блокировки на сложном объекте.

Использование очередей сообщений означает, что при получении данных для начала полезной работы не возникает странных условий гонки; get является атомарным и гарантированно имеет это свойство. Каждый поток получает локальный потокобезопасный объект. При передаче результата на следующий шаг в конвейере нет странного состояния гонки. Это помещено в очередь, где это доступно другому потоку.

Столовая Философы

Код Обеда Философов Ката имеет решение на основе очереди, которое довольно круто.

Очередь Форков может быть разделена между различными нитями Философов. Каждый Философ должен получить два ресурса Форка из очереди, съесть, философствовать, а затем снова поставить их в очередь. Он довольно короткий, его легко написать и легко продемонстрировать, что он должен работать.

Возможно, самая трудная вещь — это проектирование Столовой (также известной как Официант, Проводник или Пехотинец), которая позволяет только четырем из пяти философов обедать одновременно. Для этого уходящий Философ должен поставить себя в очередь «готового к употреблению», чтобы можно было сесть на следующий ожидающий Философ.

Решение на основе очередей восхитительно просто. 200 или около того строк кода, включая комментарии к документации, чтобы документация тоже выглядела хорошо.

Дополнительные ограничения

Самое простое решение использует одну очередь анонимных Форков. Общее ограничение состоит в том, чтобы каждый Философ использовал только две соседние вилки. Философ p может использовать вилки ( p +1 mod 5) и ( p- 1 mod 5).

Это приятно реализовывать. Философ просто снимает с вилки, проверяет позицию и повторно ставит ее в очередь, если это неправильная вилка.

FUD Factor

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

  1. Слишком много вопросов о StackOverflow, кажется, указывают на то, что медленная программа может волшебным образом стать быстрее, если каким-то образом потоки будут задействованы. Для программ, которые включают сканирование всего жесткого диска или загрузку Википедии или выполнение гигантского запроса SQL, количество потоков не имеет большого значения для реальной работы. Эти программы связаны с вводом / выводом; поскольку потоки должны совместно использовать ресурсы ввода-вывода содержащего процесса, многопоточность не поможет.
  2. Слишком много вопросов по StackOverflow, кажется, имеют простые решения для очереди сообщений. Но люди, кажется, начинают использовать неподходящую технологию. Просто научитесь пользоваться очередью сообщений. Двигаться дальше.
  3. Слишком много поставщиков инструментов (или языков) потворствуют (или создают) фактор FUD. Если программисты становятся достаточно опасными, неуверенными или сомнительными, они будут лоббировать тратить много денег на язык или пакет, который «решает» проблему.

Вздох. Ответ не программные инструменты, это дизайн. Разбейте проблему на независимые параллельные задачи и загрузите их из очередей сообщений. Соберите результаты в очереди сообщений.

Какой-то код

class Philosopher( threading.Thread ):
"""A Philosopher. When invited to dine, they will
cycle through their standard dining loop.

- Acquire two forks from the fork Queue
- Eat for a random interval
- Release the two forks
- Philosophize for a random interval

When done, they will enqueue themselves with
the "footman" to indicate that they are leaving.
"""
def __init__( self, name, cycles=None ):
"""Create this philosopher.

:param name: the number of this philosopher.
This is used by a subclass to find the correct fork.
:param cycles: the number of cycles they will eat.
If unspecified, it's a random number, u, 4 <= u < 7
"""
super( Philosopher, self ).__init__()
self.name= name
self.cycles= cycles if cycles is not None else random.randrange(4,7)
self.log= logging.getLogger( "{0}.{1}".format(self.__class__.__name__, name) )
self.log.info( "cycles={0:d}".format( self.cycles ) )
self.forks= None
self.leaving= None
def enter( self, forks, leaving ):
"""Enter the dining room. This must be done before the
thread can be started.

:param forks: The queue of available forks
:param leaving: A queue to notify the footman that they are
done.
"""
self.forks= forks
self.leaving= leaving
def dine( self ):
"""The standard dining cycle:
acquire forks, eat, release forks, philosophize.
"""
for cycle in range(self.cycles):
f1= self.acquire_fork()
f2= self.acquire_fork()
self.eat()
self.release_fork( f1 )
self.release_fork( f2 )
self.philosophize()
self.leaving.put( self )
def eat( self ):
"""Eating task."""
self.log.info( "Eating" )
time.sleep( random.random() )
def philosophize( self ):
"""Philosophizing task."""
self.log.info( "Philosophizing" )
time.sleep( random.random() )
def acquire_fork( self ):
"""Acquire a fork.

:returns: The Fork acquired.
"""
fork= self.forks.get()
fork.held_by= self.name
return fork
def release_fork( self, fork ):
"""Acquire a fork.

:param fork: The Fork to release.
"""
fork.held_by= None
self.forks.put( fork )
def run( self ):
"""Interface to Thread. After the Philosopher
has entered the dining room, they may engage
in the main dining cycle.
"""
assert self.forks and self.leaving
self.dine()

 Суть в том, чтобы метод dine был прямым выражением обеденного опыта Философа. Возможно, мы захотим переопределить  метод acqu_fork, чтобы разрешить разные стратегии получения форка.

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

 

Вилка, для сравнения, скучно.

class Fork( object ):
    """A Fork.  A Philosopher requires two of these to eat."""
    def __init__( self, name ):
        """Create the Fork.
        
        :param name: The number of this fork.  This may 
            be used by a Philosopher looking for the correct Fork.
        """
        self.name= name
        self.holder= None
        self.log= logging.getLogger( "{0}.{1}".format(self.__class__.__name__, name) )
    @property
    def held_by( self ):
        """The Philosopher currently holding this Fork."""
        return self.holder
    @held_by.setter
    def held_by( self, philosopher ):
        if philosopher:
            self.log.info( "Acquired by {0}".format( philosopher ) )
        else:
            self.log.info( "Released by {0}".format( self.holder ) )
        self.holder= philosopher

Таблица, однако, интересна. Он включает в себя специальную очередь «ухода», которая не является надлежащей частью проблемной области, но является частью этого конкретного решения.

class Table( object ):
    """The dining Table.  This uses a queue of Philosophers
    waiting to dine and a queue of forks.
    
    This sets Philosophers, allows them to dine and then
    cleans up after each one is finished dining.
    
    To prevent deadlock, there's a limit on the number
    of concurrent Philosophers allowed to dine.
    """
    def __init__( self, philosophers, forks, limit=4 ):
        """Create the Table.
        :param philosophers: The queue of Philosophers waiting to dine.
        :param forks: The queue of available Forks.
        :param limit: A limit on the number of concurrently dining Philosophers.
        """
        self.philosophers= philosophers
        self.forks= forks
        self.limit= limit
        self.leaving= Queue.Queue()
        self.log= logging.getLogger( "table" )
    def dinner( self ):
        """The essential dinner cycle:
        admit philosophers (to the stated limit);
        as philosophers finish dining, remove them and admit more;
        when the dining queue is empty, simply clean up.
        """
        self.at_table= self.limit
        while not self.philosophers.empty():
            while self.at_table != 0:
                p= self.philosophers.get()
                self.seat( p )
            # Must do a Queue.get() to wait for a resource
            p= self.leaving.get()
            self.excuse( p )
        assert self.philosophers.empty()
        while self.at_table != self.limit:
            p= self.leaving.get()
            self.excuse( p )
        assert self.at_table == self.limit
    def seat( self, philosopher ):
        """Seat a philosopher.  This increments the count 
        of currently-eating Philosophers.
        
        :param philosopher: The Philosopher to be seated.
        """
        self.log.info( "Seating {0}".format(philosopher.name) )
        philosopher.enter( self.forks, self.leaving)
        philosopher.start()
        self.at_table -= 1 # Consume a seat
    def excuse( self, philosopher ):
        """Excuse a philosopher.  This decrements the count 
        of currently-eating Philosophers.
        
        :param philosopher: The Philosopher to be excused.
        """
        philosopher.join() # Cleanup the thread
        self.log.info( "Excusing {0}".format(philosopher.name) )
        self.at_table += 1 # Release a seat

Метод обеда гарантирует, что все Философы едят, пока они не закончат. Это также гарантирует, что четыре философа сидят за столом, и когда один заканчивает, другой занимает их место. Наконец, это также гарантирует, что все философы закончили есть до закрытия столовой.

От http://slott-softwarearchitect.blogspot.com/2011/06/multithreading-fear-unterminty-and.html