Статьи

Почему примеры параллелизма сбивают с толку

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

Это было немного удивительно для меня, и мне было интересно, в чем причина.

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

 

Плохие примеры

Проблема в; в разговоре или книге вам нужен простой, понятный пример. К сожалению, простые и понятные примеры почти всегда делают ПЛОХИЕ примеры для параллелизма.

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

 

Пример переноса аккаунта

Распространенным примером, используемым для иллюстрации того, как избежать мертвой блокировки между двумя объектами, является проблема переноса учетной записи. Проблема заключается в том, что для переноса между двумя учетными записями необходимо заблокировать обе учетные записи (при условии, что вы заблокировали отдельные учетные записи). Однако, если вы не заблокируете их в том же порядке, вы можете получить взаимоблокировку, когда два потока удерживают одну из учетных записей, которая нужна другому потоку.

Took an average of 30.7 ns to transfer money with a lock on each Account.

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

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

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

Took an average of 28.9 ns to transfer money with a lock on each Account.

 

Что делать, если я использовал только одну глобальную блокировку

Took an average of 9.7 ns to transfer money with a one global lock.

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

 

Что я просто делаю код однопоточным?

Took an average of 1.8 ns to transfer money, single threaded

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

 

Действительно плохой пример

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

Это работает на 4,6 ГГц i7 2600K с 16 ГБ памяти. Код останавливается, когда не хватает ресурсов для запуска новых потоков.

Fibonacci 2 was 2, took 1,665 us, Time ratio=832.5 
Fibonacci 3 was 3, took 123 us, Time ratio=41.0 
Fibonacci 4 was 5, took 126 us, Time ratio=25.2 
Fibonacci 5 was 8, took 154 us, Time ratio=19.3 
Fibonacci 6 was 13, took 291 us, Time ratio=22.4 
Fibonacci 7 was 21, took 359 us, Time ratio=17.1 
Fibonacci 8 was 34, took 646 us, Time ratio=19.0 
Fibonacci 9 was 55, took 544 us, Time ratio=9.9 
Fibonacci 10 was 89, took 471 us, Time ratio=5.3 
Fibonacci 11 was 144, took 1,417 us, Time ratio=9.8 
Fibonacci 12 was 233, took 3,516 us, Time ratio=15.1 
Fibonacci 13 was 377, took 3,120 us, Time ratio=8.3 
Fibonacci 14 was 610, took 2,143 us, Time ratio=3.5 
Fibonacci 15 was 987, took 7,949 us, Time ratio=8.1 
Fibonacci 16 was 1,597, took 17,539 us, Time ratio=11.0 
Fibonacci 17 was 2,584, took 7,639 us, Time ratio=3.0 
Fibonacci 18 was 4,181, took 28,766 us, Time ratio=6.9 
Fibonacci 19 was 6,765, took 28,838 us, Time ratio=4.3 
Fibonacci 20 was 10,946, took 18,460 us, Time ratio=1.7 
Fibonacci 21 was 17,711, took 85,533 us, Time ratio=4.8 
Fibonacci 22 was 28,657, took 67,250 us, Time ratio=2.3 
Fibonacci 23 was 46,368, took 194,481 us, Time ratio=4.2 
Fibonacci 24 was 75,025, took 122,891 us, Time ratio=1.6 
Fibonacci 25 was 121,393, took 283,110 us, Time ratio=2.3 
Fibonacci 26 was 196,418, took 403,611 us, Time ratio=2.1 
Fibonacci 27 was 317,811, took 2,476,695 us, Time ratio=7.8 
Fibonacci 28 was 514,229, took 7,661,490 us, Time ratio=14.9 
Exception in thread "main" java.lang.AssertionError: java.util.concurrent.ExecutionException: java.lang.AssertionError: java.util.concurrent.ExecutionException: java.lang.AssertionError: java.util.concurrent.ExecutionException: java.lang.AssertionError: java.util.concurrent.ExecutionException: java.lang.OutOfMemoryError: unable to create new native thread

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

Теперь сравните это с однопоточным циклическим решением с включенной опцией -XX: + PrintCompilation.

     30    1             java.lang.String::hashCode (67 bytes)
     36    2             sun.nio.cs.UTF_8$Encoder::encode (361 bytes)
Fibonacci 2 was 2, took 1 us
Fibonacci 3 was 3, took 1 us
Fibonacci 4 was 5, took 1 us
Fibonacci 5 was 8, took 1 us
Fibonacci 6 was 13, took 1 us
Fibonacci 7 was 21, took 1 us
Fibonacci 8 was 34, took 0 us
Fibonacci 9 was 55, took 0 us
Fibonacci 10 was 89, took 1 us
Fibonacci 11 was 144, took 1 us
Fibonacci 12 was 233, took 1 us
Fibonacci 13 was 377, took 1 us
Fibonacci 14 was 610, took      42    3             java.lang.String::indexOf (87 bytes)
1 us
Fibonacci 15 was 987, took 1 us
Fibonacci 16 was 1,597, took 1 us
Fibonacci 17 was 2,584, took 0 us
     42    4             java.lang.String::charAt (33 bytes)
