Я просто не получаю продолжения!

Для чего они и чем они хороши?

У меня нет степени CS, и у меня есть опыт работы с VB6 -> ASP -> ASP.NET/C#. Кто-нибудь может объяснить это в ясной и сжатой форме?

9 ответов

Решение

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

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

Напомним, этот пример не является ни кратким, ни исключительно ясным. Это демонстрация мощного применения продолжений. Как программист на VB/ASP/C#, вы, возможно, не знакомы с концепцией системного стека или состояния сохранения, поэтому цель этого ответа - демонстрация, а не объяснение.

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

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

(define (fork)
  (display "forking\n")
  (call-with-current-continuation
   (lambda (cc)
     (enqueue (lambda ()
                (cc #f)))
     (cc #t))))

(define (context-switch)
  (display "context switching\n")
  (call-with-current-continuation
   (lambda (cc)
     (enqueue
      (lambda ()
        (cc 'nothing)))
     ((dequeue)))))

(define (end-process)
  (display "ending process\n")
  (let ((proc (dequeue)))
    (if (eq? proc 'queue-empty)
        (display "all processes terminated\n")
        (proc))))

Это предоставляет три глагола, которые может использовать функция - fork, context-switch и end-process. Операция fork разветвляет поток и возвращает #t в одном экземпляре и #f в другом. Операция переключения контекста переключает между потоками, и конечный процесс завершает поток.

Вот пример их использования:

(define (test-cs)
  (display "entering test\n")
  (cond
    ((fork) (cond
              ((fork) (display "process 1\n")
                      (context-switch)
                      (display "process 1 again\n"))
              (else (display "process 2\n")
                    (end-process)
                    (display "you shouldn't see this (2)"))))
    (else (cond ((fork) (display "process 3\n")
                        (display "process 3 again\n")
                        (context-switch))
                (else (display "process 4\n")))))
  (context-switch)
  (display "ending process\n")
  (end-process)
  (display "process ended (should only see this once)\n"))

Выход должен быть

entering test
forking
forking
process 1
context switching
forking
process 3
process 3 again
context switching
process 2
ending process
process 1 again
context switching
process 4
context switching
context switching
ending process
ending process
ending process
ending process
ending process
ending process
all processes terminated
process ended (should only see this once)

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

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

Вы, вероятно, понимаете их лучше, чем думаете.

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

Пример Python:

try:
    broken_function()
except SomeException:
    # jump to here
    pass

def broken_function():
    raise SomeException() # go back up the stack
    # stuff that won't be evaluated

Генераторы являются примерами продолжения "только вниз". Они позволяют коду повторно войти в цикл, например, чтобы создать новые значения.

Пример Python:

def sequence_generator(i=1):
    while True:
        yield i  # "return" this value, and come back here for the next
        i = i + 1

g = sequence_generator()
while True:
    print g.next()

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

Один из способов представить продолжение - это использовать процессорный стек. Когда вы вызываете с текущим продолжением c, он вызывает вашу функцию "c", а параметр, переданный "c", является вашим текущим стеком со всеми вашими автоматическими переменными (представленными как еще одна функция, вызывайте ее "k". "). Тем временем процессор начинает создавать новый стек. Когда вы вызываете "k", он выполняет инструкцию "возврат из подпрограммы" (RTS) в исходном стеке, возвращая вас обратно в контекст оригинального "call-with-current-продолжением" ("call-cc" с этого момента). на) и позволяет продолжить вашу программу, как и раньше. Если вы передали параметр в "k", он становится возвращаемым значением "call-cc".

С точки зрения вашего исходного стека, call-cc выглядит как обычный вызов функции. С точки зрения "с" ваш исходный стек выглядит как функция, которая никогда не возвращается.

Есть старая шутка о математике, который поймал льва в клетке, забравшись в клетку, заперев ее и объявив себя вне клетки, пока все остальное (включая льва) было внутри нее. Продолжения немного похожи на клетку, а "с" немного похож на математика. Ваша основная программа считает, что "c" находится внутри нее, а "c" считает, что ваша основная программа находится внутри "k".

Вы можете создавать произвольные структуры потока управления, используя продолжения. Например, вы можете создать библиотеку потоков. "yield" использует "call-cc" для помещения текущего продолжения в очередь, а затем переходит к тому, что находится в начале очереди. Семафор также имеет собственную очередь приостановленных продолжений, и поток перепланируется путем удаления его из очереди семафора и помещения его в основную очередь.

По сути, продолжением является способность функции останавливать выполнение и затем возвращаться туда, где она остановилась в более поздний момент времени. В C# вы можете сделать это, используя ключевое слово yield. Я могу вдаваться в подробности, если вы хотите, но вы хотели краткое объяснение.;-)

Я все еще "привыкаю" к продолжениям, но один из способов думать о них, который я считаю полезным, - это абстракция концепции счетчика программ (ПК). ПК "указывает" на следующую инструкцию для выполнения в памяти, но, конечно, эта инструкция (и почти каждая инструкция) указывает, неявно или явно, на следующую инструкцию, а также на любые инструкции, которые должны обслуживать прерывания. (Даже инструкция NOOP неявно выполняет JUMP для следующей инструкции в памяти. Но если происходит прерывание, это обычно включает JUMP для какой-либо другой инструкции в памяти.)

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

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

Таким образом, продолжение можно рассматривать как высокоуровневую модель ПК, поэтому концептуально оно включает в себя обычный вызов / возврат процедуры (точно так же, как древнее железо выполняло вызов / возврат процедуры через низкоуровневый JUMP, или GOTO, инструкции и запись команд). ПК по вызову и восстановление его по возвращении), а также исключения, потоки, сопрограммы и т. Д.

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

Конечно, хотя на каждый компьютер (основной процессор) обычно может приходиться только один ПК, на самом деле существует много "активных" объектов PC-ish, как упоминалось выше. Вектор прерываний содержит группу, стек больше, некоторые регистры могут содержать некоторые и т. Д. Они "активируются", когда их значения загружаются в аппаратный ПК, но продолжения являются абстракцией концепции, а не ПК или их точным эквивалентом. (не существует присущей концепции "основного" продолжения, хотя мы часто думаем и кодируем в этих терминах, чтобы все было довольно просто).

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

В C# у вас есть доступ к двум продолжениям. Один, доступ через returnПозволяет продолжить метод с того места, где он был вызван. Другой, доступ через throwПозволяет продолжить метод при ближайшем совпадении catch,

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

Continuation callback = return;
callMeLater(callback);

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

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

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

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

Другие вопросы по тегам