Визуализация concurrency в Go с WebGL

    Одной из самых сильных сторон языка программирования Go является встроенная поддержка concurrency, основанная на труде Тони Хоара «Communicating Sequential Processes». Go создан для удобной работы с многопоточным программированием и позволяет очень легко строить довольно сложные concurrent-программы. Но задумывались ли вы когда-нибудь, как выглядят различные паттерны concurrency визуально?

    Конечно, задумывались. Все мы, так или иначе, мыслим визуальными образами. Если я попрошу вас о чём-то, что включает числа «от 1 до 100», вы мгновенно их «увидите» в своей голове в той или иной форме, вероятно даже не отдавая себе в этом отчёт. Я, к примеру, ряд от 1 до 100 вижу как линия с числами уходящая от меня, поворачивающая на 90 градусов вправо на числе 20 и продолжающая до 1000+. И, покопавшись в памяти, я вспоминаю, что в самом первом детском саду в раздевалке вдоль стены были написаны номерки, и число 20 было как-раз в углу. У вас же, вероятно, какое-то свое представление. Или вот, другой частый пример — представьте круглый год и 4 сезона года — кто-то их видит как квадрат, каждая грань которого принадлежит сезону, кто-то — как круг, кто-то ещё как-то.

    Так или иначе, позвольте мне показать мою попытку визуализировать основные паттерны concurrency с помощью Go и WebGL. Эти интерактивные визуализации более-менее отражают то, как я вижу это в своей голове. Интересно будет услышать, насколько это отличается от визуализаций читателей.


    Итак, давайте начнем с простейшего примера — «Hello, concurrent world», чтобы познакомиться с концепцией моего подхода.

    Привет, concurrent мир


    package main
    
    func main() {
        // создаем новый канал типа int
        ch := make(chan int)
    
        // запускаем новую анонимную горутину
        go func() {
            // отправляем 42 в канал
            ch <- 42
        }()
        // ждем, читаем из канала
        <-ch
    }


    Ссылка на интерактивное WebGL демо
    Здесь синие линии представляют горутины, время «бежит» вниз по оси Y. Тонкие синие линии соединяющие 'main' и 'go #19' — это отметки начала и завершения жизни горутины, показывающие предков и детей. Красные стрелочки демонстрируют событие отправки сообщения в канал, и отправленное значение подписано. На самом деле, «отправка в канал» и «чтение из канала» это два отдельных события, и поведение будет сильно отличаться между буферизированными и небуферизированными каналами, но я два этих события анимирую как одно — «передача значения по каналу». Строка "#19" в названии анонимной горутины — это реальный ID запущенной горутины. Хотя официально узнать ID горутин и нельзя (чтобы люди не городили другие модели concurrency, в которых идентификаторы играют важную роль), но для вот таких хакерских случаев таки можно — об этом хорошо написано в статье Скота Мэнсфилда «Goroutine IDs».

    Таймеры


    Фактически, наш простейший Hello, world выше может использоваться для создания простейшего таймера. В стандартной библиотеке Go есть такие удобные функции, как time.After или time.Tick, но давайте реализуем наш собственный — напишем функцию, которая создает канал, запускает горутину, которая спит необходимое время и пишет в канал, и возвратим этот канал вызывающему.
    package main
    
    import "time"
    
    func timer(d time.Duration) <-chan int {
        c := make(chan int)
        go func() {
            time.Sleep(d)
            c <- 1
        }()
        return c
    }
    
    func main() {
        for i := 0; i < 24; i++ {
            c := timer(1 * time.Second)
            <-c
        }
    }


    Ссылка на интерактивное WebGL демо
    Здорово, правда? Но идём дальше.

    Пинг-понг


    Этот интересный пример concurrency был взят из известного доклада гуглера Sameer Ajmani "Advanced Concurrency Patterns". Конечно, этот пример не слишком advanced, но для тех, кто только знакомится с concurrency в Go он должен быть интересным и демонстративным.

    Итак, у нас есть канал table, выполняющий роль стола, есть мяч Ball, который является переменной типа int и хранит в себе количество ударов по нему, и есть горутины-игроки, которые «забирают мяч со стола» (читают из канала), «бьют по нему» (увеличивают переменную) и «бросают обратно на стол» (пишут в канал).
    package main
    
    import "time"
    
    func main() {
        var Ball int
        table := make(chan int)
        go player(table)
        go player(table)
    
        table <- Ball
        time.Sleep(1 * time.Second)
        <-table
    }
    
    func player(table chan int) {
        for {
            ball := <-table
            ball++
            time.Sleep(100 * time.Millisecond)
            table <- ball
        }
    }


    Ссылка на интерактивное WebGL демо
    На этом моменте я хочу ещё раз обратить ваше внимание на ссылку с интерактивным WebGL демо, доступную под каждой анимацией — открыв в новом табе, вы можете двигать, вращать, увеличивать/уменьшать и рассматривать эти 3D анимации, как вам угодно, а так же замедлять/ускорять и перезапускать их.

    Теперь, давайте запустим три игрока-горутины, вместо двух:
        go player(table)
        go player(table)
        go player(table)


    Ссылка на интерактивное WebGL демо
    В данном примере мы видим, что каждый игрок забирает мяч со стола по-очереди, и вы можете задаться вопросом — почему именно так, кто гарантирует этот порядок?

    Ответ тут прост — рантайм Go содержит FIFO очередь для горутин, готовых читать из канала, поэтому мы и наблюдаем этот порядок. В нашем случае каждая горутина становится в очередь сразу же после отправки мяча на стол. Впрочем, это поведение может измениться в будущем и расчитывать на порядок не стоит. Но пока это так, и давайте запустим не три, а сто горутин.
    for i := 0; i < 100; i++ {
        go player(table)
    }


    Ссылка на интерактивное WebGL демо
    Порядок FIFO теперь более очевиден, не так-ли? Мы можем запросто запустить и миллион горутин (они дешевые, и это ок в больших Go программах иметь сотни тысяч горутин), но для наших целей это будет слишком много. Давайте перейдем к другим паттернам.

    Fan-in


    Один из самых известных паттернов это так называемый fan-in паттерн. Он является противоположностью паттерну fan-out, который мы рассмотрим далее. Если вкратце, то fan-in — это функция, читающая из нескольких источников и мультиплексирующая всё в один канал.
    К примеру:
    package main
    
    import (
        "fmt"
        "time"
    )
    
    func producer(ch chan int, d time.Duration) {
        var i int
        for {
            ch <- i
            i++
            time.Sleep(d)
        }
    }
    
    func reader(out chan int) {
        for x := range out {
            fmt.Println(x)
        }
    }
    
    func main() {
        ch := make(chan int)
        out := make(chan int)
        go producer(ch, 100*time.Millisecond)
        go producer(ch, 250*time.Millisecond)
        go reader(out)
        for i := range ch {
            out <- i
        }
    }


    Ссылка на интерактивное WebGL демо
    Как мы видим, первый producer генерирует числа каждые 100мс, второй — каждые 250мс, а reader получает числа от обоих продюсеров сразу же. Мультиплексирование, по-сути, происходит в функции main.

    Fan-out


    Противоположностью fan-in является fan-out или workers паттерн. Множество горутин читают из одного канала, забирая на обработку какие-то данные и эффективно распределяя работу между ядрами CPU. Отсюда и название "workers". В Go реализовывать этот паттерн очень просто — запустите пачку горутин, передав канал через параметр и пишите в этот канал ваши данные, а мультиплексирование и распределение будет происходит автоматически благодаря рантайму Go.
    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    func worker(tasksCh <-chan int, wg *sync.WaitGroup) {
        defer wg.Done()
        for {
            task, ok := <-tasksCh
            if !ok {
                return
            }
            d := time.Duration(task) * time.Millisecond
            time.Sleep(d)
            fmt.Println("processing task", task)
        }
    }
    
    func pool(wg *sync.WaitGroup, workers, tasks int) {
        tasksCh := make(chan int)
    
        for i := 0; i < workers; i++ {
            go worker(tasksCh, wg)
        }
    
        for i := 0; i < tasks; i++ {
            tasksCh <- i
        }
    
        close(tasksCh)
    }
    
    func main() {
        var wg sync.WaitGroup
        wg.Add(36)
        go pool(&wg, 36, 50)
        wg.Wait()
    }


    Ссылка на интерактивное WebGL демо
    Одна вещь, на которую тут хотелось бы обратить внимание — параллелизм. Лего заметить, что горутины-воркеры бегут параллельно, забирая себе «работу» по каналам, одна за другой. По данной анимации можно также увидеть, что горутины делают это почти одновременно. К сожалению, пока что в анимации не видно, где горутина реально работает, а где блокируется, и также тут временной масштаб уже близок к порогу погрешности ошибки, но конкретно эта анимация была записана на программе, бегущей на 4 ядрах, тоесть с GOMAXPROCS=4. Чуть дальше мы рассмотрим этот вопрос подробнее.

    А пока что, давайте попробуем что-то посложнее — воркеры, у которых есть свои, саб-воркеры.
    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    const (
        WORKERS    = 5
        SUBWORKERS = 3
        TASKS      = 20
        SUBTASKS   = 10
    )
    
    func subworker(subtasks chan int) {
        for {
            task, ok := <-subtasks
            if !ok {
                return
            }
            time.Sleep(time.Duration(task) * time.Millisecond)
            fmt.Println(task)
        }
    }
    
    func worker(tasks <-chan int, wg *sync.WaitGroup) {
        defer wg.Done()
        for {
            task, ok := <-tasks
            if !ok {
                return
            }
    
            subtasks := make(chan int)
            for i := 0; i < SUBWORKERS; i++ {
                go subworker(subtasks)
            }
            for i := 0; i < SUBTASKS; i++ {
                task1 := task * i
                subtasks <- task1
            }
            close(subtasks)
        }
    }
    
    func main() {
        var wg sync.WaitGroup
        wg.Add(WORKERS)
        tasks := make(chan int)
    
        for i := 0; i < WORKERS; i++ {
            go worker(tasks, &wg)
        }
    
        for i := 0; i < TASKS; i++ {
            tasks <- i
        }
    
        close(tasks)
        wg.Wait()
    }


    Ссылка на интерактивное WebGL демо
    Здорово. Конечно, можно было сделать больше и воркеров, и саб-воркеров, но я стремился сделать анимацию максимально наглядной.

    Есть гораздо более сложные паттерны, воркеров с сабворкерами со своими сабворкерами, и каналы, которые сами передаются по каналам, но идея fan-out должна быть понятна.

    Серверы


    Следующий популярный паттерн, похожий на fan-out, это серверы. Он отличается тем, что горутины стартуют динамически, выполняют необходимую работу и завершаются. И довольно часто этот паттерн применяется для реализации серверов — слушаем порт, принимаем соединение, стартуем горутину, которая дальше займется входящим запросом, передав ей соединение, а в это время слушаем дальше, ожидая следующее соединение. Это достаточно удобно и позволяет реализовать эффективный сервер, выдерживающий 10K соединений, очень просто. Вгляните на следующий пример:
    package main
    
    import "net"
    
    func handler(c net.Conn) {
        c.Write([]byte("ok"))
        c.Close()
    }
    
    func main() {
        l, err := net.Listen("tcp", ":5000")
        if err != nil {
            panic(err)
        }
        for {
            c, err := l.Accept()
            if err != nil {
                continue
            }
            go handler(c)
        }
    }


    Ссылка на интерактивное WebGL демо
    Этот пример не слишком интересен — по-сути, тут ничего особо не происходит. Хотя, конечно, под капотом там заботливо спрятана громадная сложность и алгоритмы. «Простота сложна».

    Но давайте добавим немного активности в наш сервер, и, скажем, добавим логгер, в который каждая горутина будет писать адрес клиента.
    package main
    
    import (
        "fmt"
        "net"
        "time"
    )
    
    func handler(c net.Conn, ch chan string) {
        ch <- c.RemoteAddr().String()
        c.Write([]byte("ok"))
        c.Close()
    }
    
    func logger(ch chan string) {
        for {
            fmt.Println(<-ch)
        }
    }
    
    func server(l net.Listener, ch chan string) {
        for {
            c, err := l.Accept()
            if err != nil {
                continue
            }
            go handler(c, ch)
        }
    }
    
    func main() {
        l, err := net.Listen("tcp", ":5000")
        if err != nil {
            panic(err)
        }
        ch := make(chan string)
        go logger(ch)
        go server(l, ch)
        time.Sleep(10 * time.Second)
    }


    Ссылка на интерактивное WebGL демо
    Достаточно демонстративно, не так ли? На этой анимации видно, что наш логгер может быстро стать узким местом, если количество соединений будет расти, а логгер будет не слишком быстрым (скажем, он будет сереализовать данные и куда-то еще отправлять). Но мы можем решить это, использовав уже знакомый нам паттерн fan-out. Давайте напишем это.

    Сервер+Воркер


    Пример сервера с воркером будет чуть более продвинутым вариантом только что озвученного решения. Он не только стартует логгер в нескольких горутинах, но и собирает с них данные с результатами (скажем, результат записи на удаленный сервис).
    Посмотрим на код и анимацию:
    package main
    
    import (
        "net"
        "time"
    )
    
    func handler(c net.Conn, ch chan string) {
        addr := c.RemoteAddr().String()
        ch <- addr
        time.Sleep(100 * time.Millisecond)
        c.Write([]byte("ok"))
        c.Close()
    }
    
    func logger(wch chan int, results chan int) {
        for {
            data := <-wch
            data++
            results <- data
        }
    }
    
    func parse(results chan int) {
        for {
            <-results
        }
    }
    
    func pool(ch chan string, n int) {
        wch := make(chan int)
        results := make(chan int)
        for i := 0; i < n; i++ {
            go logger(wch, results)
        }
        go parse(results)
        for {
            addr := <-ch
            l := len(addr)
            wch <- l
        }
    }
    
    func server(l net.Listener, ch chan string) {
        for {
            c, err := l.Accept()
            if err != nil {
                continue
            }
            go handler(c, ch)
        }
    }
    
    func main() {
        l, err := net.Listen("tcp", ":5000")
        if err != nil {
            panic(err)
        }
        ch := make(chan string)
        go pool(ch, 4)
        go server(l, ch)
        time.Sleep(10 * time.Second)
    }


    Ссылка на интерактивное WebGL демо
    Мы улучшили наш сервер, эффективно распределив задачу для логгера между 4-мя горутинами, но всё равно видим, что логгер таки может стать узким местом. Тысячи соединений сходятся в одном канале. перед тем как мультиплексироваться между горутинами. Но, конечно, это случится уже при гораздо больших нагрузках, чем в предыдущем варианте.

    Решето Эратосфена


    Но довольно fan-in/fan-out экспериментов. Давайте посмотрим на более интересные пример. Один из моих любимых — это «Решето Эратосфена» на горутинах и каналах, найденное в докладе "Go Concurrency Patterns". Решето Эратосфена это древний алгоритм нахождения простых чисел до заданного лимита. Его суть заключается в последовательном вычеркивании всех чисел, делящихся на каждое последующее найденное простое число. Алгоритм «в лоб» не слишком эффективен, особенно на мультиядерных машинах.

    Вариант реализации этого алгоритма с горутинами и каналами запускает по одной горутине на каждое найденное простое число, и эта горутина фильтрует числа, которые на него делятся. Когда же найдено первое простое число в горутине — оно отправляется в главную горутину(main) и выводится на экран. Этот алгоритм тоже далеко не самый эффективный, но я нахожу его потрясающе элегантным. Вот сам код:
    // A concurrent prime sieve
    package main
    
    import "fmt"
    
    // Send the sequence 2, 3, 4, ... to channel 'ch'.
    func Generate(ch chan<- int) {
        for i := 2; ; i++ {
            ch <- i // Send 'i' to channel 'ch'.
        }
    }
    
    // Copy the values from channel 'in' to channel 'out',
    // removing those divisible by 'prime'.
    func Filter(in <-chan int, out chan<- int, prime int) {
        for {
            i := <-in // Receive value from 'in'.
            if i%prime != 0 {
                out <- i // Send 'i' to 'out'.
            }
        }
    }
    
    // The prime sieve: Daisy-chain Filter processes.
    func main() {
        ch := make(chan int) // Create a new channel.
        go Generate(ch)      // Launch Generate goroutine.
        for i := 0; i < 10; i++ {
            prime := <-ch
            fmt.Println(prime)
            ch1 := make(chan int)
            go Filter(ch, ch1, prime)
            ch = ch1
        }
    }


    А теперь взгляните на анимацию.

    Ссылка на интерактивное WebGL демо
    Не забудьте покрутить его интерактивно в 3D пространстве по ссылке выше. Мне очень нравится, как иллюстративен этот пример и изучение его в 3D может помочь понять сам алгоритм лучше. Мы видим, что первая горутина(generate) посылает первое простое число (2) в main, затем стартует первая горутина-фильтр, отсеивающая двойки, затем тройки, пятерки, семерки… и каждый раз новое найденное простое число отправляется в main — это особенно хорошо видно при виде сверху. Красивый алгоритм, в том числе и в 3D.

    GOMAXPROCS


    Теперь давайте вернёмся к нашему примеру с воркерами. Помните, я написал, что этот пример был запущен с GOMAXPROCS=4? Это потому что все эти анимации не нарисованы, это реальные трейсы реальных программ.

    Для начала, давайте освежим память и вспомним, что такое GOMAXPROCS:
    GOMAXPROCS устанавливает максимальное количество ядер CPU, которые могут исполнять код одновременно


    Я изменил код воркеров слегка, чтобы они делали реальную работу. нагружая процессор, а не просто спали. Затем запустил код без каких либо изменений на Linux-машине с двумя процессорами по 12 ядер каждый — сначала с GOMAXPROCS=1, затем с GOMAXPROCS=24.

    Итак. первая анимация показывает одну и ту же программу, бегущую на 1-м ядре, вторая — на 24-х ядрах.


    WebGL анимация 1 WebGL анимация 24
    Скорость анимации времени разная в этих примерах (я хотел, чтобы все анимации занимали фиксированное время по высоте), но разница должна быть очевидна. При GOMAXPROCS=1 следующий воркер забирает работу (читает из канала) только когда освободилось ядро процессора и предыдущая горутина отработала свою порцию. С 24-мя ядрами, горутины почти сразу же разбирают задачи и ускорение огромно.

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

    Утечки горутин


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

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

    Посмотрите:

    Ссылка на интерактивное WebGL демо
    Мне больно даже смотреть на это :) Каждая линия — это потраченные ресурсы компьютера и бомба с часовым механизмом для вашей программы.

    Concurrency is not Parallelism


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


    Если вкратце, то:
    Параллелизм — это просто много штук, запущенных параллельно.
    Concurrency — это способ структурировать программу.

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

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

    Итак, вот это параллелизм. Просто много штук, бегущих параллельно.

    Ссылка на интерактивное WebGL демо

    И вот это параллелизм. Ещё больше параллельных штук, что, впрочем, ничего не меняет.

    Ссылка на интерактивное WebGL демо

    Но вот это — concurrency:


    И вот это:


    И вот это тоже concurrency:


    Как это было сделано?


    Чтобы создать эти анимации, я написал две программы — gotracer и gothree.js. Первая делает следующие вещи:
    • парсит AST-дерево исходного кода примеров на Go (ещё один плюс простой грамматики Go) и вставляет специальный вывод на событиях, относящихся к concurrency — старт/стоп горутины, запись/чтения в канал, создание канала
    • запускает модифицированную программу
    • анализирует вывод, и генерирует специальный JSON с ивентами и таймштампами

    Пример результирующего JSON-а:


    Далее, gothree.js использует мощь шикарной библиотеки Three.js, чтобы рисовать и анимировать эти данные в 3D с помощью WebGL. Небольшой враппер, чтобы втиснуть это в одну демо-страничку и готово.

    Впрочем, этот подход очень ограничен. Мне приходилось очень аккуратно подбирать примеры, переименовывать каналы, чтобы получать корректный трейс. С этим подходом нет легкого способа связать каналы между горутинами, если они называются внутри функции иначе. Также есть проблемы с таймингом — вывод в консоль занимает порой больше времени, чем запуск горутины, запись в канал и выход, поэтому в некоторых примерах мне приходилось чуть подтюнивать, вставляя time.Sleep (в примере с таймерами анимация чуть некорректна поэтому).

    Вобщем-то. это основная причина, по которой я пока-что не открываю код. Сейчас я играюсь ещё с execution tracer-ом Дмитрия Вьюкова — он, похоже, даёт нужный уровень детализации, хотя не содержит информации о том, что именно было отправлено в канал. Возможно есть ещё какие-то способы достичь максимально детального трейса, буду исследовать дальше. Пишите мне в твиттер или тут в комментариях, если есть идеи. Было бы круто, чтобы этот инструмент в итоге перерос в 3D concurrency-дебаггер, применимый к абсолютно любой программе на Go, без исключений.

    Эта статья изначально была в виде доклада на Go Meetup-е во Львове (23 января 2016), затем опубликована на английском языке в моём блоге. Также на HackerNews и на Reddit.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 21

      –1
      Оканкаренситальная статья, Ваня!
        +2
        Отличный способ для новичка понять как работает многопоточность. Даже если использовать другие языки, думаю, смысл остается тот же. Был на Вашем докладе во Львове, очень интересно и понятно это разъяснили. Надеюсь, визуализация переберется и на другие ЯП.
          +2
          Ну concurrency visualiser есть в Visual Studio, емнип, уже 7 лет как, и как мощный отладочный инструмент, а не просто отрисовка графиков.
            0
            Но в Go это не нужно, как и отладчик и IDE! :D
              –2
              Дженерики и эксепшены забыли, что ж вы так.
                0
                Точно, это серьезное упущение!
            +1
            Ну, как отладочный инструмент, в Go есть упомянутый в статье execution tracer. Ну и visualiser в Visual Studio показывает только в 2D. Будь это открытый продукт, не вижу причин, почему бы нельзя было добавить поддержку 3-го измерения в визуализацию.
            0
            Спасибо. Я тоже считаю, что трехмерные визуализации — слишком недоиспользованный инструмент в обучении. И причина этому — пока ещё большая сложность их создания для рядового человека.
            0
            Вот уж заморочился так заморочился, спасибо в карму )
              0
              Спасибо. Два уикенда )
              • UFO just landed and posted this here
            • UFO just landed and posted this here
                +1
                Очень круто!
                  0
                  Представленная реализация алгортима решета Эратосфена таковой совсем не яляется, а в остальном красиво :)
                    0
                    Почему же она таковой совсем не является, если не секрет? :)
                      0
                      Потому что указанный алгоритм основывается на последовательном вычеркивании чисел, кратных найденному простому. Причем кратность определяется не делением и проверкой остатка, а сложением. См., например, параграф «Решето Эратосфена и однострочники» в Еще раз о поиске простых чисел. Распараллелить этот алгоритм, кстати, весьма нетривиальная задача.
                        0
                        Оригинальный алгоритм, если верить истории, заключался в протыкании дощечки в тех местах, где были написаны составные числа — отсюда и название «решето». Делать это последовательно или параллельно, как «отсчитывать» кратные числа — это детали имплементации, не более.
                        За линк спасибо, интересно.
                          0
                          Для древних греков немаловажный вопрос заключался в том как определить, является ли число составным: прибавлять (что очень просто — протыкай себе на соответствующий отсчет) или делить (что гораздо сложнее: определите без калькулятора, делится 1077 на 37 или нет? и так на каждом шаге). Насчет параллелизации такого алгоритма тоже вопросы возникают… 
                          В общем, вы мою мысль поняли, а дальше — дело хозяйское. Дискусии разводить повода нет.

                  Only users with full accounts can post comments. Log in, please.