Re: Симуляция на реактивных потоках
От: Sinclair Россия https://github.com/evilguest/
Дата: 20.01.17 07:01
Оценка:
Здравствуйте, C.A.B, Вы писали:
CAB>Идея в том, чтобы реализовать подобный инструмент, но построенный на асинхронном обмене сообщениями (на реактивных потоках в частности) вместо пошагового выселения состояния. Т.е. в целом это будет работать также: значения с выходов блоков будут передаватся на входы, но в виде посылки сообщений, а вычисления блоков будет выполнятся асинхронно.
А как вы делаете синхронизацию?
Ну, то есть если у нас два блока генерируют одинаковые последовательности из (1, -1, 1, -1, ...), а третий их складвает, то в пошаговой модели мы имеем умножение на 2: (2, -2, 2, -2, ...).
А в асинхронной — всё, что угодно, если нет гарантии порядка прихода сообщений: {0, -2, -2, 0, 2, -2, 0, ...)
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[2]: Симуляция на реактивных потоках
От: C.A.B LinkedIn
Дата: 20.01.17 18:35
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>А как вы делаете синхронизацию?

S>Ну, то есть если у нас два блока генерируют одинаковые последовательности из (1, -1, 1, -1, ...), а третий их складвает, то в пошаговой модели мы имеем умножение на 2: (2, -2, 2, -2, ...).

Есть такая штука, о которой я узнал из мира CUDA, называется «barrier synchronization».
Суть в том, что третий блок ждёт пока будут получены оба сообщения, и только за тем складывает и ждёт следующие 2 сообщения.
Между тем,что я думаю,тем,что я хочу сказать,тем,что я,как мне кажется,говорю,и тем,что вы хотите услышать,тем,что как вам кажется,вы слышите,тем,что вы понимаете,стоит десять вариантов возникновения непонимания.Но всё-таки давайте попробуем...(Э.Уэллс)
Re[3]: Симуляция на реактивных потоках
От: Sinclair Россия https://github.com/evilguest/
Дата: 23.01.17 05:25
Оценка:
Здравствуйте, C.A.B, Вы писали:
CAB>Есть такая штука, о которой я узнал из мира CUDA, называется «barrier synchronization».
CAB>Суть в том, что третий блок ждёт пока будут получены оба сообщения, и только за тем складывает и ждёт следующие 2 сообщения.
Это как бы убивает весь эффект от параллелизма. Плюс к этому, вам надо существенно переписывать логику блоков.
Потому, что блок B может генерировать просто константу. В обычном случае у него просто GetValue() переписывается в return 1;. В реактивном случае ему надо "не забыть" сгенерировать нужное количество событий.
Опять же — а что делать, если блоку C скормить на вход блок A дважды? Он будет генерировать вдвое меньше сообщений, чем блок А?
В общем, схема сама по себе не плоха — просто надо понимать, что сопоставление её с классическим образцом затруднено.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[4]: Симуляция на реактивных потоках
От: C.A.B LinkedIn
Дата: 23.01.17 22:31
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Это как бы убивает весь эффект от параллелизма...


С чего вдруг?

S>...В реактивном случае ему надо "не забыть" сгенерировать нужное количество событий.


Не обязательно, для подобных блоков мы можем ожидать только первого сообщения и далее хранить его пока не будет получено новое. Если система типов достаточно мощная, то выбор логики будет выполнятся прозрачно для пользователя. Если вы читаете Скалу, здесь как раз пример блоков где один вход "константный" а другий "ждущий": https://github.com/AlexCAB/MathAct/blob/master/mathact_examples/src/main/scala/examples/common/SimplePidExample.scala (Не работает вставка ссылок в MS Edge)

S>В общем, схема сама по себе не плоха — просто надо понимать, что сопоставление её с классическим образцом затруднено.


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

S>...сопоставление её с классическим образцом...


Дедушка Фрейд икнул
Между тем,что я думаю,тем,что я хочу сказать,тем,что я,как мне кажется,говорю,и тем,что вы хотите услышать,тем,что как вам кажется,вы слышите,тем,что вы понимаете,стоит десять вариантов возникновения непонимания.Но всё-таки давайте попробуем...(Э.Уэллс)
Re[5]: Симуляция на реактивных потоках
От: Sinclair Россия https://github.com/evilguest/
Дата: 24.01.17 10:05
Оценка:
Здравствуйте, C.A.B, Вы писали:

