Быстрый в изучении - мощный в программировании
>> Telegram ЧАТ для Python Программистов

Свободное общение и помощь советом и решением проблем с кодом! Заходите в наш TELEGRAM ЧАТ!

>> Python Форум Помощи!

Мы создали форум где отвечаем на все вопросы связанные с языком программирования Python. Ждем вас там!

>> Python Канал в Telegram

Обучающие статьи, видео и новости из мира Python. Подпишитесь на наш TELEGRAM КАНАЛ!

Модуль threading на примерах

10 сентября 2017 г. Archy Просмотров: 174190 RSS 11
Python для начинающих » Общие вопросы ,

Модуль threading впервые был представлен в Python 1.5.2 как продолжение низкоуровневого модуля потоков. Модуль threading значительно упрощает работу с потоками и позволяет программировать запуск нескольких операций одновременно. Обратите внимание на то, что потоки в Python лучше всего работают с операциями I/O, такими как загрузка ресурсов из интернета или чтение файлов и папок на вашем компьютере.

Если вам нужно сделать что-то, для чего нужен интенсивный CPU, тогда вам, возможно, захочется взглянуть на модуль multiprocessing, вместо threading. Причина заключается в том, что Python содержит Global Interpreter Lock (GIL), который запускает все потоки внутри главного потока. По этой причине, когда вам нужно запустить несколько интенсивных операций с потоками, вы заметите, что все работает достаточно медленно. Так что мы сфокусируемся на том, в чем потоки являются лучшими: операции I/O.

Небольшое интро

Поток позволяет вам запустить часть длинного кода так, как если бы он был отдельной программой. Это своего рода вызов наследуемого процесса, за исключением того, что вы вызываете функцию или класс, вместо отдельной программы. Я всегда находил конкретные примеры крайне полезными. Давайте взглянем на нечто совершенно простое:

import threading
 
def doubler(number):
    """
    A function that can be used by a thread
    """
    print(threading.currentThread().getName() + '\n')
    print(number * 2)
    print()
 
if __name__ == '__main__':
    for i in range(5):
        my_thread = threading.Thread(target=doubler, args=(i,))
        my_thread.start()

Здесь мы импортируем модуль threading и создаем обычную функцию под названием doubler. Наша функция принимает значение и удваивает его. Также она выводи название потока, который вызывает функцию и выводит бланк-строчку в конце. Далее, в последнем блоке кода, мы создаем пять потоков, и запускаем каждый из них по очереди.

Вам больше не придется долгими часами настраивать дорогостоящую рекламу в Фейсбуке и тратить на нее весомый бюджет. Сайт Avi1 готов помочь Вам купить ее по самым приятным ценам, и как можно быстрее развиться в данной социальной сети. Здесь Вам будут доступны пакетные предложения с разнообразными ресурсами: лайки, подписчики, друзья и пр.

Используя многопоточность можно решить много рутинных моментов. Например загрузка видео или другого материала в социальные сети, такие как Youtube или Facebook. Для развития своего Youtube канала можно использовать https://publbox.com/ru/youtube который возьмет на себя администрирование вашего канала. Youtube отличный источник заработка и чем больше каналов тем лучше. Без Publbox вам не обойтись.

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

Thread-1
 
0
 
Thread-2
 
2
 
Thread-3
 
4
 
Thread-4
 
6
 
Thread-5
 
8

Конечно, вам скорее всего не захочется выводить вашу выдачу в stdout. Это может закончиться сильным беспорядком. Вместо этого, вам нужно использовать модуль Python под названием logging. Это защищенный от потоков модуль и он прекрасно выполняет свою работу. Давайте немного обновим указанный ранее пример и добавим модуль logging, и заодно назовем наши потоки:

import logging
import threading
 
def get_logger():
    logger = logging.getLogger("threading_example")
    logger.setLevel(logging.DEBUG)
 
    fh = logging.FileHandler("threading.log")
    fmt = '%(asctime)s - %(threadName)s - %(levelname)s - %(message)s'
    formatter = logging.Formatter(fmt)
    fh.setFormatter(formatter)
 
    logger.addHandler(fh)
    return logger
 
 
