Я верю что за многопоточными(параллельными) программами бушующее (которое уже почти настоящее ) и потому изначально проектировал свой ЯП со "встроенным" параллельным программированием.
Этот пост я писал как ответ на сообщения AlexRK
в ветке про дедлоки. Но решил вынести его в отдельный тред, так как хочу обсудить ниже описанную концепцию.
CAB>>Можно запретить закольцовывание ожиданий потоков. KP>А как это можно сделать на этапе компиляции?
Я точно не знаю, но вот как собираюсь сделать: следуя идее "автоматическое управление потоками"("У нас есть авт. управление памятью, почему мы до сих пор управляем потоками в ручную?"), я хочу "выкинуть" само понятие "поток"(как некая отдельная сущность), и сделать почти как в Go "все функции выполняются параллельно"(в Go не все), т.е. каждый вызов выполняется в новом потоке. В тоже время внутри функции как-бы(с точки зрения программиста) существует только один поток, т.е. если я например пишу чистую функцию(не обращающуюся к общим переменным), то о синхронизации в общем-то можно почти не думать.
Внутри функции есть два вида синхронизации:
По результатам:
...
let f = foo() //Вызов мееедленой функции возвращающей Int
... //Делаем что-то пока foo выполняется
let s = f + 1 //Здесь останавливаемся и ждём пока foo вернёт результат(ели ещё не вернула)
..
И по выходу:
let bar = {) //Это функция bar
...
foo() //Вызов мееедленой функции
...
(} //Не выходим пока foo не завершится
Для случая когда все функции чистые проблема дедлоков решается "сама собой", так как вызовы представляют из себя дерево без перекрёстных зависимостей, например:
Когда программа остановится дерево вызовов будет иметь такой вид:
Такой синхронизации достаточно для реализации некоторых простых вещей, например для параллельной обработки данных:
def procData = {l:LIST[String]) //Функция подсчитывает количество символов в списке строк.
let s = LIST[Int] //Список int'ов
foreach i in l => s :: size(i) //Число символов подсчитывается для каждой строки, затем результат помещается список int'ов(без сохранения порядка).
let r = 0 //Для суммирования
foreach i in s => r := r + i //Сложение всех int'ов*
r
(Int}
*Сразу подсчитать сумму("foreach i in l => r := r + size(i)") в рамках этой модели не получится, так как на второй итерации оператор + не сможет прочитать r пока не завершится вызов size() из первой, т.е. параллелизма не будет.
Когда появляются общие переменные всё становится несколько сложнее, например:
И соответственно граф зависимостей будет выглядеть так:
Что определённо плохо, так как в более сложных случаях может привести к дедлокам. Чтобы этого избежать нужно сделать чтобы в любой момент времени существовала только одна из зависимостей(a или b). Для чего я хочу использовать хитрый алгоритм синхронизации для общих переменных:
У каждой общей переменной может быть только одна изменяющая её функция, т.е. один писатель много читателей(это ограничения для оптимизации).
Каждая функция-писатель является критической секцией и захватывает записываемые переменные на время своего выполнения, кроме того пред захватом она читает все требуемые общие переменные, т.е это примерно эквивалентно передаче их через аргументы, например для foo из пример выше:
def foo = {v:Int) v1 := v1 + v (}
Чтение:
1.Захварываем общую переменную на чтение(если выполняется запись ждём).
2.Читаем.
3.Освобождаем.
Запись:
1.Читаем используемые переменные.
2.Захватываем переменную на запись(если выполняется чтение ждём завершения не допуская старта новых чтений).
3.Выполняем тело функции-писателя.
4.Освобождаем.
Таким образом, например, если первой начнёт выполнятся foo то она предварительно(перед началом выполнения тела) прочитает переменную v2, и во время выполнения тела зависимости "a" не будет, будет только "b", т.е. bar будет ожидать завершения foo.
Существенный недостаток такого подхода это значительная растрата ресурсов: много потоков и копирований значений. Но, думаю это можно оптимизировать.
Это будет работать? Как по вашему, сложно писать в таком стиле(когда каждый вызов это отдельный поток)?
Спасибо.
Между тем,что я думаю,тем,что я хочу сказать,тем,что я,как мне кажется,говорю,и тем,что вы хотите услышать,тем,что как вам кажется,вы слышите,тем,что вы понимаете,стоит десять вариантов возникновения непонимания.Но всё-таки давайте попробуем...(Э.Уэллс)
Здравствуйте, C.A.B, Вы писали:
CAB> Я точно не знаю, но вот как собираюсь сделать: следуя идее "автоматическое управление потоками"("У нас есть авт. управление памятью, почему мы до сих пор управляем потоками в ручную?"), я хочу "выкинуть" само понятие "поток"(как некая отдельная сущность), и сделать почти как в Go "все функции выполняются параллельно"(в Go не все), т.е. каждый вызов выполняется в новом потоке. В тоже время внутри функции как-бы(с точки зрения программиста) существует только один поток, т.е. если я например пишу чистую функцию(не обращающуюся к общим переменным), то о синхронизации в общем-то можно почти не думать.
Это и есть парадигма функционального программирования. Там нет "общих переменных" (наверное, правильно будет сказать глобальные переменные). Но вот последовательность есть. Правда есть еще и "ленивые вычисления". В общем, все это уже было.
Хорошая идея разделить участки там где идет только чтение, и там где допустимо изменение хорошая, но императивная парадигма основана на присваивании. Нужна другая парадигма. Например, машины управляемые потоками данных.. Ну, и еще кое-что есть.. в этом духе
Здравствуйте, C.A.B, Вы писали:
CAB> Я точно не знаю, но вот как собираюсь сделать: следуя идее "автоматическое управление потоками"("У нас есть авт. управление памятью, почему мы до сих пор управляем потоками в ручную?"), я хочу "выкинуть" само понятие "поток"(как некая отдельная сущность), и сделать почти как в Go "все функции выполняются параллельно"(в Go не все), т.е. каждый вызов выполняется в новом потоке. В тоже время внутри функции как-бы(с точки зрения программиста) существует только один поток, т.е. если я например пишу чистую функцию(не обращающуюся к общим переменным), то о синхронизации в общем-то можно почти не думать.
Если функции чистые, то получается зависимость по данным. Семантика программы при этом не зависит от того, есть вообще параллелизм или нет. Функции могут быть выполнены и последовательно, и параллельно — результат будет одинаков. Главное, что соблюдается требуемая последовательность вызовов.
Но — функции это не потоки. Потоки могут обмениваться сообщениями. У вас аналога такого варианта не предусмотрено?
А где есть примитивы send и receive, там могут быть и дедлоки. И разрулить такие конфликты на этапе компиляции сложно, если вообще возможно...
CAB> Когда появляются общие переменные всё становится несколько сложнее, например:
Думаю, такая синхронизация убьет всю достигнутую за счет параллелизма производительность.
Здравствуйте, C.A.B, Вы писали:
CAB>Внутри функции есть два вида синхронизации: CAB> По результатам: CAB>
CAB> ...
CAB> let f = foo() //Вызов мееедленой функции возвращающей Int
CAB> ... //Делаем что-то пока foo выполняется
CAB> let s = f + 1 //Здесь останавливаемся и ждём пока foo вернёт результат(ели ещё не вернула)
CAB> ..
CAB>
Примерно так работают современные процессоры. Только на уровне отдельных операций (ассемблерных команд), а не функций. Компилятор им в этом старается помогать, максимально разнося по коду связанные по данным операции (чтобы процессору всегда было, чем заняться).
Работает это довольно средненько — именно поэтому индустрия пошла по пути многоядерных процессоров, с явно выделенными потоками исполнения, а не по пути процессоров с очень большой степенью параллелизмом на уровне команд.
Здравствуйте, C.A.B, Вы писали:
CAB>Я верю что за многопоточными(параллельными) программами бушующее (которое уже почти настоящее ) и потому изначально проектировал свой ЯП со "встроенным" параллельным программированием.
А разве сейчас многопоточные программы это трудность? Чего ты хочешь этим достичь ? Скорости, масштабируемости ? Какие проблемы решит ?
Здравствуйте, AlexRK, Вы писали: ARK>Но — функции это не потоки*. Потоки могут обмениваться сообщениями. У вас аналога такого варианта не предусмотрено?
*В данном контексте под "потоки" я подразумеваю параллельно выполняющейся код(потоки выполнения/инструкций), не зависимо от того обмениваются ли они сообщениями.
Пока нет, пока я не вижу особой нужды в них(как в прочих wait-конструкциях).
Типичный паттерн использования сообщений выглядит примерно так (кто пользуется сообщениями поправьте если я не прав):
//Поток-отправитель
...
while(...){
...
trd.send("Some message")
...
}
...
//Поток-получаетль
...
while(...){
val m = waitMessage() //Ждём сообщение
... //Обрабатываем
}
...
Если каждый вызов это отдельный поток, то можно сделать так:
Т.е. каждое сообщение будет обрабатывается в своём потоке. Иначе говоря, мы не заставляем потоки ждать, мы просто создаём их тогда когда они нужны.
ARK>Думаю, такая синхронизация убьет всю достигнутую за счет параллелизма производительность.
Почему вы так думаете? Есть алгоритмы для оптимизации всего этого (если вам интересно я распишу подробней).
Между тем,что я думаю,тем,что я хочу сказать,тем,что я,как мне кажется,говорю,и тем,что вы хотите услышать,тем,что как вам кажется,вы слышите,тем,что вы понимаете,стоит десять вариантов возникновения непонимания.Но всё-таки давайте попробуем...(Э.Уэллс)
Здравствуйте, C.A.B, Вы писали:
CAB>Это будет работать? Как по вашему, сложно писать в таком стиле(когда каждый вызов это отдельный поток)?
Простите, а зачем делать только такой стиль? Нужны и последовательные конструкции. Вы Occam видели? Судя по вопросам, нет. А для написания таких языков это даже не классика — это как текст из Библии для христианина. Обратите внимание, как легко в нём синтаксически организуется и параллельное, и последовательное выполнение, и связь между частями.
Ознакомьтесь, и по результатам можете вернуться к своему проектированию.
Здравствуйте, Kubyshev Andrey, Вы писали:
KA>Здравствуйте, C.A.B, Вы писали:
CAB>>Я верю что за многопоточными(параллельными) программами бушующее (которое уже почти настоящее ) и потому изначально проектировал свой ЯП со "встроенным" параллельным программированием.
KA>А разве сейчас многопоточные программы это трудность?
Здравствуйте, Kubyshev Andrey, Вы писали: KA>А разве сейчас многопоточные программы это трудность?
Сейчас, если ты например захочешь написать параллельное приложение(даже банальный GUI — work с парой потоков), тебе нужно будет думать о многих вещах: о запуске потоков, о корректном завершении потоков, о приостановке(wiat'ы), об синхронизации, об разделяемых ресурсах и т.п. KA>Чего ты хочешь этим достичь ?
Я хочу чтобы программисты (в частности я ) тратили меньше времени и сил на написание параллельных приложений. В идеале полностью автоматизировать управление потоками(процессорным временем), как это было сделано с памятью. KA>Скорости, масштабируемости ?
Список приоритетов выглядит примерно так: простота, надёжность, масштабируемость, скорость. KA>Какие проблемы решит ?
Прежде всего проблему освоения вычислительной мощности современных процессоров (разробы процессоров вдохнут с облегчением ).
Между тем,что я думаю,тем,что я хочу сказать,тем,что я,как мне кажется,говорю,и тем,что вы хотите услышать,тем,что как вам кажется,вы слышите,тем,что вы понимаете,стоит десять вариантов возникновения непонимания.Но всё-таки давайте попробуем...(Э.Уэллс)
Здравствуйте, C.A.B, Вы писали:
CAB>Я верю что за многопоточными(параллельными) программами бушующее (которое уже почти настоящее ) и потому изначально проектировал свой ЯП со "встроенным" параллельным программированием. CAB>Этот пост я писал как ответ на сообщения AlexRK
в ветке про дедлоки. Но решил вынести его в отдельный тред, так как хочу обсудить ниже описанную концепцию.
Параллелить вычисления надо там, где нужна производительность. Очень часто в таких случаях речь идет о том, чтобы выполнять множество однотипных действий. Это идеально ложится на видеокарты, вот только ваш подход не ложиться на видеокарты.
А то, о чем ты говоришьо, уже реализовано в функциональных языках.
CAB>Это будет работать? Как по вашему, сложно писать в таком стиле(когда каждый вызов это отдельный поток)?
В этом стиле писать будет очень сложно, потому что задача усложняется во много раз. По сути параррелизм у нас может появляться в самых неожиданных местах. Синхронизация между потоками тема нетривиальная, и везде ее стараются свести к минимуму. Потому что вечно где-нить что-нить упустишь, и оно выстрелит в самый неподходящий момент но неизвестно когда. Ты же предлагаешь это делать в каждой функции
Здравствуйте, batu, Вы писали: B>Хорошая идея разделить участки там где идет только чтение, и там где допустимо изменение хорошая, но императивная парадигма основана на присваивании. Нужна другая парадигма. Например, машины управляемые потоками данных.. Ну, и еще кое-что есть.. в этом духе
Потоки данных штука хорошая(хоть и сложная), в будущем я бы хотел добавить их так-же. Но пока "разбираюсь" с потоками инструкций
Между тем,что я думаю,тем,что я хочу сказать,тем,что я,как мне кажется,говорю,и тем,что вы хотите услышать,тем,что как вам кажется,вы слышите,тем,что вы понимаете,стоит десять вариантов возникновения непонимания.Но всё-таки давайте попробуем...(Э.Уэллс)
Здравствуйте, C.A.B, Вы писали:
CAB>Т.е. каждое сообщение будет обрабатывается в своём потоке. Иначе говоря, мы не заставляем потоки ждать, мы просто создаём их тогда когда они нужны.
Понятно. Нечто вроде SCOOP в Eiffel. Но как быть, когда между потоками нет отношения "родитель-подчиненный"? К примеру, один поток — это диспетчер IO-вызовов, а другой — наша программа. Диспетчер мы создать не можем, он существовал в операционной системе до того, как мы появились на свет, и останется после того, как мы умрем. (И наоборот — наша программа не создается этим диспетчером, ее запускает пользователь.) А общаться с ним надо. Тут для вашей модели нужен какой-то аналог общих переменных на уровне ОС.
Кстати, как мне кажется, прием-отправка сообщений и синхронизация на общих переменных — дуальные сущности, можно одно реализовать поверх другого. Если дедлок возможен в одном месте, то и в другом тоже. Но сообщения мне больше нравятся, как-то они прозрачнее для понимания.
ARK>>Думаю, такая синхронизация убьет всю достигнутую за счет параллелизма производительность. CAB>Почему вы так думаете? Есть алгоритмы для оптимизации всего этого (если вам интересно я распишу подробней).
Сомнительно все это выглядит. Формализовать ощущения пока не могу.
Кстати, вроде у вас дедлок все равно возможен. Псевдокод:
shared int a;
shared int b;
void foobar()
{
foo(); // spawn потока, захват переменной "a", возврат управления
bar(); // spawn потока, захват переменной "b", возврат управления - все успешно проходит
}
void foo()
{
use(a);
... // тут чего-то долго делаем
bar(); // ВЫЗОВ НОМЕР 2: попытка захвата переменной "b", а вот и дедлок
}
void bar()
{
use(b);
}
Или же у вас функция должна блокировать все общие переменные на всю глубину вызовов, которые изнутри ее делаются. Это, по-моему, просто невозможно, т.к. в конечном счете вся программа вызывается из одной функции.
ARK>Понятно. Нечто вроде SCOOP в Eiffel. Но как быть, когда между потоками нет отношения "родитель-подчиненный"? К примеру, один поток — это диспетчер IO-вызовов, а другой — наша программа. Диспетчер мы создать не можем, он существовал в операционной системе до того, как мы появились на свет, и останется после того, как мы умрем. (И наоборот — наша программа не создается этим диспетчером, ее запускает пользователь.) А общаться с ним надо. Тут для вашей модели нужен какой-то аналог общих переменных на уровне ОС.
Как вариант можно установить такие отношения, как например это делается в WinAPI: мы "говорим" системе "вот у нас есть callback функция(WinProc), дёргай её если что" ARK>Кстати, как мне кажется, прием-отправка сообщений и синхронизация на общих переменных — дуальные сущности, можно одно реализовать поверх другого. Если дедлок возможен в одном месте, то и в другом тоже. Но сообщения мне больше нравятся, как-то они прозрачнее для понимания.
Можно так реализовать:
ARK>Кстати, вроде у вас дедлок все равно возможен. Псевдокод: ARK>
ARK> void foo()
ARK> { //!!Для читаемых перменных здесь(пред выполнением тела функции) выполняется чтение(копирование) значения,
ARK> //т.е. foo() не начнёт выполнение пока не прочтиает "a", и затем сразу освободи его, так что новый вызов foo() так же сможет выполнить чтение.
ARK> //Для записываемых переменных здесь выполнятся захват(с освобождением по завершении выполнения тела функции), так что новый вызов foo() не начнётся пока не будет
ARK> //выполнен предыдущий.
ARK> use(a);
ARK> ... // тут чего-то долго делаем
ARK> bar(); // ВЫЗОВ НОМЕР 2: попытка захвата переменной "b", а вот и дедлок
ARK> }
ARK>
Как-то так. ARK>Или же у вас функция должна блокировать все общие переменные на всю глубину вызовов, которые изнутри ее делаются.
Нет, только в передела одной функции.
Синхронизация доступа к переменным вообще нужна чтобы не дать одному потоку "втихаря" подменить значение переменной пока другой поток с ней работает. Например в Scala меня делали грустным внезапные и не предсказуемые исключения (NullPointerException) в коде на подобии:
...
if(n != null){ //n - разделяемая переменная
n.m()} //Здесь поток иногда вылетает по NullPointerException, потому-что другой поток обнулил n между проверкой и вызовов метода m()
...
PS:
Пока писал ответ придумал как подвесить это :
var v1 = 0
def w = {) //Функция-писатель
let s = r()
v1 := 1 + s
(}
def r = {) //Функция-читатель
v1
(}
Т.е. w блокирует переменную v1 и вызывает r, затем ожидает завершения r. Стартуя r останавливается в ожидание завершения w. Дедлок.
Возможное решение — не блокировать v1 на время выполнения функции-писателя, и обновлять значение по её завершению.
В общем я ещё подумаю над этим.
Между тем,что я думаю,тем,что я хочу сказать,тем,что я,как мне кажется,говорю,и тем,что вы хотите услышать,тем,что как вам кажется,вы слышите,тем,что вы понимаете,стоит десять вариантов возникновения непонимания.Но всё-таки давайте попробуем...(Э.Уэллс)
Здравствуйте, C.A.B, Вы писали:
CAB>Здравствуйте, batu, Вы писали: B>>Хорошая идея разделить участки там где идет только чтение, и там где допустимо изменение хорошая, но императивная парадигма основана на присваивании. Нужна другая парадигма. Например, машины управляемые потоками данных.. Ну, и еще кое-что есть.. в этом духе CAB>Потоки данных штука хорошая(хоть и сложная), в будущем я бы хотел добавить их так-же. Но пока "разбираюсь" с потоками инструкций
Ничего сложного. Пришли данные (или изменились)- это событие. Событие имеет значение. Вот формирование события не изменят данные, а анализирует. Т.е. оно может выполняться параллельно. Поток инструкций обрабатывающий это событие пользуется пространством конкретного объекта и значением события в качестве параметра. Так что вполне может выполняться параллельно синхронизируясь только по доступу к объекту. Если это в ООП.
CAB>Внутри функции есть два вида синхронизации: CAB> По результатам: CAB>
CAB> ...
CAB> let f = foo() //Вызов мееедленой функции возвращающей Int
CAB> ... //Делаем что-то пока foo выполняется
CAB> let s = f + 1 //Здесь останавливаемся и ждём пока foo вернёт результат(ели ещё не вернула)
CAB> ..
CAB>
Это
1. Много где называется promise http://en.wikipedia.org/wiki/Futures_and_promises
2. Как только это появляется в коде, код становится недетерминированным (когда primise вернет результат? а никому не известно)
CAB> Когда появляются общие переменные всё становится несколько сложнее, например: CAB> Для чего я хочу использовать хитрый алгоритм синхронизации для общих переменных:
Все. Забудь о параллелизации. Вспоминай дедлоки и т.п.
CAB> У каждой общей переменной может быть только одна изменяющая её функция, т.е. один писатель много читателей(это ограничения для оптимизации). CAB>Каждая функция-писатель является критической секцией и захватывает записываемые переменные на время своего выполнения, кроме того пред захватом она читает все требуемые общие переменные, т.е это примерно эквивалентно передаче их через аргументы, например для foo из пример выше:
Ага. Часть из эти переменных будет promise'ами. И обновится они не смогут, когда придут данные, потому что их уже захватила критическая секция.
CAB>Существенный недостаток такого подхода это значительная растрата ресурсов: много потоков и копирований значений. Но, думаю это можно оптимизировать.
Эрланг спокойно живет и с множеством потоков и с копированием значений. Потому что
1. Потоки не имеют отношения к потокам системы
2. Все данные иммутабельны
3. Так как данные иммутабельные, можно копировать/передавать только указатели на данные в памяти
CAB>Это будет работать? Как по вашему, сложно писать в таком стиле(когда каждый вызов это отдельный поток)?
Неправильные приоритеты. Надежность должна быть первой. Из нее органично вытекает масштабируемость, кстати. Простота должна решаться параллельно с надежностью.
KA>>Какие проблемы решит ? CAB>Прежде всего проблему освоения вычислительной мощности современных процессоров (разробы процессоров вдохнут с облегчением ).
И таки вы, AlexRK, правы, это подход(синхронизация общих переменных) не спасает от деделоков:
var v1 = 0
doer a = {)
while(ra()){ //Будет крутится и ждать v1 == 0
...
}
v1 := 1
(}
def ra = {)
v1 == 0
(Bool}
doer b = {) //Будет крутится и ждать v1 == 1while(rb()){
...
}
v1 := 1
(}
def rb = {)
v1 == 1
(Bool}
Печально
Между тем,что я думаю,тем,что я хочу сказать,тем,что я,как мне кажется,говорю,и тем,что вы хотите услышать,тем,что как вам кажется,вы слышите,тем,что вы понимаете,стоит десять вариантов возникновения непонимания.Но всё-таки давайте попробуем...(Э.Уэллс)
Здравствуйте, Mamut, Вы писали: M>2. Как только это появляется в коде, код становится недетерминированным (когда primise вернет результат? а никому не известно)
Какая разница когда она его вернёт(кроме того что вызвавший поток остановится не в месте вызова, а в месте где потребуется результат)? CAB>>Каждая функция-писатель является критической секцией и захватывает записываемые переменные на время своего выполнения, кроме того пред захватом она читает все требуемые общие переменные, т.е это примерно эквивалентно передаче их через аргументы, например для foo из пример выше: M>Ага. Часть из эти переменных будет promise'ами. И обновится они не смогут, когда придут данные, потому что их уже захватила критическая секция.
Только одна функция может изменять некоторую общую переменную, она же является критической секцией, т.е. переменные обновляются внутри критической секции, а но не вне. M>Эрланг спокойно живет и с множеством потоков и с копированием значений. Потому что M>1. Потоки не имеют отношения к потокам системы M>2. Все данные иммутабельны M>3. Так как данные иммутабельные, можно копировать/передавать только указатели на данные в памяти
Как, в нескольких словах, в Эрланге реализовано например такое:
Т.е. когда большая структура немного изменяется, и получается вторая большая структура. Как оптимизировано копирование.
Между тем,что я думаю,тем,что я хочу сказать,тем,что я,как мне кажется,говорю,и тем,что вы хотите услышать,тем,что как вам кажется,вы слышите,тем,что вы понимаете,стоит десять вариантов возникновения непонимания.Но всё-таки давайте попробуем...(Э.Уэллс)
M>>2. Как только это появляется в коде, код становится недетерминированным (когда primise вернет результат? а никому не известно) CAB>Какая разница когда она его вернёт(кроме того что вызвавший поток остановится не в месте вызова, а в месте где потребуется результат)?
Скобки отвечают на твой вопрос
CAB>>>Каждая функция-писатель является критической секцией и захватывает записываемые переменные на время своего выполнения, кроме того пред захватом она читает все требуемые общие переменные, т.е это примерно эквивалентно передаче их через аргументы, например для foo из пример выше: M>>Ага. Часть из эти переменных будет promise'ами. И обновится они не смогут, когда придут данные, потому что их уже захватила критическая секция. CAB>Только одна функция может изменять некоторую общую переменную, она же является критической секцией, т.е. переменные обновляются внутри критической секции, а но не вне.
Если это общие переменные, то...?
CAB>Как, в нескольких словах, в Эрланге реализовано например такое: CAB>
Добавление нового элемента в конец равнозначно:
1. Создаем новый cons cell, содержащий 1
2. Переписываем два указателя:
2.1 С последнего cons списка на новый cons.
2.2 С нового cons списка на nil