CAB>Не обязательно, для подобных блоков мы можем ожидать только первого сообщения и далее хранить его пока не будет получено новое.

Не понял, кто должен быть подобен? Т.е. где делается выбор — в блоке А или в блоке C?
CAB>Если система типов достаточно мощная, то выбор логики будет выполнятся прозрачно для пользователя. Если вы читаете Скалу, здесь как раз пример блоков где один вход "константный" а другий "ждущий": https://github.com/AlexCAB/MathAct/blob/master/mathact_examples/src/main/scala/examples/common/SimplePidExample.scala (Не работает вставка ссылок в MS Edge)
Недостаточно хорошо читаю, чтобы понять, что происходит.

S>>...сопоставление её с классическим образцом...

CAB>Дедушка Фрейд икнул
Не знаю, причём тут фрейд. Я просто лет 15 тому назад участвовал в проекте по переписыванию подобной системы с TurboPascal на Delphi. И весь смысл был в том, что собсно сами симуляции, написанные ещё в 80х, нужно было гонять на новой системе. Если мы меняем вычислительную модель, то нужно уметь как-то переписывать симуляции со старого формата в новый — причём так, чтобы поведение сохранялось.
Сходу не вполне понятно, как это сделать, с учётом изменения понятия "шаг".
Либо я не догоняю в вашу идею — может быть, у вас понятие шагов в каком-то виде есть, и запрос "дай мне значение с шага N" отличается от запроса "дай мне значение с шага N+1".

А если писать симуляцию с нуля, то и вопросов возникать не будет — ну да, вот такие блоки, вот они так работают. Хочешь потреблять значение блока одновременно в двух местах- втыкай мультиплексор, иначе будешь получать смешение времён.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[6]: Симуляция на реактивных потоках
От: C.A.B LinkedIn
Дата: 24.01.17 21:50
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Не понял, кто должен быть подобен? Т.е. где делается выбор — в блоке А или в блоке C?


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

S>...Я просто лет 15 тому назад участвовал в проекте по переписыванию подобной системы с TurboPascal на Delphi. И весь смысл был в том, что собсно сами симуляции, написанные ещё в 80х...


Я верю что никто в здравом уме и трезвой памяти не будет занимается переписыванием симуляций из 80-х Зачем их вообще переписывать? В моём понимании это вещь одноразовая: запилили -> посчитали -> выкинули. Но может быть и нет, спорить не буду.

S>А если писать симуляцию с нуля, то и вопросов возникать не будет — ну да, вот такие блоки, вот они так работают...


Ну да, так и есть. Вообще конкретно этот проект (что я написал) больше исследовательский, из разряда "а что если взять реактивные потоки запилить на них..."
Так что если вам это тоже интересно, можете скачать и поиграть.
Между тем,что я думаю,тем,что я хочу сказать,тем,что я,как мне кажется,говорю,и тем,что вы хотите услышать,тем,что как вам кажется,вы слышите,тем,что вы понимаете,стоит десять вариантов возникновения непонимания.Но всё-таки давайте попробуем...(Э.Уэллс)
Re[7]: Симуляция на реактивных потоках
От: Sinclair Россия https://github.com/evilguest/
Дата: 25.01.17 05:15
Оценка:
Здравствуйте, C.A.B, Вы писали:
CAB>Я имею ввиду блок который генерирует константу (пусть будет А, шлющий только 1 сообщение на старте), и блок с логикой оптимизированной для получения оной (пусть буде С, получающий сообщение от А и хранящий его)
По-моему, это убивает всю идею на корню. Блок C не должен знать, получает ли он константу или синусоиду — иначе порушится вся модульность.

CAB>Я верю что никто в здравом уме и трезвой памяти не будет занимается переписыванием симуляций из 80-х Зачем их вообще переписывать? В моём понимании это вещь одноразовая: запилили -> посчитали -> выкинули. Но может быть и нет, спорить не буду.

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