def doubler(number, logger):
    """
    A function that can be used by a thread
    """
    logger.debug('doubler function executing')
    result = number * 2
    logger.debug('doubler function ended with: {}'.format(result))
 
 
if __name__ == '__main__':
    logger = get_logger()
    thread_names = ['Mike', 'George', 'Wanda', 'Dingbat', 'Nina']
    for i in range(5):
        my_thread = threading.Thread(
            target=doubler, name=thread_names[i], args=(i,logger))
        my_thread.start()

Самое большое изменение в этом коде – это добавление функции get_logger. Эта часть кода создаст логгер, который настроен на дебаг. Это сохранит log в нынешнюю рабочую папку (другими словами, туда, откуда запускается скрипт) и затем мы настраиваем формат каждой линии на логированный. Формат включает временной штамп, название потока, уровень логгирования и логгированое сообщение. В функции doubler мы меняем наши операторы вывода на операторы логгирования.

Обратите внимание на то, что мы передаем логгер в функцию doubler, когда создаем поток. Это связанно с тем, что если вы определяет объект логгирования в каждом потоке, вы получите несколько singletons и ваш журнал будет содержать множество повторяющихся строк. В конце мы называем наши потоки, создав список наименований, и затем устанавливаем каждый поток на особое наименование, использую параметр name. Когда вы запустите этот код, вы должны получить лог файл со следующим содержимым:

Эта выдача достаточно понятная, так что давайте пойдем дальше. Я хочу разобрать еще один вопрос в этой статье. Мы поговорим о наследовании класса под названием threading.Thread. Давайте снова рассмотрим предыдущий пример, только вместо вызова потока напрямую, мы создадим свой собственный подкласс. Вот обновленный код:

import logging
import threading
 
class MyThread(threading.Thread):
 
    def __init__(self, number, logger):
        threading.Thread.__init__(self)
        self.number = number
        self.logger = logger
 
 
    def run(self):
        """
        Run the thread
        """
        logger.debug('Calling doubler')
        doubler(self.number, self.logger)
 
 
    def get_logger():
        logger = logging.getLogger("threading_example")
        logger.setLevel(logging.DEBUG)
 
        fh = logging.FileHandler("threading_class.log")
        fmt = '%(asctime)s - %(threadName)s - %(levelname)s - %(message)s'
        formatter = logging.Formatter(fmt)
        fh.setFormatter(formatter)
 
        logger.addHandler(fh)
        return logger
 
 
def doubler(number, logger):
    """
    A function that can be used by a thread
    """
    logger.debug('doubler function executing')
    result = number * 2
    logger.debug('doubler function ended with: {}'.format(result))
 
 
if __name__ == '__main__':
    logger = get_logger()
    thread_names = ['Mike', 'George', 'Wanda', 'Dingbat', 'Nina']
    for i in range(5):
        thread = MyThread(i, logger)
        thread.setName(thread_names[i])
        thread.start()

В этом примере мы только что унаследовали класс threading.Thread. Мы передали число, которое хотим удвоить, а также передали объект логгированмя, как делали это ранее. Но на этот раз, мы настроим название потока по-другому, вызвав функцию setName в объекте потока. Нам все еще нужно вызвать старт в каждом потоке, но запомните, что нам не нужно определять это в наследуемом классе. Когда вы вызываете старт, он запускает ваш поток, вызывая метод run. В нашем классе мы вызываем функцию doubler для выполнения наших вычислений. Выдача сильно похожа на ту, что была в примере ранее, за исключением того, что я добавил дополнительную строку в выдаче. Попробуйте сами и посмотрите, что получится.

Замки и Синхронизация

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

Решение проблемы – это использовать замки. Замок предоставлен модулем Python threading и может держать один поток, или не держать поток вообще. Если поток пытается acquire замок на ресурсе, который уже закрыт, этот поток будет ожидать до тех пор, пока замок не откроется. Давайте посмотрим на практичный пример одного кода, который не имеет никакого замочного функционала, но мы попробуем его добавить:

import threading
 
total = 0
 
def update_total(amount):
    """
    Updates the total by the given amount
    """
    global total
    total += amount
    print (total)
if __name__ == '__main__':
    for i in range(10):
        my_thread = threading.Thread(target=update_total, args=(5,))
        my_thread.start()

