Почему является явное управление потоками плохой вещью?

В предыдущем вопросе я сделал что-то вроде бестактности. Вы видите, я читал о потоках и имел впечатление, что они были самыми вкусными вещами начиная с желе киви.

Вообразите мой беспорядок затем, когда я считал материал как это:

[T] hreads являются Очень Плохой Вещью. Или по крайней мере, явное управление потоками является плохой вещью

и

Обновление UI через потоки обычно является знаком, что Вы злоупотребляете потоками.

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

Как я должен использовать поток?

45
задан Community 23 May 2017 в 12:08
поделиться

11 ответов

Энтузиазм для изучения потоковой передачи большой; не поймите меня неправильно. Энтузиазм по поводу использования большого количества потоков , напротив, является симптомом того, что я называю болезнью ниточного счастья.

Разработчики, которые только что узнали о возможностях потоков, начинают задавать вопросы вроде «сколько потоков я могу создать в одной программе?» Это больше похоже на вопрос английского специалиста: «Сколько слов я могу использовать в предложении?» Типичный совет для писателей - делать предложения короткими и по существу, а не пытаться втиснуть как можно больше слов и идей в одно предложение. Нитки такие же; правильный вопрос заключается не в том, «скольким мне сойдет с рук создание?» а скорее «как я могу написать эту программу, чтобы количество потоков было минимумом , необходимым для выполнения работы?»

Потоки решают множество проблем, это правда, но они также создают огромные проблемы:

  • Анализ производительности многопоточных программ часто чрезвычайно сложен и глубоко противоречит здравому смыслу. Я видел реальные примеры в сильно многопоточных программах, в которых выполнение функции быстрее без замедления других функций или использования большего количества памяти снижает общую пропускную способность системы . Почему? Потому что потоки часто похожи на улицы в центре города.Представьте, что вы берете каждую улицу и волшебным образом делаете ее короче , не меняя время светофора . Будет ли пробка лучше или хуже? Написание более быстрых функций в многопоточных программах ускоряет перегрузку процессоров .

Вы хотите, чтобы потоки были похожи на межгосударственные автомагистрали: без светофоров, они были очень параллельны, пересекались в небольшом количестве очень четко определенных, тщательно спроектированных точек. Это очень сложно сделать. Большинство программ с высокой степенью многопоточности больше похожи на города с плотной застройкой и повсюду светофоры.

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

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

  • По сути, потоки заставляют ваши методы лгать . Позвольте привести пример. Предположим, у вас есть:

    if (! Queue.IsEmpty) queue.RemoveWorkItem (). Execute ();

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

 if (queue.WasNotEmptyAtSomePointInThePast) ...

, что явно бесполезно.

Предположим, вы решили решить проблему, заблокировав очередь. Это правильно?

lock(queue) {if (!queue.IsEmpty) queue.RemoveWorkItem().Execute(); }

Это тоже обязательно неправильно. Предположим, выполнение вызывает запуск кода, который ожидает ресурса, заблокированного в данный момент другим потоком, но этот поток ожидает блокировки очереди - что происходит? Оба потока ждут вечно. Чтобы заблокировать кусок кода, вы должны знать все , что код может , возможно, делать с любым общим ресурсом, чтобы вы могли решить, будет ли быть никаких тупиков. Опять же, это чрезвычайно тяжелое бремя для того, кто пишет, что должно быть очень простым кодом. (Правильнее всего здесь, вероятно, извлечь рабочий элемент из блокировки, а затем выполнить его за пределами блокировки. Но ... что, если элементы находятся в очереди, потому что они должны выполняться в определенном порядке? Теперь, когда код тоже неверен, потому что другие потоки могут сначала выполнять более поздние задания.)

  • Дальше становится хуже.Спецификация языка C # гарантирует, что однопоточная программа будет иметь наблюдаемое поведение, точно такое, как указано в программе. То есть, если у вас есть что-то вроде «if (M (ref x)) b = 10;"тогда вы знаете, что сгенерированный код будет вести себя так, как будто M обращается к x перед записью b. Теперь компилятор, джиттер и ЦП могут оптимизировать это. Если один из них может определить, что M будет истинным, и если мы знаем, что в этом потоке значение b не читается после вызова M, тогда b может быть присвоено до обращения к x. Все, что гарантировано, - это то, что однопоточная программа , похоже, работает так, как было написано .

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