CAB>Ну да, так и есть. Вообще конкретно этот проект (что я написал) больше исследовательский, из разряда "а что если взять реактивные потоки запилить на них..."

CAB>Так что если вам это тоже интересно, можете скачать и поиграть.
Увы, я уже от этого всего слишком далёк.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[8]: Симуляция на реактивных потоках
От: C.A.B LinkedIn
Дата: 25.01.17 08:46
Оценка:
Здравствуйте, Sinclair, Вы писали:

CAB>>Я имею ввиду блок который генерирует константу (пусть будет А, шлющий только 1 сообщение на старте), и блок с логикой оптимизированной для получения оной (пусть буде С, получающий сообщение от А и хранящий его)

S>По-моему, это убивает всю идею на корню. Блок C не должен знать, получает ли он константу или синусоиду — иначе порушится вся модульность.

Да, в какой-то степени модульность страдает, но определённо не "порушится вся". Конкретно в моей реализации входы и выходы блоков имеют тип (Double, Int, String, Object, so on), так что соединять можно только те из них которые имеют совместимый тип.
С другой стороны, образно говоря, благодаря типизированости блок знает что за значение он получает и что с ним делать.
Между тем,что я думаю,тем,что я хочу сказать,тем,что я,как мне кажется,говорю,и тем,что вы хотите услышать,тем,что как вам кажется,вы слышите,тем,что вы понимаете,стоит десять вариантов возникновения непонимания.Но всё-таки давайте попробуем...(Э.Уэллс)
Re[9]: Симуляция на реактивных потоках
От: Sinclair Россия https://github.com/evilguest/
Дата: 26.01.17 09:50
Оценка:
Здравствуйте, C.A.B, Вы писали:

CAB>Да, в какой-то степени модульность страдает, но определённо не "порушится вся". Конкретно в моей реализации входы и выходы блоков имеют тип (Double, Int, String, Object, so on), так что соединять можно только те из них которые имеют совместимый тип.

Продолжаю непонимать, как это отвечает на вопрос про ожидание событий. В моём примере все типы — Int.
Дальше что?
Как мне написать блок C, чтобы результат его работы был понятен без 0.75 сорокаградусной? Напомню: работа блока C — складывать значения двух входов.
Блок заранее не знает, что к нему подключат — генератор константы или синусоиды.
Вот у меня есть блок типа A, который генерирует -1 в степени N.
Если я подключаю на вход к блоку C два идентичных блока A1 и A2, то ожидаю, что C выдаст на выходе 2*(-1)^N.
Того же самого я ожидаю, подключая к обоим входам блока С блок A1.
В "обычной схеме", где есть в явном виде "значения всех слотов на предыдущем шаге" и "значения всех слотов на новом шаге", которые меняются местами каждый такт, это интуитивно понятное ожидание выполняется без приседаний.
В "реактивной схеме" мы налетаем либо на то, что C генерирует мусор (при отказе от синхронизации), либо на генерацию 0, если блок C оборудован "ожиданием" каждого из сигналов.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re: Симуляция на реактивных потоках
От: Nick Linker Россия lj://_lcr_
Дата: 28.01.17 10:20
Оценка:
C.A.B,

CAB>Что думаете вы?

CAB>Спасибо!