Мы можем сделать этот пример еще интереснее, добавив вызов time.sleep. Следовательно, проблема здесь в том, что один поток может вызывать update_total и перед тем, как он обновится, другой поток может вызвать его и тоже попытается обновить его. В зависимости от порядка операций, значение может быть добавлено единожды. Давайте добавим замок к функции. Существует два способа сделать эта. Первый – это использование try/finally, если мы хотим убедиться, что замок снят. Вот пример:

import threading
 
total = 0
lock = threading.Lock()
 
def update_total(amount):
    """
    Updates the total by the given amount
    """
    global total
    lock.acquire()
 
    try:
        total += amount
    finally:
        lock.release()
 
    print (total)
 
 
if __name__ == '__main__':
    for i in range(10):
        my_thread = threading.Thread(target=update_total, args=(5,))
        my_thread.start()

Здесь мы просто вешаем замок, перед тем как сделать что-либо другое. Далее, мы пытаемся обновить total и finally, мы снимаем замок и выводим нынешний total. Мы можем упростить данную задачу, используя оператор Python под названием with:

import threading
 
total = 0
lock = threading.Lock()
 
def update_total(amount):
    """
    Updates the total by the given amount
    """
    global total
    with lock:
        total += amount
 
    print (total)
 
 
if __name__ == '__main__':
    for i in range(10):
        my_thread = threading.Thread(target=update_total, args=(5,))
        my_thread.start()

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

import threading
 
total = 0
lock = threading.Lock()
 
def do_something():
    lock.acquire()
 
    try:
        print('Lock acquired in the do_something function')
    finally:
        lock.release()
        print('Lock released in the do_something function')
 
    return "Done doing something"
 
 
def do_something_else():
    lock.acquire()
 
    try:
        print('Lock acquired in the do_something_else function')
    finally:
        lock.release()
        print('Lock released in the do_something_else function')
 
    return "Finished something else"
 
 
if __name__ == '__main__':
    result_one = do_something()
    result_two = do_something_else()

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

import threading
 
total = 0
lock = threading.RLock()
 
def do_something():
    with lock:
        print('Lock acquired in the do_something function')
 
    print('Lock released in the do_something function')
 
    return "Done doing something"
 
 
def do_something_else():
    with lock:
        print('Lock acquired in the do_something_else function')
 
    print('Lock released in the do_something_else function')
    return "Finished something else"
 
 
def main():
    with lock:
        result_one = do_something()
        result_two = do_something_else()
 
    print (result_one)
    print (result_two)
 
 
if __name__ == '__main__':
    main()

Когда вы запустите этот код, вы увидите, что он просто висит. Причина в том, что мы просто указываем модулю threading повесить замок. Так что когда мы вызываем первую функцию, она видит, что замок уже висит и блокируется. Это будет длиться до тех пор, пока замок не снимут, что никогда и не случится, так как это не предусмотрено в коде. Хорошее решение в данном случае – использовать re-entrant замок. Модуль threading предоставляет такой, в виде функции RLock. Просто замените строку lock = threading.Lock() на lock = threading.RLock() и попробуйте перезапустить код. Теперь он должен заработать. Если вы хотите попробовать код выше но добавить в него потоки, то мы можем заменить call на main следующим образом:

if __name__ == '__main__':
    for i in range(10):
        my_thread = threading.Thread(target=main)
        my_thread.start()

Так мы запустим основную функцию в каждом потоке, что в свою очередь приведет к вызову остальных двух функций. В конце вы получите достаточно крупную выдачу.

Таймеры

Модуль threading включает в себя один очень удобный класс, под названием Timer, который вы можете использовать запуска действия, спустя определенный отрезок времени. Данный класс запускает собственный поток и начинают работу с того же метода start(), как и обычные потоки. Вы также можете остановить таймер, используя метод cancel. Обратите внимание на то, что вы можете отменить таймер еще до того, как он стартовал. Однажды у меня был случай, когда мне нужно было наладить связь с под-процессом, который я начал, но мне нужен было обратный отсчет. Несмотря на существования ряда различных способов решения этой отдельной проблемы, моим любимым решением всегда было использование класса Timer модуля threading. Для этого примера мы взглянем на применение команды ping. В Linux, команда ping будет работать, пока вы её не убьете. Так что класс Timer становится особенно полезным для мира Linux. Вот пример:

import subprocess
 
from threading import Timer
 
kill = lambda process: process.kill()
cmd = ['ping', 'www.google.com']
ping = subprocess.Popen(
    cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
my_timer = Timer(5, kill, [ping])
 
try:
    my_timer.start()
    stdout, stderr = ping.communicate()
finally:
    my_timer.cancel()
print (str(stdout))

Здесь мы просто настраиваем лямбду, которую мы можем использовать, чтобы убить процесс. Далее мы начинаем нашу работу над ping и создаем объект Timer. Обратите внимание на то, что первый аргумент – это время ожидания в секундах, затем – функция, которую нужно вызвать и аргумент, который будет передан функции. В нашем случае, наша функция – это лямбда, и мы передаем её список аргументов, где список содержит только один элемент. Если вы запустите этот код, он будет работать примерно 5 секунд, после чего выведет результат пинга.

Другие Компоненты Потоков

Модуль threading также включает в себя поддержу других объектов. Например, вы можете создать семафор, который является одним из древнейших синхронизационных примитивов в компьютерной науке. Семафор управляет внутренним счетчиком, который будет декрементируется когда вы вызываете acquire и увеличивается, когда вы вызываете release. Счетчик разработан таким образом, что он не может падать ниже нуля. Так что если так вышло, что вы вызываете acquire когда он равен нулю, то он заблокируется.

Еще один полезный инструмент, который содержится в модуле, это Event. С его помощью вы можете получить связь между двумя потоками, используя сигналы. Мы рассмотрим примеры применения Event в следующей статье. Наконец-то, в версии Python 3.2 был добавлен объект Barrier. Это примитив, который управляет пулом потока, при этом не важно, где потоки должны ждать своей очереди. Для передачи барьера, потоку нужно вызвать метод wait(), который будет блокировать до тех пор, пока все потоки не сделают вызов. После чего все потоки пройдут дальше одновременно.

Связь потоков

Существует ряд случаев, когда вам нужно сделать так, чтобы потоки были связанны друг с другом. Как я упоминал ранее, вы можете использовать Event для этой цели. Но более удобный способ – использовать Queue. В нашем примере мы используем оба способа! Давайте посмотрим, как это будет выглядеть:

import threading
from queue import Queue
 
 
def creator(data, q):
    """
    Creates data to be consumed and waits for the consumer
    to finish processing
    """
    print('Creating data and putting it on the queue')
    for item in data:
        evt = threading.Event()
        q.put((item, evt))
        print('Waiting for data to be doubled')
        evt.wait()
 
 
def my_consumer(q):
    """
    Consumes some data and works on it
    In this case, all it does is double the input
    """
    while True:
        data, evt = q.get()
        print('data found to be processed: {}'.format(data))
        processed = data * 2
 
        print(processed)
 
        evt.set()
        q.task_done()
 
 
if __name__ == '__main__':
    q = Queue()
    data = [5, 10, 13, -1]
    thread_one = threading.Thread(target=creator, args=(data, q))
    thread_two = threading.Thread(target=my_consumer, args=(q,))
 
    thread_one.start()
    thread_two.start()
 
    q.join()

Давайте немного притормозим. Во первых, у нас есть функция creator (также известная, как producer), которую мы используем для создания данных, с которыми мы хотим работать (или использовать). Далее мы получаем еще одну функцию, которую мы используем для обработки данных, под названием my_consumer. Функция creator использует метод Queue под названием put, чтобы добавить данные в очередь, затем потребитель, в свою очередь, будет проверять, есть ли новые данные и обрабатывать их, когда такие появятся. Queue обрабатывает все закрытия и открытия замков, так что лично вам эта участь не грозит.

В данном примере мы создали список значений, которые мы хотим дублировать. Далее мы создаем два потока, один для функции creator/producer, второй для consumer (потребитель). Обратите внимание на то, что мы передаем объект Queue каждому потоку, что является прямо таки магией, учитывая то, как обрабатываются замки. Очередь начнется с первого потока, который передает данные второму. Когда первый поток передает те или иные данные в очередь, он также передает их к Event, после чего дожидается, когда произойдет события, чтобы закончить. Далее, в функции consumer, данные обрабатываются, и после этого вызывается метод настройки Event, который указывает первому потоку, что второй закончил обработку, так что он может продолжать. Последняя строка кода вызывает метод join объекта Queue, который указывает Queue подождать, пока потоки закончат обработку. Первый поток заканчивает, когда ему больше нечего передавать в Queue.

Подведем итоги

Мы рассмотрели достаточно много материала. А именно:

  1. Основы работы с модулем threading
  2. Как работают замки
  3. Что такое Event и как его можно использовать
  4. Как использовать таймер
  5. Внутрипотоковая связь с использованием Queue/Event

Теперь вы знаете, как использовать потоки, и в чем они хороши. Надеюсь, вы найдете им применение в своем собственном коде!

Комментариев: 11
  1. А не могли бы вы привести пример для функции,в поток её запустить?функция в коде окна виджета def Start

    from PyQt5 import QtWidgets

    from PyQt5 import QtWidgets as QtGui

    from samplestrorint import *

    import sys

    import strorint1

    def log_uncaught_exceptions(ex_cls, ex, tb):

    text = '{}: {}:\n'.format(ex_cls.__name__, ex)

    import traceback

    text += ''.join(traceback.format_tb(tb))

    print(text)

    QMessageBox.critical(None, 'Error', text)

    quit()

    sys.excepthook = log_uncaught_exceptions

    class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, parent=None):

    QtWidgets.QWidget.__init__(self, parent)

    self.ui = Ui_MainWindow()

    self.ui.setupUi(self)

    self.statusBar().showMessage('Stop')

    self.ui.pushButton.clicked.connect(self.Start)

    self.ui.pushButton_2.clicked.connect(self.Stop)

    self.ui.pushButton.clicked.connect(lambda: self.statusBar().showMessage('запущен'))

    self.ui.pushButton_2.clicked.connect(lambda: self.statusBar().showMessage('остановлен'))

    def Start(self):

    r = self.ui.textEdit_9.toPlainText()

    strorint1.r = int(r)

    def Stop(self):

    strorint1.stop()

    if __name__=="__main__":

    app = QtWidgets.QApplication(sys.argv)

    myapp = MainWindow()

    myapp.show()

    sys.exit(app.exec_())

  2. Виталий | 2018-08-31 в 12:56:04

    Для PyQt потоки реализуются с помощью QtCore.QThread, нет необходимости использовать threading напрямую.

    Модуль threading юзается для консольных приложений и tkinter, ну возможно для иных фреймворков GUI где нет родной реализации потоков.

  3. Виталий | 2018-08-31 в 13:06:00

    Автору статьи - большое спасибо, информация очень полезна, особенно на родном языке)

    У вас в третьем примере опечатка - функция get_logger() находится в подклассе MyThread, при такой реализации при запуске скрипта вы не сможете получить переменную logger. Видимо вы данную функцию "затабулировали" случайно в этот подкласс.

  4. ООО! Спасибо, давно искал на русском, так как только учу англ., сейчас засяду изучать ))) вот вам тоже ссылочка по теме, только оттуда https://writeabout.tech/programming/learning-to-create-python-multi-threaded-and-multi-process/

  5. VarangaOfficial - варанга в аптеке цена спб - все, что нужно знать об этом препарате. Воспользовавшись нашим порталам, вы получите возможность узнать обстоятельную информацию касательно этого натурального лекарственного комплекса. Лично увидеть данные о клиническом тестировании геля, прочитать реальные отзывы пользователей и медицинского персонала. Изучить инструкцию по использованию, прочесть особенности и методы работы мази, осмыслить, в чем заключаются особенности работы крема Варанга, где можно приобрести сертифицированный, оригинальный препарат и, как избежать покупки подделки. Мы тщательно проверяем размещаемые данные. Предоставляем пользователям нашего ресурса сведения, взятые только из авторитетных источников. Если вы обнаружили у себя признаки грибкового поражения стоп или уже довольно продолжительное время, без ощутимых результатов пытаетесь излечиться от этого коварного, неприятного недуга, на нашем сайте вы отыщете легкий и быстрый способ устранения проблемы. Приобщайтесь и живите полноценной, здоровой жизнью. Благодаря нам, все ответы на самые волнующие вопросы, теперь собраны в одном месте на удобной в использовании и высоко информационном ресурсе.

  6. odżywki na siłe

  7. Вы следите за прогнозом погоды каждый день? Как Вы узнаете что будет завтра с погодой? Прогноз погоды в Украине https://www.gismeteo.ua/ недооценена!

  8. курс догкоина сегодня

  9. Мед – сладкий вязкий продукт, который вырабатывают пчелы. Он богат витаминами, полезными микроэлементами, и является хорошим источником энергии. Купить натуральный мед от лучших украинских производителей по доступной цене можно в онлайн магазине Веселый Шершень Заказать акациевый мед в Житомире https://shop.med-na-dom.com/kupit-med-v-ukrainie/med-s-akacii/ Мы предоставим исчерпывающие и профессиональные ответы на все вопросы, поможем определиться с выбором меда и оформим заказ в считанные минуты.

  10. Магазин питания для спорта, официальный веб-сайт которого доступен по адресу: SportsNutrition-24.Com, реализует обширный выбор спортивного питания, которые принесут пользу и заслуги как проф спортсменам, так и любителям. Интернет-магазин осуществляет свою деятельность большой промежуток времени, предоставляя клиентам со всей Рф качественное питание для спорта, а также витамины и особые препараты - https://sportsnutrition-24.com/aktivnoe-dolgoletie/. Спортпит представляет собой категорию продуктов, которая призвана не только лишь сделать лучше спортивные заслуги, но и благоприятно влияет на здоровье организма. Подобное питание вводится в повседневный рацион с целью получения микро- и макроэлементов, витаминов, аминокислот и белков, а помимо этого прочих недостающих веществ. Не секрет, что организм спортсмена в процессе наращивания мышечной массы и адаптации к повышенным нагрузкам, остро нуждается в должном количестве полезных веществ. При этом, даже правильное питание и употребление растительной, а кроме этого животной пищи - не гарантирует того, что организм получил нужные аминокислоты или белки. Чего нельзя сказать о высококачественном питании для спорта. Об ассортименте товаров Интернет-магазин "SportsNutrition-24.Com" продает качественную продукцию, которая прошла ряд проверок и получила сертификаты качества. Посетив магазин, заказчики смогут найти для себя товары из следующих категорий: - L-карнитинг (Л-карнитин) представляет собой вещество, схожее витамину B, синтез которого осуществляется в организме; - гейнеры, представляющие из себя, белково-углеводные консистенции; - BCAA - средства, содержащие в собственном составе три важные аминокислоты, стимулирующие рост мышечной массы; - протеин - чистый белок, употреблять который вы можете в виде коктейлей; - различные аминокислоты; - а также ряд прочих товаров (нитробустеры, жиросжигатели, специальные препараты, хондропротекторы, бустеры гормона роста, тестобустеры и все остальное). Об оплате и доставке Интернет-магазин "SportsNutrition-24.Com" предлагает большое обилие товаров, которое полностью способно удовлетворить проф и начинающих спортсменов, включая любителей. Большой опыт дозволил компании сделать связь с наикрупнейшими поставщиками и производителями питания для спорта, что позволило сделать политику цен гибкой, а цены - демократичными! К примеру, аминокислоты или гейнер заказать можно по стоимости, которая на 10-20% ниже, чем у конкурентов. Оплата возможна как наличным, так и безналичным расчетом. Магазин предлагает широкий выбор способов оплаты, включая оплату разными электронными платежными системами, а также дебетовыми и кредитными картами. Главный офис организации размещен в Санкт-Петербурге, однако доставка товаров осуществляется во все населенные пункты РФ. Кроме самовывоза, получить товар можно при помощи любой транспортной фирмы, найти которую каждый клиент может в личном порядке.

  11. Благодаря массовой доступности интернета, стало совсем не обязательно посещать кинотеатры или ожидать трансляции любимого сериала по ТВ, смотреть сериалы 2022. Интернет-портал, находящийся по адресу: HDserialclub.Net, открывает каждому пользователю доступ к тысячам любимых кинофильмов и сериалов!