Всё про конкурентность в Go — Transcript

Обзор конкурентности в Go: отличие от параллелизма, гарутины, планировщик и синхронизация потоков.

Key Takeaways

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

Summary

  • Go — язык, созданный для эффективных конкурентных и параллельных вычислений на многоядерных процессорах.
  • Различие между конкурентностью (логический уровень) и параллелизмом (физический уровень) с примерами из жизни.
  • Пояснение, как работает многозадачность на одном ядре с переключением между задачами.
  • В Go вместо потоков используются легковесные гарутины, которые управляются планировщиком языка, а не ОС.
  • Асинхронность в Go реализуется через ключевое слово go без необходимости менять тело функции.
  • Механизмы переключения контекста: кооперативный и вытесняющий подходы, в Go используется смешанный метод.
  • Компилятор Go автоматически расставляет точки переключения контекста в местах блокировок и системных вызовов.
  • Обзор синхронизации потоков и гарутин, примеры с игровым сервером и взаимодействием между игроками.
  • Использование каналов для безопасного обмена данными между гарутинами и предотвращения гонок данных.
  • Примитивы синхронизации в Go: WaitGroup, атомарные операции и их применение для управления конкурентным доступом.

Full Transcript — Download SRT & Markdown

00:00
Speaker A
Всем привет! Go — это язык, который не просто обладает хорошими инструментами, позволяющими использовать всю мощь современных многоядерных процессоров. Go — это язык, который проектировался в первую очередь для конкурентных и параллельных вычислений, поэтому пройти мимо темы конкурентного я просто не мог.
00:19
Speaker A
Парочка важных дисклееров. Во-первых, данное видео является обзорным, ну или введением в конкурентные вычисления. То есть я не буду погружаться слишком глубоко в каждую из множества рассматриваемых сегодня тем. Для этой цели в будущем появится отдельное видео на некоторые из сегодняшних тем. Кроме
00:36
Speaker A
того, видео может содержать неточности с точки зрения использования некоторых терминов. Это сделано намеренно для упрощения, чтобы не вводить слишком много новых понятий. Что ж, поехали! [музыка] Википедия гласит: параллелизм — это свойство систем, при котором несколько вычислений выполняется одновременно, при
01:03
Speaker A
этом возможно взаимодействуют друг с другом. Также Википедия приводит важное примечание: часто параллелизм и конкурентность принимают за одно и то же, хотя работают они на разных уровнях: параллелизм — на физическом, а конкурентность — на логическом. Теперь давайте разбираться с этим последовательно. Представьте себе очень
01:22
Speaker A
простой процессор, у которого всего одно ядро. На этом ядре в один момент времени может исполняться только одна программа или процесс. Чтобы компьютер мог исполнять несколько задач одновременно, процессор можно сделать многоядерным. Тогда он сможет исполнять одновременно не более n-программ, где N — собственно
01:38
Speaker A
количество ядер. В нашем случае это 4. Это все еще не похоже на современные устройства, которые могут держать в фоне сотни и тысячи приложений, занимающихся какой-то работой. Необходимо, чтобы наш воображаемый компьютер мог выполнять сразу несколько задач или потоков на одном ядре. Как этого можно достичь?
01:57
Speaker A
Вспомните, как вы подходите к готовке ужина, состоящего из нескольких блюд. Вряд ли вы будете ждать, пока одно блюдо запечется в духовке, чтобы приступить к варке супа. Чтобы не терять время впустую, после включения духовки, плиты, вы начинаете заниматься супом. Этот же
02:13
Speaker A
подход можно переложить и на вычисление на компьютере. Если мы знаем, что программа сейчас ожидает чего-то и не делает никакую полезную работу, можно на время отдать ресурс процессора другой программе. То же самое справедливо для потоков и корутин. Что такое корутины, мы
02:29
Speaker A
разберемся чуть позже. Координацией таких задач на разных уровнях — от процессора до программы — занимается планировщик. Причем на каждом уровне планировщик свой, его алгоритм работы не зависит от алгоритмов планировщиков на других уровнях. Однако планировщик может влиять на результаты работы планировщика на уровне выше. Важно
02:50
Speaker A
заметить, что в данном случае вычисление не происходит параллельно по-настоящему. Планировщик просто выбирает, какая задача может продолжить исполнение в данный момент времени, но в результате эффективность такого подхода приближена к эффективности параллелизма. В этом и есть суть конкурентных вычислений, чтобы
03:08
Speaker A
лучше понять разницу между конкурентными и параллельными вычислениями, давайте построим такое соответствие: конкурентность — это когда один повар переключается между готовкой разных блюд, а параллелизм — это когда несколько поваров одновременно готовят блюда, причем каждое свое. Процессы в современных операционных системах могут
03:29
Speaker A
создавать потоки — части программы, которые исполняются параллельно и в общем случае независимо друг от друга, но они разделяют общее адресное пространство процесса. То есть разные потоки могут использовать обычно всю память, доступную программе. В языке Go нет концепции потоков как инструментов для работы с
03:47
Speaker A
ними. Вместо этого авторы предлагают работать с гарутинами. Гарутины можно сравнить с гринтрадами в Java или с корутинами в других языках. Гарутина — это как системный поток, но с некоторыми важными отличиями. Во-первых, гарутина гораздо легковеснее потоков, поэтому их можно создавать миллионами и не страдать
04:06
Speaker A
от этого. Также гарутины контролируются не на уровне операционной системы, и на одном потоке может исполняться несколько гарутин. Иными словами, Go скрывает от разработчика тяжелую работу с потоками операционной системы, беря эту ношу на себя, а взамен предоставляя гарутины — гораздо более удобный и гибкий
04:27
Speaker A
инструмент для работы с конкурентными вычислениями. Ввиду того, что на уровне языка нет разделения на потоки и гарутины, эти два термина могут использоваться как синонимы в контексте языка Go. И еще одно важное отличие — асинхронности в Go и асинхронности в других языках.
04:43
Speaker A
Строго говоря, любые функции могут использоваться как синхронно, так и асинхронно. В таких языках, как Python и JavaScript, объявления функции, которые исполняются асинхронно, необходимо сопровождать ключевым словом. При этом вызов такой функции нужно предварять ключевым словом await, и тогда вызывающая
05:00
Speaker A
функция тоже обязана быть асинхронной, то есть использовать ключевое слово async. Другой вариант — использовать Future, Promise. Однако это целая отдельная концепция, в которую необходимо погружаться. Go предлагает более простой подход: если функция вызывается напрямую, то она ведет себя как синхронная, а если
05:17
Speaker A
перед вызовом функции стоит ключевое слово go, то функция исполняется в отдельной корутине, то есть становится асинхронной. При этом тело функции можно никак не изменять. Чтобы переключать контекст исполнения эффективно, нужно выбирать для этого правильный момент. Для решения этой задачи есть два подхода: кооперативный и
05:35
Speaker A
вытесняющий. Кооперативное многозадачность предполагает, что контекст переключается в тот момент, когда текущая активная задача явно объявляет о том, что она готова отдать ресурс другой задаче. Вытесняющая многозадачность — это вид многозадачности, при котором существует некий компонент, который управляет переключением контекста и может принудительно ставить
05:56
Speaker A
одни задачи на паузу и продолжать выполнение других. Обычно такой компонент и называется планировщиком. У обоих подходов есть свои плюсы и минусы. Так, при неправильном использовании кооперативной многозадачности есть риск, что задача оккупирует все вычислительные ресурсы и не даст выполняться другим
06:14
Speaker A
задачам, даже когда внутри нее не происходит полезной работы. В то же время при вытесняющей многозадачности не всегда можно гарантировать, что переключение контекстов в тот или иной момент будет эффективным. В Go используется смешанный подход. В языке есть нечто похожее на планировщик,
06:31
Speaker A
но работает он не во время исполнения программы, а во время компиляции. Компилятор расставляет метки для переключения контекстов в различных местах программы, основываясь на некоторых эвристиках о том, что в таких местах программа скорее всего будет заблокирована в ожидании чего-то. То есть
06:46
Speaker A
мы имеем по сути кооперативное многозадачность, но разработчик не полностью контролирует моменты, где гарутина сообщает, что готова отдать процессорное время другим гарутинам. На самом деле в большинстве случаев такой необходимости и нет. Разработчик может явно объявить о том, что в том или ином
07:03
Speaker A
месте можно переключиться на другую гарутину с помощью вызова функции runtime.Gosched, но делать это стоит только если вы понимаете, что делаете и в этом действительно есть необходимость. Компилятор сам расставляет переключение контекста в следующих местах: во-первых, это операции с сетью, системные вызовы,
07:22
Speaker A
вызовы функции, ну и конечно же блокировки. Алгоритм расстановки переключения контекста может меняться от версии к версии, поэтому как-то полагаться на конкретные места в программе в этом смысле не стоит. Окей, теперь поговорим о синхронизации потоков. Для чего вообще это нужно делать? Ну, такая задача
07:40
Speaker A
возникает, когда один поток может влиять на работу других потоков. Представьте, что вы программируете сервер для онлайн-игры. Соединение каждого игрока живет в своей гарантине. Каждый игрок постоянно генерирует различные события в игровом мире, и эти события не просто могут
07:55
Speaker A
видеть другие игроки, но также и реагировать на них. Кроме того, вам наверняка захочется вести лог всех игровых событий. Если просто дать возможность каждому потоку писать в логи, скорее всего на выходе вы получите кашу символов. Произойдет это потому, что пока
08:11
Speaker A
поток А начинае...
08:26
Speaker A
чтобы разработчику было проще решать задачу синхронизации гарутин в языке присутствует специальный инструмент каналы каналы это переменные специального типа который позволяет производить над ними запись и чтения из разных карутин и при этом практически не беспокоиться конкурентном доступе к ним
08:44
Speaker A
так как вся грязная работа происходит под капотом синтаксисы работы с каналами отличается от синтаксиса операции над обычными переменными Обратите внимание на символ который похож на стрелочку расположение данного символа соответствует направлению движения данных то есть при записи в канал
09:01
Speaker A
стрелочка ставится после имени переменной канала а при чтении перед именем по умолчанию операции чтения и записи блокирует текущий поток за счет чего и обеспечивается асинхронность без явных блоков Давайте взглянем пример из эту ровго представим что у нас есть функция сам
09:19
Speaker A
которая во-первых принимает слайс чисел которые необходимо просуммировать а также канал который затем необходимо положить результат функции Main Мы сначала инициализируем канал затем запускаем две грузины из функции Sum в каждую из которых мы Передаем половину исходного слайса после этого в переменной X Y мы читаем из
09:42
Speaker A
канала оси в этот момент текущий поток будет заблокирован пока функция сам два раза не отработает но затем Ничего необычного мы просто выводим результат каналы могут быть буферизированными или иными словами иметь емкость Это значит что запись в канал будет блокирующей
10:00
Speaker A
только если канал будет записана N элементов где N это как раз емкость в свою очередь чтение будет заблокировано только если канал будет пуст чтобы канал был буферизированным необходимо указать его емкость при инициализации в данном случае емкость канала равна двум такие свойства
10:18
Speaker A
буферизированных каналов действительно могут быть полезны поскольку блокировка при каждой записи или каждом чтении это не всегда удобно Однако как и с небуфризированными каналами стоит пользоваться этим Осторожно если из канала нигде не производится чтение и он переполняется происходит взаимная
10:35
Speaker A
блокировка потоков которую невозможно разрешить такое явление называется Deadlock воспроизвести подобную ситуацию очень просто представим что у нас есть следующая программа создается канал с емкостью 4 после чего в него записывается 4 значения после чего производится 4 чтения Окей но Что
10:55
Speaker A
будет если добавить еще одну запись в канал например строку Friend но при этом не добавлять еще одно чтение что ж программа упадет при выполнении с ошибкой примерно похожего содержания то есть произойдет канал можно закрыть с помощью встроенной функции Close чтобы дать знать читателю
11:15
Speaker A
что новых значений в канал поступать не будет в свою очередь читатель может при чтении проверить открытый канал запись в закрытый канал вызывает панику по этой причине не стоит закрывать канал на читающей стороне лучше это сделать в том же месте где канал создается или
11:30
Speaker A
передается другой гарантине также закрытие канала является опциональным поэтому использовать Close имеет смысл когда вам нужно явно сообщить читающей стороне что новых значений не будет например чтобы цикл по каналу Прекратил читать значение из этого канала Да по каналам можно и оперироваться примерно
11:47
Speaker A
так же как по слайсам или мапам на каждой итерации текущий поток блокируется до следующего значения в канале выход из такого цикла происходит когда канал закрывается цикл по каналам Хорошо подходит для случаев когда вы ожидаете из канала не одно а множество
12:02
Speaker A
периодических значений например это удобно при написании telegram-ботов конструкция Select позволяет подождать В текущей га рутине значение из нескольких каналов Как вы видите Select очень похож на Switch по синтаксису Здесь вы видите пример функции для чисел Фибоначчи реализованные на каналах и с
12:27
Speaker A
помощью selecto Однако логика обработки различных кейсов у селекта отличается от свеча во-первых сначала сработает тот кейс для которого запись в ожидаемый канал произойдет первый во-вторых кейс дефолт срабатывает если в момент вызова селекта ни один из каналов не готов для чтения в-третьих в
12:46
Speaker A
случае если несколько каналов доступных для чтения runtime он будет выбран Случайный кейс поэтому опираться на порядок объявления кейсов нет смысла в отличие от свитча Как вы видите тик здесь напечатан неравномерно поскольку иногда Селект выбирал дефолта иногда выбирал тигр когда приходило время тика
13:04
Speaker A
Если у вас есть сложности запоминанием деталей связанных с каналами можно обратиться к так называемым максимумам каналов которые сформулировал Да и в чейни аксиома номер один запись в неинициализированный канал блокирует поток навсегда вторая аксиома чтение из неинициализированного канала блокирует
13:21
Speaker A
поток навсегда третий аксиома гласит что запись в закрытый канал вызывает панику Ну и последний аксиома чтение из закрытого канала дает нулевое значение мгновенно ранее Я уже упоминал проявление когда несколько потоков обращаются к одной и той же области памяти хороший инструмент
13:44
Speaker A
в таком случае это каналы Но их не всегда удобно использовать например их использование может быть избыточной мерой когда задача заключается не в синхронизации потоков А в синхронизации доступа к памяти чтобы понять разницу давайте рассмотрим пример вернемся к онлайн игре которую мы с вами писали в
14:02
Speaker A
предыдущей части Представьте что теперь у каждого игрока есть ресурс в виде монет монеты расходуются и зарабатываются двумя способами активно Когда игрок непосредственно тратит их например в магазине предметов и пассивно это всякие автоматические расходы на работу юнитов и доходы от
14:19
Speaker A
производственных зданий все процессы и события влияющие на баланс игрока происходят Независимо друг от друга расходы на юниты не связаны с доходом от зданий и покупок в игровом магазине напрямую мы не можем быть уверены что такие события будут происходить последовательно Представьте
14:35
Speaker A
в следующую ситуацию у игрока в данный момент на балансе 5 монет При следующем обновлении игрового мира с него будут списаны деньги за работу зданий юнитов в размере трех монет в тот же момент когда началось списывание средств игрок нажимает на кнопку купить за 4 монеты в
14:53
Speaker A
игровом магазине желая купить в свою армию и еще один Юнит с точки зрения здравого смысла он не может так поступить У него всего пять монет а потратить необходимо в сумме 7 если кнопка покупки была нажата в тот момент
15:06
Speaker A
когда три монеты начали списываться но баланс все еще не был обновлен мы получаем состояние гонки две разные гарутины одновременно прочитали что у игрока пять монет Однако после того как каждая гарутина завершит расчеты на балансе у игрока минус Две монеты чего
15:22
Speaker A
мы не можем допустить в данном случае обнаружить гонку в программе было довольно просто но иногда в своей работе Мы сталкиваемся совсем неочевидными случаями гонки который с одной стороны трудно обнаружить а с другой стороны они вызывают баги которые крайне трудно локализовать для
15:39
Speaker A
облегчения задачи обнаружения состояния гонки уже в первых версиях Go появился Race Detector Ну или детектор гонок достаточно при запуске тестов или компиляции добавить флаг Race после чего компилятор будет следить за тем как происходит доступ к различным местам памяти программы если компиляторы
15:56
Speaker A
обнаруживает гонку он сообщает об этом печатая в вортинг есть пара важных моментов про reset во-первых он никогда не ошибается в том смысле что Race детектор может не найти все гонки но все гонки о которых он сообщает гарантированно являются гонками
16:11
Speaker A
во-вторых включен рейс детектор добавляет заметные накладные расходы на исполнение программы Поэтому если для вашего приложения критично производительность имеет смысл включать флаг Race в первую очередь На тестах для нагрузочных Окей с рейс детектором или без мы обнаружили состояние гонки теперь
16:30
Speaker A
нам необходимо избавиться от данного эффекта Для этого нам стоит обратиться к примитивам синхронизации стоит отметить что каждый из примитивов рассмотрен верхнеуровнево чтобы не раздувать Данное видео примитивы о которых можно рассказывать много интересного получит свои отдельные материалы одним из таких примитивов
16:48
Speaker A
является mutex ньютокс это объект который может находиться в двух состояниях разблокированном и заблокированным алгоритм работы сметоксом обычно такой в потоке а вызывается метод M модифицирующие данные доступ к которым необходимо синхронизировать внутри м блокируется mitex после чего выполняется остальная
17:07
Speaker A
часть функции и ньютекс разблокируется если во время того как mitex все еще заблокирован метод будет вызван из другого потока Ну назовем его б произойдёт попытка повторно заблокировать уже заблокированным ютекс в таком случае выполнение потока блокируется пока YouTube не будет
17:24
Speaker A
разблокирован перед завершением вызова м в потоке таким образом если м производит изменение в данных это произойдет последовательно Даже при одновременном вызове м из разных корутин в виде кода ногой это может выглядеть примерно так [музыка] в кошельке игрока кроме непосредственно
17:49
Speaker A
баланса Мы также объявляем поле с ньютексом в методе spadcoins в самом начале мы сначала блокируем YouTube а также указываем Что mitex необходимо разблокировать после выхода из этого метода Ну и также у нас есть Helper методы для подписанных выше двух кейсов
18:05
Speaker A
то есть оплата юнитов зданий и покупка юнитов в магазине функции Main Main запускаем две га рутины в каждой из которых мы вызываем эти два Helper метода здесь серым отмечено Wait группа она здесь безусловно нужна но они мы поговорим
18:28
Speaker A
чуть позже Если мы запустим данный код несколько раз мы увидим что не успевает то одна га рутина то другая но при этом ситуация с отрицательным балансом не возникает при использовании мьютокса вам стоит следовать нескольким правилам назовем это техника безопасности чтобы не
18:46
Speaker A
напороться на некоторые трудно отлаживаемые проблемы во-первых никогда не встраивайте ньютокс в структуру как здесь при встраивании мьютакс станет экспортированным полем и таким образом будет доступен для контроля за пределами методов структуры во-вторых никогда не храните ссылку на ютекс в поле структуры
19:03
Speaker A
иначе во всех копиях структуры будет храниться ссылка на один и тот же мьютекс Думаю нет необходимости объяснять Какие Вас ждут сюрпризы в таком случае в-третьих методы которые блокируют и разблокируют ньютекс должны иметь ссылочный ресивер в противном случае мьютекс будет заблокирован в
19:20
Speaker A
копии структуры а не в том экземпляре у которого был вызван метод когда приложение имеет высокую частоту операции чтения и записи в какой-то области памяти использование обычного mitex заметно снижает производительность онлайн игры часто являются такими приложениями поэтому наш пример с
19:36
Speaker A
монетами Здесь отлично подходит частично решить проблему с накладными расходами в связи с блокировками Может rwtax это все тот же мьютекс только Он позволяет получать эксклюзивные блокировки отдельно на чтение и отдельно на запись rw-этекс может стать неплохой оптимизацией местах Где используется
19:54
Speaker A
одна и та же блокировка и для записей и для чтения данных еще один примитив синхронизации который Хорошо подходит к примеру с балансом монеты игрока это атомарные счетчики из пакета сингтомик а томики интересны прежде всего тем что они позволяют конкурен читать и писать
20:09
Speaker A
данные без блокировок как это возможно отдельная интересная тема Сейчас мы ее опустим в примере рассмотренном выше мы можем избавиться от ньютекса а метод spincoins мы можем переписать так как вы видите на экране А томики это довольно низкоуровневый примитив синхронизации
20:25
Speaker A
возможно поэтому их даже вынесли в отдельный подпакет Они часто используются как кирпичики для построения других примитивов синхронизацию если у вас нет действительно хороших причин для использования томиков Воспользуйтесь лучше более высокоуровневыми инструментами из пакета Синг сотомиками легко наломать дров достаточно где-то
20:43
Speaker A
один раз обратиться к переменной счетчику напрямую не используя функции обертки типа Load и так далее syncmap - Это примерно как мэп из Энни Вене но она уже готова к конкурентному доступу то есть ее нет необходимости обкладывать блокировками так как синхмэп это не
20:59
Speaker A
встроенный в язык тип А обертка как я уже сказал над мап из Энни Вене обращение к элементам по ключу Не сработает Для этого необходимо использовать Load Store Delete Ну и их вариации Wait группа ждет пока набор гарутин завершит работу
21:15
Speaker A
главная гарутина то есть став которой была создана вает группа должна содержать вызовы метода F чтобы сообщить вейт-группе сколько гарутин Необходимо дождаться каждая гарутина вызывает метод Дан чтобы сообщить в этой группе что одна из гарутин закончила работу и счетчик можно понизить метод вейд
21:32
Speaker A
используется чтобы заблокировать текущую га рутину пока внутренний счетчик vate группы не достигнет нуля за счет вызовов метода данных группа полезна Когда вам необходимо выполнить несколько независимых Долгих операций но результат каждой из этих операций вам важен Ну и поговорим про те примитивы которые
21:51
Speaker A
достойны упоминания пол является реализацией паттерна облик-пул которую можно безопасно использовать конкурентной среде object Pool это объект который хранит внутри себя фиксированное количество приотсозданных объектов и выдает их по необходимости его часто используют там где нужно часто создавать большое количество объектов
22:07
Speaker A
вместо того чтобы создавать такую через Новый объект каждый раз что может быть долго обжиг-пул старается переиспользовать уже выделенную память и возвращать объекты из своего внутреннего набора а конт может быть полезен для того чтобы заставить гарутину дождаться и среагировать на внешние события и если
22:25
Speaker A
пул на моей практике еще встречается хоть и не очень часто то конт кажется я не видел ни разу это не случайно Ту же самую задачу в большинстве случаев можно решить с помощью каналов Это все что я хотел рассказать по теме concarns в го
22:39
Speaker A
на этот раз Но я думаю это не последнее видео по данной теме на нашем канале поэтому не забудьте подписаться но и заодно поставить лайк в этот раз у меня заготовлено много ссылок на дополнительную информацию по теме канкарнсе эти ссылки Вы можете найти в
22:53
Speaker A
нашем Telegram канале нашим дискорде а также на бусте И патреоне кстати Спасибо всем кто нас поддерживает на данных площадках платные подписчики получают много бонусов один из которых Кстати это доступ к конспекту данного видео в публичном доступе конспект появится
23:10
Speaker A
только когда выйдет следующее видео ссылки на соцсети и на бусины с патреоном вы найдете в описании данного ролика Всем большое спасибо пока [музыка]
Topics:GoконкурентностьпараллелизмгарутинымногозадачностьпланировщиксинхронизацияканалыасинхронностьWaitGroup

Frequently Asked Questions

В чем отличие конкурентности от параллелизма в Go?

Конкурентность — это логическое разделение задач, когда одна задача переключается между несколькими потоками, а параллелизм — это одновременное выполнение нескольких задач на разных ядрах процессора.

Что такое гарутины и чем они отличаются от потоков ОС?

Гарутины — это легковесные потоки, управляемые планировщиком Go, которые гораздо легче системных потоков и позволяют создавать миллионы одновременно без значительных затрат ресурсов.

Как в Go реализуется переключение контекста между гарутинами?

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

Get More with the Söz AI App

Transcribe recordings, audio files, and YouTube videos — with AI summaries, speaker detection, and unlimited transcriptions.

Or transcribe another YouTube video here →