Может я ошибаюсь, но первая возникшая мысль была, что идея аналогична библиотеке pipes (http://hackage.haskell.org/package/pipes). Я ещё поковыряюсь в вашей реализации, попробую разобраться. Может позже что-нибудь умное напишу
quicksort =: (($:@(<#[),(=#[),$:@(>#[)) ({~ ?@#)) ^: (1<#)
Re[2]: Симуляция на реактивных потоках
От: C.A.B LinkedIn
Дата: 30.01.17 07:32
Оценка:
Здравствуйте, Nick Linker, Вы писали:

NL>Может я ошибаюсь, но первая возникшая мысль была, что идея аналогична библиотеке pipes (http://hackage.haskell.org/package/pipes).


Спасибо, интересная сриминговая библиотека.
В целом похоже, но я позаимсвовал стриминговую модель из этой библитеки.
Между тем,что я думаю,тем,что я хочу сказать,тем,что я,как мне кажется,говорю,и тем,что вы хотите услышать,тем,что как вам кажется,вы слышите,тем,что вы понимаете,стоит десять вариантов возникновения непонимания.Но всё-таки давайте попробуем...(Э.Уэллс)
Re[10]: Симуляция на реактивных потоках
От: C.A.B LinkedIn
Дата: 30.01.17 10:27
Оценка: 33 (1)
Здравствуйте, Sinclair, Вы писали:

S>Как мне написать блок C, чтобы результат его работы был понятен без 0.75 сорокаградусной?


Ох, это очень плохая идея

S>Напомню: работа блока C — складывать значения двух входов.

S>Блок заранее не знает, что к нему подключат — генератор константы или синусоиды.

Он знает.

S>... либо на генерацию 0, если блок C оборудован "ожиданием" каждого из сигналов.


Если блок ожидает сигнала то он ничего не генерирует.

S>Вот у меня есть блок типа A, который генерирует -1 в степени N.

S>Если я подключаю на вход к блоку C два идентичных блока A1 и A2, то ожидаю, что C выдаст на выходе 2*(-1)^N.
S>Того же самого я ожидаю, подключая к обоим входам блока С блок A1.

Попробую объяснить на псевдокоде:
/* Определим блоки */

class A {                                                                     //Класс блока A имеет один выход 'out' который на старте последовательно посылает четыре значения на выход любого другого подключенного блока
    final Stream<Int> out = Stream{1, -1, 1, -1}                              //здесь это (-1)^N но может быть синусоида или что угодно. 
}                                                                        

class B {                                                                     //Класс блока B так же с одним выходом, посылает только одно значение (константу).
    final Unitary<Int> out = Unitary(2)
}

class Base {                                                                  //Базовый класс, скрывающий всю машинерию общую для блоков-матоператоров.

    protected abstract Int doCalc(Array<Int> input)                           //Операция которую должен реализовать субкласс 

    private Array<Queue<Int>> streamBuf = new Array()                         //Буфер для последовательностей входящих значений (для реализации пороговой синхронизации).
    private Array<Option<Int>> unitaryBuf = new Array()                       //Буфер для единичных входящих значений (констант).
                                                                              //Буферы изначально заполнены Queue() и None значениями, и имеющие количество элементов равное количеству подключений к соответсвующему входу.

    final Stream<Int> out = new Empty                                         //Выход, ничего не посылает на старте, "ожидает" вызова .send(Int) метода.

    private void chechIfAllValsReadyAndDoCalcIfSo(){
        while(streamBuf.notContainsEmptyQueue && unitaryBuf.notContainsNone){ //Если получены значения для всех входов,
            out.send(doCalc(                                                  //выполнить операцию и отправить результат на выход,
                streamBuf.map(_.dequeueFirst) ++ unitaryBuf.map(_.get)))      //и очистить буфер входа принимающего последовательность значений (не константу).                                              
        }
    }

    final Stream<Int> in = onReceive((index, value) -> {                      //Вход с логикой заточенной для получения последовательности значений,
        streamBuf[index].enqueue(value)                                       //index - номе подключения к данному входу.
        chechIfAllValsReadyAndDoCalcIfSo() 
    })
    final Unitary<Int> in = onReceive((index, value) -> {                     //Вход для получения единичных значений (констант). 
        unitaryBuf[index] = new Some(value)
        chechIfAllValsReadyAndDoCalcIfSo()
    })
}

class C extends Base {                                                        //Собственно операция суммирование
  protected Int doCalc(Array<Int> input) { return input.sum() } 
}

class P {                                                                     //Класс блока для вывода результата
    final Stream<Int> in = onReceive((_, value) -> { println(value) })
}


/* Создадим несколько блоков */

A a1 = new A() 
A a2 = new A() 
B b = new B() 
C c = new C()
P printer = new P()


/* Соберём из них пару примеров */

a1 ~> c                //Блоки 'a1', 'a2' генерирующие [1, -1, 1, -1], будут подключены с входу 'Stream<Int> in' (т.е. к входу с совместимым типом) блока 'c', 
a2 ~> c ~> printer     //выход блока 'c' будет подключен к 'printer'.
                       //Все четыре блока работают асинхронно, например:
                       // 1. Блок 'a1' получает первым свой квант процессорного времени и отправляет сразу все четыре значения.
                       // 2. Которые попадают в первую очередь в блок 'c' (streamBuf выглядит как [[1,-1,1,-1],[]]), 'c' не отправляет ничего на выход так как вторая очередь пуста.
                       // 3. Блок 'a2' выполняется затем и успевает отправить только 2 значения из 4-х.
                       // 4. Каждый раз при получении нового значения от 'a2', 'c' ставит его в очередь (streamBuf = [[1,-1,1,-1],[1]]) и так как нет пустых очередей 
                       //    забирает по одному элементу из каждой, выполняет операцию, и отправляет результат на свой выход.
                       // 5. Таким образом после обработки 2-х сообщений от 'a2': streamBuf = [[1,-1],[]], блоком 'printer' будет выведено: "2", "-2". 
                       // 6. Соответсвенно по получении оставшихся 2-х сообщений от 'a2': streamBuf = [[],[]], блоком 'printer' будет выведено: "2", "-2", "2", "-2". 
                       //PS: Такой прикольный синтаксис можно сделать в Scala, но в целом это примерно эквивалентно: 
                       //  a1.out.connectTo(c.in)
                       //  a2.out.connectTo(c.in)
                       //  c.out.connectTo(printer.in)

a1 ~> c                //Для блока 'b' генерирующего константу, всё примерно так-же, роме того что он будет подключен к входу 'Unitary<Int> in' с отличной логикой обработки значений.
b  ~> c ~> printer     //Например:
                       // 1. Блок 'a1' опять выполняется первым и отправляет сразу все четыре значения. 
                       // 2. streamBuf = [[1,-1,1,-1]], unitaryBuf = [None]
                       // 3. Блок 'b' выполняется и посылает своё единственное значение: streamBuf = [[1,-1,1,-1]], unitaryBuf = [Some(2)]
                       // 4. Блок 'c' выполняет операцию сложения для всех значений в очереди и отправляет полученные значения на выход.
                       // 5. Блок 'printer' выводит: "3", "1", "3", "1".


Как то так
Между тем,что я думаю,тем,что я хочу сказать,тем,что я,как мне кажется,говорю,и тем,что вы хотите услышать,тем,что как вам кажется,вы слышите,тем,что вы понимаете,стоит десять вариантов возникновения непонимания.Но всё-таки давайте попробуем...(Э.Уэллс)
Re[11]: Симуляция на реактивных потоках
От: Sinclair Россия https://github.com/evilguest/
Дата: 30.01.17 17:06
Оценка:
Здравствуйте, C.A.B, Вы писали:

CAB>Он знает.

По-моему, это — очень плохая идея.

S>>Вот у меня есть блок типа A, который генерирует -1 в степени N.

S>>Если я подключаю на вход к блоку C два идентичных блока A1 и A2, то ожидаю, что C выдаст на выходе 2*(-1)^N.
S>>Того же самого я ожидаю, подключая к обоим входам блока С блок A1.

CAB>Попробую объяснить на псевдокоде:

CAB>A a1 = new A()
CAB>A a2 = new A()
CAB>B b = new B()
CAB>C c = new C()
CAB>P printer = new P()


CAB>a1 ~> c //Блоки 'a1', 'a2' генерирующие [1, -1, 1, -1], будут подключены с входу 'Stream<Int> in' (т.е. к входу с совместимым типом) блока 'c',

CAB>a2 ~> c ~> printer //выход блока 'c' будет подключен к 'printer'.
CAB> //Все четыре блока работают асинхронно, например:
CAB> // 1. Блок 'a1' получает первым свой квант процессорного времени и отправляет сразу все четыре значения.
CAB> // 2. Которые попадают в первую очередь в блок 'c' (streamBuf выглядит как [1,-1,1,-1],[]]), 'c' не отправляет ничего на выход так как вторая очередь пуста.
CAB> // 3. Блок 'a2' выполняется затем и успевает отправить только 2 значения из 4-х.
CAB> // 4. Каждый раз при получении нового значения от 'a2', 'c' ставит его в очередь (streamBuf = [1,-1,1,-1],[1]]) и так как нет пустых очередей
CAB> // забирает по одному элементу из каждой, выполняет операцию, и отправляет результат на свой выход.
CAB> // 5. Таким образом после обработки 2-х сообщений от 'a2': streamBuf = [1,-1],[]], блоком 'printer' будет выведено: "2", "-2".
CAB> // 6. Соответсвенно по получении оставшихся 2-х сообщений от 'a2': streamBuf = [[],[]], блоком 'printer' будет выведено: "2", "-2", "2", "-2".
CAB> //PS: Такой прикольный синтаксис можно сделать в Scala, но в целом это примерно эквивалентно:
CAB> // a1.out.connectTo(c.in)
CAB> // a2.out.connectTo(c.in)
CAB> // c.out.connectTo(printer.in)

CAB>a1 ~> c //Для блока 'b' генерирующего константу, всё примерно так-же, роме того что он будет подключен к входу 'Unitary<Int> in' с отличной логикой обработки значений.

CAB>b ~> c ~> printer //Например:
CAB> // 1. Блок 'a1' опять выполняется первым и отправляет сразу все четыре значения.
CAB> // 2. streamBuf = [1,-1,1,-1]], unitaryBuf = [None]
CAB> // 3. Блок 'b' выполняется и посылает своё единственное значение: streamBuf = [1,-1,1,-1]], unitaryBuf = [Some(2)]
CAB> // 4. Блок 'c' выполняет операцию сложения для всех значений в очереди и отправляет полученные значения на выход.
CAB> // 5. Блок 'printer' выводит: "3", "1", "3", "1".
CAB>[/java]

CAB>Как то так

Вот смотрите.
1. В "традиционной" системе у нас есть блок типа C. Ему не надо вот эти 30 строк трудноотлаживаемой машинерии; вся его логика сводится к
Int Output1 { return Input1.GetValue() + Input2.GetValue()}
2. Что вы собираетесь делать с блоками, генерирующими бесконечные последовательности? У вас блок а1 собрался оптом запихать сразу всё. Если ему дать это сделать, то он напрочь забьёт очередь блока C, не дав шанса блоку а2.
3. По-прежнему не раскрыт вопрос о том, что делать с двукратным подключением блока a1 к блоку с. Оно вообще поддерживается?
4. Что делать со starvation? Где гарантия, что блок a2 вообще хоть когда-то получит свой квант процессорного времени?
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[12]: Симуляция на реактивных потоках
От: C.A.B LinkedIn
Дата: 30.01.17 19:42
Оценка: 33 (1)
Здравствуйте, Sinclair, Вы писали:

S>1. В "традиционной" системе у нас есть блок типа C. Ему не надо вот эти 30 строк трудноотлаживаемой машинерии; вся его логика сводится к

S>Int Output1 { return Input1.GetValue() + Input2.GetValue()}

Да, также вся эта возня с сообщения отъедает не мало процессорного времени. Используете подходящий инструмент.

S>2. Что вы собираетесь делать с блоками, генерирующими бесконечные последовательности? У вас блок а1 собрался оптом запихать сразу всё. Если ему дать это сделать, то он напрочь забьёт очередь блока C, не дав шанса блоку а2.

S>4. Что делать со starvation? Где гарантия, что блок a2 вообще хоть когда-то получит свой квант процессорного времени?

Для этого у нас есть back pressure алгоритм, который балансирует нагрузку (размеры очередей).

S>3. По-прежнему не раскрыт вопрос о том, что делать с двукратным подключением блока a1 к блоку с. Оно вообще поддерживается?


Да, просто два раза выполнить подключение:

a1 ~> c   
a1 ~> c ~> printer  //Результат: "2", "-2", "2", "-2".


Или три

a1 ~> c  
a1 ~> c ~> printer  //Результат: "3", "-3", "3", "-3". 
a1 ~> c
Между тем,что я думаю,тем,что я хочу сказать,тем,что я,как мне кажется,говорю,и тем,что вы хотите услышать,тем,что как вам кажется,вы слышите,тем,что вы понимаете,стоит десять вариантов возникновения непонимания.Но всё-таки давайте попробуем...(Э.Уэллс)
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.