Это означает, что для написания многопоточных программ, в которых e есть зависимость в логике от наблюдаемых событий, происходящих в том же порядке, в котором фактически написан код, вы должны иметь подробное понимание «модели памяти» языка и среды выполнения. . Вы должны точно знать, какие существуют гарантии того, как доступ может перемещаться во времени. И вы не можете просто протестировать на своем x86-компьютере и надеяться на лучшее; чипы x86 имеют довольно консервативную оптимизацию по сравнению с некоторыми другими чипами.

Это всего лишь краткий обзор некоторых проблем, с которыми вы сталкиваетесь при написании собственной многопоточной логики.Есть еще много всего. Итак, несколько советов:

  • Узнайте о многопоточности.
  • Не пытайтесь написать собственное управление потоками в производственном коде.
  • Используйте высокоуровневые библиотеки, написанные экспертами, для решения проблем с потоками. Если у вас есть много работы, которую нужно выполнить в фоновом режиме, и вы хотите передать ее рабочим потокам, используйте пул потоков, а не пишите собственную логику создания потока. Если у вас есть проблема, которую можно решить сразу несколькими процессорами, используйте библиотеку параллельных задач. Если вы хотите лениво инициализировать ресурс, используйте класс ленивой инициализации, а не пытайтесь написать код без блокировки самостоятельно.
  • Избегайте общего состояния.
  • Если вы не можете избежать общего состояния, поделитесь неизменяемым состоянием.
  • Если вам нужно совместно использовать изменяемое состояние, предпочитайте использование блокировок методам без блокировки.
109
ответ дан 26 November 2019 в 20:50
поделиться

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

8
ответ дан 26 November 2019 в 20:50
поделиться

Я думаю, что первое утверждение лучше всего объяснить как таковое: с теперь доступно множество расширенных API , вручную писать собственный код потока почти никогда не нужно. Новые API проще в использовании, а труднее испортить !. Принимая во внимание, что со старым стилем потоковой передачи вы должны быть достаточно хороши, чтобы не испортить. API старого стиля ( Thread и др.) Все еще доступны, но новые API ( Task Parallel Library , Parallel LINQ и Реактивные расширения ) - это путь в будущее.

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

4
ответ дан 26 November 2019 в 20:50
поделиться

Я думаю, вам следует как можно больше экспериментировать с Threads и узнавать о преимуществах и недостатках их использования. Только экспериментируя и используя их, вы сможете лучше понять их. Прочтите как можно больше по этой теме.

Когда дело доходит до C # и пользовательского интерфейса (который является однопоточным, и вы можете изменять элементы пользовательского интерфейса только в коде, выполняемом в потоке пользовательского интерфейса). Я использую следующую утилиту, чтобы оставаться в здравом уме и крепко спать по ночам.

 public static class UIThreadSafe {

     public static void Perform(Control c, MethodInvoker inv) {
            if(c == null)
                return;
            if(c.InvokeRequired) {
                c.Invoke(inv, null);
            }
            else {
                inv();
            }
      }
  }

Вы можете использовать это в любом потоке, которому необходимо изменить элемент пользовательского интерфейса, например:

UIThreadSafe.Perform(myForm, delegate() {
     myForm.Title = "I Love Threads!";
});
2
ответ дан 26 November 2019 в 20:50
поделиться

Я бы начал с сомнения в этом восприятии:

Я читал о нитках и у меня сложилось впечатление, что они были самыми вкусными вещами после желе из киви.

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

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

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

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

3
ответ дан 26 November 2019 в 20:50
поделиться

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

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

1
ответ дан 26 November 2019 в 20:50
поделиться

