{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "3f098f8d",
   "metadata": {},
   "source": [
    "# Создание потоков #"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a3903d26",
   "metadata": {},
   "source": [
    "Итак, на прошлой лекции мы обсудили процессы. На этой лекции мы поговорим о потоках. Мы обсудим создание потоков при помощи модуля `threading` и использование класса `ThreadPoolExecutor`. С прикладной точки зрения поток целиком и полностью напоминает процесс. Он имеет свою последовательность инструкций для исполнения, у каждого потока есть свой собственный стек, но все потоки выполняются в рамках одного процесса. Этим они отличаются.\n",
    "\n",
    "Если мы говорили о процессах и у каждого процесса были свои ресурсы и память, то все созданные потоки разделяют память процесса и все его ресурсы. Управлением и выполнением потоков занимается операционная система. Но в Python есть свои ограничения для потоков. Их мы обсудим отдельно. Итак, давайте рассмотрим пример, как создать поток на Python."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cc38c882",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Создание потока\n",
    "from threading import Thread\n",
    "\n",
    "def f(name):\n",
    "    print(f\"hello {name}\")\n",
    "    \n",
    "th = Thread(target=f, args=(\"Bob\",))\n",
    "th.start()\n",
    "th.join()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d0b9bf31",
   "metadata": {},
   "source": [
    "Всё делается очень просто и похоже на создание процессов. Используем модуль `threading`, импортируем класс `Thread`. Далее мы объявляем функцию, которую хотим исполнить в отдельном потоке функции `f`, которая просто выводит `hello` и аргумент, который ей передали. Далее мы создаем объект класса `Thread`, передаем в него нашу функцию `f` в качестве параметров и аргументы, с которыми эта функция должна будет быть вызвана. Опять же, после того как мы создали объект, никакого потока запущено не будет. Поток будет запущен, после того как мы вызовем метод `start` у этого объекта. Также очень важно дожидаться выполнения завершения всех созданных потоков при помощи метода `join` у этого объекта. Если мы выполним пример в консоли с этого слайда, то получим вывод `hello Bob`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "945c9899",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "hello Bob\r\n"
     ]
    }
   ],
   "source": [
    "! python ex7.py"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c1d72475",
   "metadata": {},
   "source": [
    "Существует также альтернативный метод создания потока при помощи наследования."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "98d9c32c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Создание потока\n",
    "from threading import Thread\n",
    "\n",
    "class PrintThread(Thread):\n",
    "    def __init__(self, name):\n",
    "        super().__init__()\n",
    "        self.name = name\n",
    "        \n",
    "    def run(self):\n",
    "        print(f\"hello {self.name}\")\n",
    "        \n",
    "th = PrintThread(\"Mike\")\n",
    "th.start()\n",
    "th.join()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5d3931d3",
   "metadata": {},
   "source": [
    "Опять же, всё очень похоже на использование модуля `multiprocessing`, объявляем свой класс, наследуемся от класса `Thread`, в конструктор передаем нужные аргументы, для того чтобы выполнить нашу функцию в отдельном потоке. Запоминаем их в `self`. Далее мы переопределяем метод `run`, и метод `run` уже будет выполнен в отдельном потоке управления. В этом методе run мы можем использовать переданные аргументы конструктору, которые мы запомнили в `self`. Далее создаем объект класса `PrintThread`, передаем туда нужные аргументы, в качестве примера я передал строку `Mike`. Далее вызываем метод `start`. Здесь всё то же самое. Метод `start` создаст поток, и при помощи `join` мы будем ждать, пока этот поток завершится в основном потоке. Если мы исполним этот пример, то увидим вывод `hello Mike`. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "9cf33666",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "hello Mike\r\n"
     ]
    }
   ],
   "source": [
    "! python ex8.py "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "34ff1df1",
   "metadata": {},
   "source": [
    "Итак, вы можете использовать любой из приведенных способов, всё зависит от ваших предпочтений, любите ли вы наследование либо просто любите передавать функции как аргументы.\n",
    "\n",
    "В Python3 появился очень удобный класс для создания пула потоков. Называется он `ThreadPoolExecutor` модуль `concurrent.futures`. Предположим, у нас есть некий массив чисел, и нам нужно ограничить количество потоков и рассчитать квадраты чисел для этого массива."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "58346784",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Пул потоков, concurrent.futures.Future\n",
    "from concurrent.futures import ThreadPoolExecutor, as_completed\n",
    "\n",
    "def f(a):\n",
    "    return a * a\n",
    "\n",
    "# .shutdown() in exit\n",
    "with ThreadPoolExecutor(max_workers=3) as pool:\n",
    "    results = [pool.submit(f, i) for i in range(10)]\n",
    "    \n",
    "    for future in as_completed(results):\n",
    "        print(future.result())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7c473795",
   "metadata": {},
   "source": [
    "Для этого можно использовать контекстный менеджер, указать в нем вызов `ThreadPoolExecutor` с параметром `max_workers`, который как раз отвечает за максимальное количество потоков, которые будут созданы в этом блоке `with`. Не нужно заботиться о завершении потоков. Нужное количество потоков будет создано автоматически, и при завершении контекстного менеджера будет вызвана функция `shutdown`, которая дождется завершения всех созданных потоков. Основная функция у этого `ThreadPoolExecutor` — это метод `submit`. Метод `submit` создает объект класса `concurrent.futures` `Future` — это такой объект, который еще не завершился, но выполняется и будет завершен в будущем. При помощи удобного метода `as_completed` из этого модуля `concurrent.futures` мы можем дождаться завершения всех наших объектов и получить результаты по мере завершения всех этих потоков, созданных нашим `Executor`. Если мы запустим пример, мы опять получим вывод, похожий на слайд."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "a3ff48d3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1\r\n",
      "4\r\n",
      "0\r\n",
      "9\r\n",
      "16\r\n",
      "25\r\n",
      "36\r\n",
      "49\r\n",
      "64\r\n",
      "81\r\n"
     ]
    }
   ],
   "source": [
    "! python ex9.py"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4f9461cd",
   "metadata": {},
   "source": [
    "Итак, на этой лекции мы обсудили, что такое поток. Мы рассмотрели особенности создания потоков при помощи модуля `threading`. Ключевое отличие потоков от процессов состоит в том, что они разделяют память и все ресурсы процессов, в рамках которого они запущены. Также мы рассмотрели работу класса `ThreadPoolExecutor` и обсудили, как можно ограничить количество потоков для решения конкретной задачи. \n",
    "\n",
    "На следующей лекции мы рассмотрим вопрос синхронизации потоков и блокировки."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
