Потоковая безопасность

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

Очень важная особенность работы с потоками: на небольшом количестве потоков и небольших тестовых задачах «все работает». Например, вывод информации с помощью print, при подключении к 20 устройствам в 5 потоков, будет работать нормально. А при подключении к большому количеству устройств с большим количеством потоков окажется, что иногда сообщения «налазят» друг на друга. Такая особенность проявляется очень часто, поэтому не доверяйте варианту когда «все работает» на базовых примерах, соблюдайте правила работы с потоками.

Прежде чем разбираться с правилами, надо разобраться с термином «потоковая безопасность». Потоковая безопасность - это концепция, которая описывает работу с многопоточными программами. Код считается потокобезопасным (thread-safe), если он может работать нормально при использовании нескольких потоков.

Например, функция print не является потокобезопасной. Это проявляется в том, что когда код выполняет print из разных потоков, сообщения на стандартном потоке вывода могут смешиваться. Может выводиться сначала часть сообщения из одного потока, потом часть из второго, потом часть из первого и так далее. То есть, функция print не работает нормально (как положено) в потоках. В этом случае говорят, что функция print не является потокобезопасной (not thread-safe).

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

Примечание

С print проблемы потому что из разных потоков пишем в один стандартный поток вывода, а print не потокобезопасен.

В том случае, если надо все же из разных потоков писать в один и тот же ресурс, есть два варианта:

  1. Писать в один и тот же ресурс после того как работа в потоке закончилась. Например, в потоках 1, 2 и 3 выполнилась функция, ее результат по очереди (последовательно) получен из каждого потока, а затем записан в файл.
  2. Использовать потокобезопасную альтернативу (не всегда доступна и/или не всегда простая). Например, вместо функции print использовать модуль logging.

Рекомендации при работе с потоками:

  1. Не пишите в один и тот же ресурс из разных потоков, если ресурс или то, чем пишете не предназначено для многопоточной работы. Выяснить это, проще всего, погуглив что-то вроде «python write to file from threads».
  • В этой рекомендации есть нюансы. Например, можно писать из разных потоков в один и тот же файл, если использовать Lock или использовать потокобезопасную очередь. Эти варианты, чаще всего, непросты в использовании и не рассматриваются в книге. Скорее всего, 95% задач, с которыми вы будете сталкиваться, можно решить без них.
  • К этой категории относится запись/изменение списков/словарей/множеств из разных потоков. Сами по себе эти объекты потокобезопасны, но нет гарантии, что при изменении с одного и того же списка из разных потоков, данные в списке будут правильные. В случае, если надо использовать общий контейнер для данных для разных потоков, надо использовать очередь queue из модуля Queue. Она потокобезопасна и с ней можно работать из разных потоков.
  1. Если есть воможность, избегайте коммуникаций между потоками в процессе их работы. Это непростая задача и лучше постараться обойтись без нее.
  2. Соблюдайте принцип KISS (Keep it simple, stupid) - постарайтесь, чтобы решение было максимально простым.

Примечание

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

Модуль concurrent.futures, который будет рассматриваться дальше, упрощает соблюдение первого принципа «Не пишите в один и тот же ресурс из разных потоков…». Сам интерфейс работы с модулем к этому подталкивает, но конечно не запрещает его нарушать.

Однако, перед знакомством с concurrent.futures, надо рассмотреть основы работы с модулем logging. Он будет использоваться вместо функции print, которая не является потокобезопасной.