Fibonacci 18 was 4,181, took 1 us
Fibonacci 19 was 6,765, took 1 us
Fibonacci 20 was 10,946, took 1 us
Fibonacci 21 was 17,711, took 1 us
Fibonacci 22 was 28,657, took 1 us
Fibonacci 23 was 46,368, took 1 us
Fibonacci 24 was 75,025, took 1 us
Fibonacci 25 was 121,393, took 0 us
Fibonacci 26 was 196,418, took 1 us
Fibonacci 27 was 317,811, took 1 us
Fibonacci 28 was 514,229, took 1 us
Fibonacci 29 was 832,040, took 1 us
Fibonacci 30 was 1,346,269, took 0 us
Fibonacci 31 was 2,178,309, took 1 us
Fibonacci 32 was 3,524,578, took 1 us
Fibonacci 33 was 5,702,887, took 1 us
Fibonacci 34 was 9,227,465, took 1 us
Fibonacci 35 was 14,930,352, took 1 us
Fibonacci 36 was 24,157,817, took 0 us
Fibonacci 37 was 39,088,169, took 1 us
Fibonacci 38 was 63,245,986, took 1 us
Fibonacci 39 was 102,334,155, took 1 us
Fibonacci 40 was 165,580,141, took 3 us
Fibonacci 41 was 267,914,296, took 0 us
Fibonacci 42 was 433,494,437, took 1 us
Fibonacci 43 was 701,408,733, took 1 us
Fibonacci 44 was 1,134,903,170, took 1 us
Fibonacci 45 was 1,836,311,903, took 1 us
Fibonacci 46 was 2,971,215,073, took 1 us
Fibonacci 47 was 4,807,526,976, took 1 us
Fibonacci 48 was 7,778,742,049, took 1 us
Fibonacci 49 was 12,586,269,025, took 1 us
Fibonacci 50 was 20,365,011,074, took 1 us
Fibonacci 51 was 32,951,280,099, took 1 us
Fibonacci 52 was 53,316,291,173, took 1 us
Fibonacci 53 was 86,267,571,272, took 1 us
Fibonacci 54 was 139,583,862,445, took 1 us
Fibonacci 55 was 225,851,433,717, took 1 us
Fibonacci 56 was 365,435,296,162, took 1 us
Fibonacci 57 was 591,286,729,879, took 1 us
Fibonacci 58 was 956,722,026,041, took 1 us
Fibonacci 59 was 1,548,008,755,920, took 1 us
Fibonacci 60 was 2,504,730,781,961, took 1 us
Fibonacci 61 was 4,052,739,537,881, took 1 us
Fibonacci 62 was 6,557,470,319,842, took 2 us
Fibonacci 63 was 10,610,209,857,723, took 2 us
Fibonacci 64 was 17,167,680,177,565, took 2 us
Fibonacci 65 was 27,777,890,035,288, took 1 us
Fibonacci 66 was 44,945,570,212,853, took 1 us
Fibonacci 67 was 72,723,460,248,141, took 1 us
Fibonacci 68 was 117,669,030,460,994, took 2 us
Fibonacci 69 was 190,392,490,709,135, took 2 us
Fibonacci 70 was 308,061,521,170,129, took 1 us
Fibonacci 71 was 498,454,011,879,264, took 1 us
Fibonacci 72 was 806,515,533,049,393, took 1 us
Fibonacci 73 was 1,304,969,544,928,657, took 1 us
Fibonacci 74 was 2,111,485,077,978,050, took 1 us
Fibonacci 75 was 3,416,454,622,906,707, took 1 us
Fibonacci 76 was 5,527,939,700,884,757, took 1 us
Fibonacci      49    5             java.util.regex.Matcher::77reset (83 bytes)
 was 8,944,394,323,791,464, took 1 us
Fibonacci 78 was 14,472,334,024,676,221, took 1 us
Fibonacci 79 was 23,416,728,348,467,685, took 1 us
Fibonacci 80 was 37,889,062,373,143,906, took 2 us
Fibonacci 81 was 61,305,790,721,611,591, took 3 us
Fibonacci 82 was 99,194,853,094,755,497, took 2 us
Fibonacci 83 was 160,500,643,816,367,088, took 1 us
Fibonacci 84 was 259,695,496,911,122,585, took 2 us
Fibonacci 85 was 420,196,140,727,489,673, took 2 us
Fibonacci 86 was 679,891,637,638,612,258, took 1 us
Fibonacci 87 was 1,100,087,778,366,101,931, took 2 us
Fibonacci 88 was 1,779,979,416,004,714,189, took 2 us
Fibonacci 89 was 2,880,067,194,370,816,120, took 1 us
Fibonacci 90 was 4,660,046,610,375,530,309, took 1 us
Fibonacci 91 was 7,540,113,804,746,346,429, took 2 us

Примечание: метод вычисления значений Фибоначчи даже не скомпилирован и все еще работает в относительно медленном интерпретаторе. Все же это только использует больше чем 2 микросекунды. При компиляции в нативный код это занимает около 40 наносекунд. Если многопоточность Фибоначчи занимает 2 микросекунды на каждое значение результата, то подразумевается, что «Фибоначчи 91» может занять около 180 миллионов лет. К сожалению, количество требуемых потоков также пропорционально значению результата, которое требует порядка триллиона триллионов потоков.

Вывод

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

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

Код

Для учетных записей посмотрите на ThreadedAccountMain, ThreadedAccountOneLockMain и UnthreadedAccountMain.

Для Фибоначчи, посмотрите на ThreadedFibonacciMain и UthhreadedFibonacciMain

 

Код темы

 Я хотел бы поблагодарить Роджера Линдсё за его предложения.

С http://vanillajava.blogspot.com/2011/11/why-concurency-examples-are-confusing.html