Создание потоков может быть простым, но не таким простым ...

Большинство современных компьютеров имеют несколько процессоров или, по крайней мере, быстрое переключение контекста для одного процессора. Даже если у вас есть один процессор, у вас будет много времени, когда ему нечего делать, кроме как ждать, пока одна программа будет обслуживать другую. Чтобы воспользоваться этим в ваших собственных программах, вы можете использовать потоки. Но есть несколько хитрых вещей, связанных с потоковой передачей, о которых вам нужно подумать при разработке программного обеспечения, чтобы воспользоваться этим преимуществом.

То, как именно вы создаете потоки и управляете ими, будет зависеть от языка и, в некоторой степени, от вашей операционной системы. Ари Саиф рассказывает о C ++ 14 и более поздних версиях в этом посте. Однако, независимо от языка или других деталей, вам необходимо знать об определенных ключевых дизайнерских идеях.

Критические секции и мьютекс

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

Представьте себе этот код:

счетчик = счетчик + 1;

Если вы поделитесь переменной счетчика с другим потоком, у вас могут быть проблемы. Это связано с тем, что ЦП выполняет инструкции на языке ассемблера, поэтому для выполнения задачи может потребоваться более одной инструкции.

Предположим, целевая машина может выполнять только 8-битные вычисления, а это 16-битный счетчик (конечно, это все еще применимо к другим размерам). В каком-то мифическом наборе инструкций это могло бы выглядеть так:

ДОБАВИТЬ СЧЕТЧИК, 1

СКИПНОКАРРИ

ДОБАВИТЬ СЧЕТЧИК + 1,1

Это будет работать почти все время, потому что будет выполняться первая инструкция, переноса не будет, и на этом все. Но что делать, если есть керри. Нет проблем, правда? Код работает. И вряд ли сразу после первой инструкции вас прервут. Но это могло случиться.

Предположим, что счетчик равен 255 (0x00FF) и выполняется первая инструкция. Вы получаете перенос, и теперь счетчик 0x0000. Если другой поток берет на себя прямо в этот момент и обращается к счетчику, он увидит это значение. Это приведет к неправильному поведению. Хуже того, что, если второй поток также изменит счетчик? Большой беспорядок.

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

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

Общие ресурсы и тупик

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

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

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

Инверсия приоритета

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

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

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

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

Резюме

Многопоточность и многозадачность - сложные темы, и может показаться сложным думать обо всех особых случаях. Отладка часто бывает очень сложной, потому что проблемы, как правило, появляются периодически, и трудно воссоздать точные шаблоны. Тем не менее, потоки и задачи - важная часть получения максимальной отдачи от современного оборудования, и на их освоение стоит потратить время.