При обновлении пользовательского интерфейса из потока, отличного от пользовательского интерфейса, важно отметить несколько моментов:

  1. Если вы часто используете «Invoke», производительность ваш поток, не связанный с пользовательским интерфейсом, может серьезно пострадать, если другие факторы заставят поток пользовательского интерфейса работать медленно. Я предпочитаю избегать использования «Invoke», если только потоку, не относящемуся к пользовательскому интерфейсу, нужно дождаться выполнения действия потока пользовательского интерфейса, прежде чем оно продолжится.
  2. Если вы опрометчиво используете «BeginInvoke» для таких вещей, как обновления элементов управления, чрезмерное количество делегатов вызова может оказаться в очереди, некоторые из которых вполне могут оказаться довольно бесполезными к тому времени, когда они действительно произойдут.

Во многих случаях я предпочитаю, чтобы состояние каждого элемента управления было инкапсулировано в неизменяемый класс, а затем имелся флаг, указывающий, не требуется ли обновление, ожидает ли оно или необходимо, но не ожидает (последняя ситуация может возникнуть, если делается запрос на обновление элемента управления до его полного создания). Процедура обновления элемента управления должна, если требуется обновление, начинать с очистки флага обновления, захвата состояния и рисования элемента управления. Если установлен флаг обновления, он должен повторить цикл.Чтобы запросить другой поток, подпрограмма должна использовать Interlocked.Exchange для установки флага обновления на ожидающее обновление и - если оно не было отложено - попытаться BeginInvoke подпрограммы обновления; если BeginInvoke завершается неудачно, установите флаг обновления на «необходимо, но не ожидает».

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

0
ответ дан 26 November 2019 в 20:50
поделиться

Явное управление потоками не по своей сути плохая вещь, но она чревата опасностями, и ее нельзя делать без крайней необходимости.

Сказать, что резьба - это абсолютно хорошо, - все равно что сказать, что пропеллер - это абсолютно хорошая вещь: пропеллеры отлично работают на самолетах (когда реактивные двигатели не лучшая альтернатива), но это не будет хорошей идеей для автомобиля. .

11
ответ дан 26 November 2019 в 20:50
поделиться

Если вы не на уровне написания полноценного планировщика ядра, вы получите явное управление потоками всегда неправильно.

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

Как правило, если проблема разбита на потоки, они должны быть как можно более независимыми, с минимальным количеством, но четко определенными общими ресурсами, насколько это возможно, с наиболее минималистичной концепцией управления.

6
ответ дан 26 November 2019 в 20:50
поделиться

Многие продвинутые GUI-приложения обычно состоят из двух потоков, один для пользовательского интерфейса, один (или иногда больше) для обработки данных (копирование файлов, выполнение тяжелых вычислений, загрузка данных из базы данных и т.д.).

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

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

2
ответ дан 26 November 2019 в 20:50
поделиться

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

Рассмотрим классический пример: у вас есть два потока, которые получают некоторые элементы из общего списка и, сделав что-то, удаляют элемент из списка.

Метод потока, который периодически вызывается, может выглядеть следующим образом:

void Thread()
{
   if (list.Count > 0)
   {
      /// Do stuff
      list.RemoveAt(0);
   }
}

Помните, что потоки, теоретически, могут переключиться в любой строке вашего кода, который не синхронизирован. Так, если список содержит только один элемент, один поток может пройти условие list.Count, непосредственно перед list.Remove потоки переключаются и другой поток проходит list.Count (список по-прежнему содержит один элемент). Теперь первый поток продолжает list.Remove, а после него второй поток продолжает list.Remove, но последний элемент уже удален первым потоком, поэтому второй поток терпит крах. Поэтому его нужно синхронизировать с помощью оператора lock, чтобы не возникло ситуации, когда два потока находятся внутри оператора if.

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

В предыдущих версиях .NET, если вы хотели обновить UI в другом потоке, вы должны были синхронизироваться с помощью методов Invoke, но поскольку это было достаточно сложно реализовать, в новых версиях . NET поставляются с классом BackgroundWorker, который упрощает задачу, оборачивая все вещи и позволяя вам делать асинхронные вещи в событии DoWork и обновлять UI в событии ProgressChanged.

1
ответ дан 26 November 2019 в 20:50
поделиться
Другие вопросы по тегам:

Похожие вопросы: