Control Flow
Julia предоставляет различные конструкции управления потоком:
- Compound Expressions:
начало
и;
. - Conditional Evaluation:
если
-иначе если
-иначе
и?:
(тернарный оператор). - Short-Circuit Evaluation: логические операторы
&&
(“и”) и||
(“или”), а также цепные сравнения. - Repeated Evaluation: Loops:
пока
идля
. - Exception Handling:
попробовать
-поймать
,error
иthrow
. - Tasks (aka Coroutines):
yieldto
.
Первые пять механизмов управления потоком стандартны для языков программирования высокого уровня. Task
не являются стандартными: они обеспечивают нелокальное управление потоком, что позволяет переключаться между временно приостановленными вычислениями. Это мощная конструкция: как обработка исключений, так и кооперативная многозадачность реализованы в Julia с использованием задач. Обычное программирование не требует прямого использования задач, но определенные проблемы могут быть решены гораздо проще с помощью задач.
Compound Expressions
Иногда удобно иметь одно выражение, которое последовательно вычисляет несколько подвыражений, возвращая значение последнего подвыражения в качестве своего значения. Существует два конструкта Julia, которые это осуществляют: блоки begin
и цепочки ;
. Значение обоих составных выражений — это значение последнего подвыражения. Вот пример блока begin
:
julia> z = begin
x = 1
y = 2
x + y
end
3
Поскольку это довольно маленькие, простые выражения, их можно легко разместить в одной строке, где синтаксис цепочки ;
оказывается полезным:
julia> z = (x = 1; y = 2; x + y)
3
Этот синтаксис особенно полезен с краткой однострочной формой определения функции, введенной в Functions. Хотя это типично, нет требования, чтобы блоки begin
были многострочными или чтобы цепочки ;
были однострочными:
julia> begin x = 1; y = 2; x + y end
3
julia> (x = 1;
y = 2;
x + y)
3
Conditional Evaluation
Условная оценка позволяет частям кода оцениваться или не оцениваться в зависимости от значения булевого выражения. Вот анатомия синтаксиса условного оператора if
-elseif
-else
:
if x < y
println("x is less than y")
elseif x > y
println("x is greater than y")
else
println("x is equal to y")
end
Если условное выражение x < y
истинно, то соответствующий блок выполняется; в противном случае оценивается условное выражение x > y
, и если оно истинно, выполняется соответствующий блок; если ни одно из выражений не истинно, выполняется блок else
. Вот как это работает:
julia> function test(x, y)
if x < y
println("x is less than y")
elseif x > y
println("x is greater than y")
else
println("x is equal to y")
end
end
test (generic function with 1 method)
julia> test(1, 2)
x is less than y
julia> test(2, 1)
x is greater than y
julia> test(1, 1)
x is equal to y
Блоки elseif
и else
являются необязательными, и можно использовать столько блоков elseif
, сколько необходимо. Условные выражения в конструкции if
-elseif
-else
оцениваются до тех пор, пока первое из них не оценится как true
, после чего соответствующий блок выполняется, и никакие дальнейшие условные выражения или блоки не оцениваются.
if
блоки "протекают", т.е. они не вводят локальную область видимости. Это означает, что новые переменные, определенные внутри if
условий, могут использоваться после блока if
, даже если они не были определены ранее. Таким образом, мы могли бы определить функцию test
выше как
julia> function test(x,y)
if x < y
relation = "less than"
elseif x == y
relation = "equal to"
else
relation = "greater than"
end
println("x is ", relation, " y.")
end
test (generic function with 1 method)
julia> test(2, 1)
x is greater than y.
Переменная relation
объявлена внутри блока if
, но используется снаружи. Однако, полагаясь на это поведение, убедитесь, что все возможные пути кода определяют значение для переменной. Следующее изменение в приведенной выше функции приводит к ошибке во время выполнения.
julia> function test(x,y)
if x < y
relation = "less than"
elseif x == y
relation = "equal to"
end
println("x is ", relation, " y.")
end
test (generic function with 1 method)
julia> test(1,2)
x is less than y.
julia> test(2,1)
ERROR: UndefVarError: `relation` not defined in local scope
Stacktrace:
[1] test(::Int64, ::Int64) at ./none:7
if
блоки также возвращают значение, что может показаться неинтуитивным для пользователей, приходящих из многих других языков. Это значение просто является возвращаемым значением последнего выполненного оператора в выбранной ветке, так что
julia> x = 3
3
julia> if x > 0
"positive!"
else
"negative..."
end
"positive!"
Обратите внимание, что очень короткие условные операторы (однострочные) часто выражаются с использованием короткозамыкательной оценки в Julia, как описано в следующем разделе.
В отличие от C, MATLAB, Perl, Python и Ruby – но как Java и несколько других более строгих типизированных языков – это ошибка, если значение условного выражения не является true
или false
:
julia> if 1
println("true")
end
ERROR: TypeError: non-boolean (Int64) used in boolean context
Эта ошибка указывает на то, что условие было неправильного типа: Int64
, а не требуемого Bool
.
Так называемый "тернарный оператор", ?:
, тесно связан с синтаксисом if
-elseif
-else
, но используется там, где требуется условный выбор между одиночными значениями выражений, в отличие от условного выполнения более длинных блоков кода. Он получил свое название, потому что является единственным оператором в большинстве языков, принимающим три операнда:
a ? b : c
Выражение a
, перед ?
, является условным выражением, и тернарная операция вычисляет выражение b
, перед :
, если условие a
истинно, или выражение c
, после :
, если оно ложно. Обратите внимание, что пробелы вокруг ?
и :
обязательны: выражение, такое как a?b:c
, не является допустимым тернарным выражением (но новая строка допустима после ?
и :
).
Самый простой способ понять это поведение — увидеть пример. В предыдущем примере вызов println
разделяется всеми тремя ветвями: единственный реальный выбор — это какая литеральная строка будет напечатана. Это можно записать более лаконично, используя тернарный оператор. Для ясности давайте сначала попробуем двустороннюю версию:
julia> x = 1; y = 2;
julia> println(x < y ? "less than" : "not less than")
less than
julia> x = 1; y = 0;
julia> println(x < y ? "less than" : "not less than")
not less than
Если выражение x < y
истинно, то всё выражение тернарного оператора оценивается как строка "less than"
, а в противном случае оно оценивается как строка "not less than"
. Оригинальный пример с тремя вариантами требует объединения нескольких использований тернарного оператора:
julia> test(x, y) = println(x < y ? "x is less than y" :
x > y ? "x is greater than y" : "x is equal to y")
test (generic function with 1 method)
julia> test(1, 2)
x is less than y
julia> test(2, 1)
x is greater than y
julia> test(1, 1)
x is equal to y
Чтобы облегчить цепочку, оператор ассоциируется справа налево.
Важно отметить, что как и в if
-elseif
-else
, выражения до и после :
оцениваются только в том случае, если условное выражение оценивается как true
или false
, соответственно:
julia> v(x) = (println(x); x)
v (generic function with 1 method)
julia> 1 < 2 ? v("yes") : v("no")
yes
"yes"
julia> 1 > 2 ? v("yes") : v("no")
no
"no"
Short-Circuit Evaluation
Операторы &&
и ||
в Julia соответствуют логическим операциям "и" и "или", соответственно, и обычно используются для этой цели. Однако у них есть дополнительное свойство короткого замыкания при оценке: они не обязательно оценивают свой второй аргумент, как объяснено ниже. (Существуют также побитовые операторы &
и |
, которые могут использоваться как логические "и" и "или" без поведения короткого замыкания, но будьте осторожны, что &
и |
имеют более высокий приоритет, чем &&
и ||
для порядка оценки.)
Краткая оценка является довольно похожей на условную оценку. Это поведение встречается в большинстве императивных языков программирования, имеющих логические операторы &&
и ||
: в серии логических выражений, соединенных этими операторами, оценивается только минимальное количество выражений, необходимых для определения окончательного логического значения всей цепочки. Некоторые языки (например, Python) называют их and
(&&
) и or
(||
). Явно это означает, что:
- В выражении
a && b
подвыражениеb
оценивается только в том случае, еслиa
оценивается какtrue
. - В выражении
a || b
подвыражениеb
оценивается только в том случае, еслиa
оценивается какfalse
.
Причина в том, что a && b
должно быть false
, если a
равно false
, независимо от значения b
, и наоборот, значение a || b
должно быть истинным, если a
истинно, независимо от значения b
. Операторы &&
и ||
ассоциируются вправо, но &&
имеет более высокий приоритет, чем ||
. Легко поэкспериментировать с этим поведением:
julia> t(x) = (println(x); true)
t (generic function with 1 method)
julia> f(x) = (println(x); false)
f (generic function with 1 method)
julia> t(1) && t(2)
1
2
true
julia> t(1) && f(2)
1
2
false
julia> f(1) && t(2)
1
false
julia> f(1) && f(2)
1
false
julia> t(1) || t(2)
1
true
julia> t(1) || f(2)
1
true
julia> f(1) || t(2)
1
2
true
julia> f(1) || f(2)
1
2
false
Вы можете легко экспериментировать таким же образом с ассоциативностью и приоритетом различных комбинаций операторов &&
и ||
.
Это поведение часто используется в Julia для формирования альтернативы очень коротким операторам if
. Вместо if <cond> <statement> end
можно написать <cond> && <statement>
(что можно прочитать как: <cond> и затем <statement>). Аналогично, вместо if ! <cond> <statement> end
можно написать <cond> || <statement>
(что можно прочитать как: <cond> или же <statement>).
Например, рекурсивная процедура вычисления факториала может быть определена так:
julia> function fact(n::Int)
n >= 0 || error("n must be non-negative")
n == 0 && return 1
n * fact(n-1)
end
fact (generic function with 1 method)
julia> fact(5)
120
julia> fact(0)
1
julia> fact(-1)
ERROR: n must be non-negative
Stacktrace:
[1] error at ./error.jl:33 [inlined]
[2] fact(::Int64) at ./none:2
[3] top-level scope
Булевы операции без короткого замыкания могут быть выполнены с помощью побитовых булевых операторов, введенных в Mathematical Operations and Elementary Functions: &
и |
. Это обычные функции, которые поддерживают синтаксис инфиксного оператора, но всегда оценивают свои аргументы:
julia> f(1) & t(2)
1
2
false
julia> t(1) | t(2)
1
2
true
Так же, как и условные выражения, используемые в if
, elseif
или тернарном операторе, операнды &&
или ||
должны быть логическими значениями (true
или false
). Использование нелогического значения в любом месте, кроме последнего элемента в условной цепочке, является ошибкой:
julia> 1 && true
ERROR: TypeError: non-boolean (Int64) used in boolean context
С другой стороны, любой тип выражения может быть использован в конце условной цепочки. Он будет оценен и возвращен в зависимости от предшествующих условий:
julia> true && (x = (1, 2, 3))
(1, 2, 3)
julia> false && (x = (1, 2, 3))
false
Repeated Evaluation: Loops
Существует два конструкта для повторной оценки выражений: цикл while
и цикл for
. Вот пример цикла while
:
julia> i = 1;
julia> while i <= 3
println(i)
global i += 1
end
1
2
3
Цикл while
оценивает условное выражение (i <= 3
в данном случае), и пока оно остается true
, продолжает также оценивать тело цикла while
. Если условное выражение равно false
, когда цикл while
впервые достигается, тело никогда не оценивается.
Цикл for
упрощает написание общих идиом повторяющейся оценки. Поскольку подсчет вверх и вниз, как в приведенном выше цикле while
, является очень распространенным, его можно выразить более лаконично с помощью цикла for
:
julia> for i = 1:3
println(i)
end
1
2
3
Здесь 1:3
— это объект range
, представляющий последовательность чисел 1, 2, 3. Цикл for
проходит через эти значения, последовательно присваивая каждое из них переменной i
. В общем, конструкция for
может перебирать любой "итерируемый" объект (или "контейнер"), от диапазона, такого как 1:3
или 1:3:13
(это StepRange
, указывающий на каждое третье целое число 1, 4, 7, …, 13) до более общих контейнеров, таких как массивы, включая iterators defined by user code или внешние пакеты. Для контейнеров, отличных от диапазонов, альтернативное (но полностью эквивалентное) ключевое слово in
или ∈
обычно используется вместо =
, так как это делает код более читаемым:
julia> for i in [1,4,0]
println(i)
end
1
4
0
julia> for s ∈ ["foo","bar","baz"]
println(s)
end
foo
bar
baz
В различных разделах руководства будут представлены и обсуждены различные типы итерируемых контейнеров (см., например, Multi-dimensional Arrays).
Одно довольно важное различие между предыдущей формой цикла while
и формой цикла for
заключается в области видимости, в течение которой переменная видима. Цикл for
всегда вводит новую переменную итерации в своем теле, независимо от того, существует ли переменная с тем же именем в окружающей области. Это означает, что, с одной стороны, i
не нужно объявлять перед циклом. С другой стороны, она не будет видима за пределами цикла, и переменная с тем же именем вне цикла не будет затронута. Вам понадобится либо новая интерактивная сессия, либо другое имя переменной, чтобы протестировать это:
julia> for j = 1:3
println(j)
end
1
2
3
julia> j
ERROR: UndefVarError: `j` not defined in `Main`
julia> j = 0;
julia> for j = 1:3
println(j)
end
1
2
3
julia> j
0
Используйте for outer
, чтобы изменить поведение и повторно использовать существующую локальную переменную.
Смотрите Scope of Variables для подробного объяснения области видимости переменных, outer
, и как это работает в Julia.
Иногда удобно завершить повторение цикла while
до того, как условие проверки станет ложным, или остановить итерацию в цикле for
до достижения конца итерируемого объекта. Это можно сделать с помощью ключевого слова break
:
julia> i = 1;
julia> while true
println(i)
if i >= 3
break
end
global i += 1
end
1
2
3
julia> for j = 1:1000
println(j)
if j >= 3
break
end
end
1
2
3
Без ключевого слова break
вышеуказанный цикл while
никогда не завершится самостоятельно, а цикл for
будет выполняться до 1000. Оба этих цикла завершаются досрочно с помощью break
.
В других обстоятельствах полезно иметь возможность остановить итерацию и сразу перейти к следующей. Ключевое слово continue
выполняет это:
julia> for i = 1:10
if i % 3 != 0
continue
end
println(i)
end
3
6
9
Это несколько искусственный пример, поскольку мы могли бы более ясно продемонстрировать то же поведение, отрицая условие и помещая вызов println
внутри блока if
. В реалистичном использовании есть больше кода, который нужно оценить после continue
, и часто есть несколько точек, из которых вызывается continue
.
Несколько вложенных for
циклов можно объединить в один внешний цикл, образуя декартово произведение его итерируемых объектов:
julia> for i = 1:2, j = 3:4
println((i, j))
end
(1, 3)
(1, 4)
(2, 3)
(2, 4)
С помощью этого синтаксиса итерируемые объекты все еще могут ссылаться на переменные внешнего цикла; например, for i = 1:n, j = 1:i
является допустимым. Однако оператор break
внутри такого цикла завершает весь вложенный цикл, а не только внутренний. Обе переменные (i
и j
) устанавливаются на их текущие значения итерации каждый раз, когда выполняется внутренний цикл. Следовательно, присвоения i
не будут видны для последующих итераций:
julia> for i = 1:2, j = 3:4
println((i, j))
i = 0
end
(1, 3)
(1, 4)
(2, 3)
(2, 4)
Если этот пример был бы переписан с использованием ключевого слова for
для каждой переменной, то вывод был бы другим: второе и четвертое значения содержали бы 0
.
Несколько контейнеров можно перебирать одновременно в одном цикле for
, используя zip
:
julia> for (j, k) in zip([1 2 3], [4 5 6 7])
println((j,k))
end
(1, 4)
(2, 5)
(3, 6)
Использование zip
создаст итератор, который является кортежем, содержащим подитераторы для переданных ему контейнеров. Итератор zip
будет итерироваться по всем подитераторам в порядке, выбирая $i$-й элемент каждого подитератора на $i$-й итерации цикла for
. Как только любой из подитераторов исчерпает свои элементы, цикл for
остановится.
Exception Handling
Когда возникает неожиданное условие, функция может не суметь вернуть разумное значение своему вызывающему коду. В таких случаях лучше всего, чтобы исключительное условие либо завершало программу с выводом диагностического сообщения об ошибке, либо, если программист предоставил код для обработки таких исключительных обстоятельств, позволить этому коду предпринять соответствующие действия.
Built-in Exception
s
Exception
выбрасываются, когда происходит неожиданное условие. Встроенные Exception
, перечисленные ниже, все прерывают нормальный поток управления.
Например, функция sqrt
вызывает DomainError
, если применить её к отрицательному действительному значению:
julia> sqrt(-1)
ERROR: DomainError with -1.0:
sqrt was called with a negative real argument but will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]
Вы можете определить свои собственные исключения следующим образом:
julia> struct MyCustomException <: Exception end
The throw
function
Исключения могут быть созданы явно с помощью throw
. Например, функция, определенная только для неотрицательных чисел, может быть записана в 4d61726b646f776e2e436f64652822222c20227468726f772229_40726566
как DomainError
, если аргумент отрицательный:
julia> f(x) = x>=0 ? exp(-x) : throw(DomainError(x, "argument must be non-negative"))
f (generic function with 1 method)
julia> f(1)
0.36787944117144233
julia> f(-1)
ERROR: DomainError with -1:
argument must be non-negative
Stacktrace:
[1] f(::Int64) at ./none:1
Обратите внимание, что DomainError
без скобок не является исключением, а представляет собой тип исключения. Его необходимо вызвать, чтобы получить объект Exception
:
julia> typeof(DomainError(nothing)) <: Exception
true
julia> typeof(DomainError) <: Exception
false
Кроме того, некоторые типы исключений принимают один или несколько аргументов, которые используются для отчетности об ошибках:
julia> throw(UndefVarError(:x))
ERROR: UndefVarError: `x` not defined
Этот механизм можно легко реализовать с помощью пользовательских типов исключений, следуя тому, как написано UndefVarError
:
julia> struct MyUndefVarError <: Exception
var::Symbol
end
julia> Base.showerror(io::IO, e::MyUndefVarError) = print(io, e.var, " not defined")
При написании сообщения об ошибке предпочтительно начинать первое слово с маленькой буквы. Например,
size(A) == size(B) || throw(DimensionMismatch("размер A не равен размеру B"))
предпочтительно по сравнению с
size(A) == size(B) || throw(DimensionMismatch("Размер A не равен размеру B"))
.
Однако иногда имеет смысл сохранить заглавную первую букву, например, если аргументом функции является заглавная буква:
size(A,1) == size(B,2) || throw(DimensionMismatch("A имеет первую размерность..."))
.
Errors
Функция error
используется для создания ErrorException
, которая прерывает нормальный поток управления.
Предположим, мы хотим немедленно остановить выполнение, если берется квадратный корень из отрицательного числа. Для этого мы можем определить нечеткую версию функции sqrt
, которая вызывает ошибку, если ее аргумент отрицательный:
julia> fussy_sqrt(x) = x >= 0 ? sqrt(x) : error("negative x not allowed")
fussy_sqrt (generic function with 1 method)
julia> fussy_sqrt(2)
1.4142135623730951
julia> fussy_sqrt(-1)
ERROR: negative x not allowed
Stacktrace:
[1] error at ./error.jl:33 [inlined]
[2] fussy_sqrt(::Int64) at ./none:1
[3] top-level scope
Если fussy_sqrt
вызывается с отрицательным значением из другой функции, вместо того чтобы пытаться продолжить выполнение вызывающей функции, он немедленно возвращает, отображая сообщение об ошибке в интерактивной сессии:
julia> function verbose_fussy_sqrt(x)
println("before fussy_sqrt")
r = fussy_sqrt(x)
println("after fussy_sqrt")
return r
end
verbose_fussy_sqrt (generic function with 1 method)
julia> verbose_fussy_sqrt(2)
before fussy_sqrt
after fussy_sqrt
1.4142135623730951
julia> verbose_fussy_sqrt(-1)
before fussy_sqrt
ERROR: negative x not allowed
Stacktrace:
[1] error at ./error.jl:33 [inlined]
[2] fussy_sqrt at ./none:1 [inlined]
[3] verbose_fussy_sqrt(::Int64) at ./none:3
[4] top-level scope
The try/catch
statement
Оператор try/catch
позволяет проверять Exception
и обеспечивать корректную обработку вещей, которые могут обычно сломать ваше приложение. Например, в приведенном ниже коде функция квадратного корня обычно вызывает исключение. Поместив блок try/catch
вокруг него, мы можем смягчить это здесь. Вы можете выбрать, как вы хотите обработать это исключение, будь то его запись в журнал, возврат значения-заполнителя или, как в приведенном ниже случае, когда мы просто вывели сообщение. Одна из вещей, о которых стоит подумать при принятии решения о том, как обрабатывать неожиданные ситуации, заключается в том, что использование блока try/catch
значительно медленнее, чем использование условной ветвления для обработки этих ситуаций. Ниже приведены еще примеры обработки исключений с помощью блока try/catch
:
julia> try
sqrt("ten")
catch e
println("You should have entered a numeric value")
end
You should have entered a numeric value
try/catch
операторы также позволяют сохранить Exception
в переменной. Следующий искусственный пример вычисляет квадратный корень второго элемента x
, если x
индексируемый, в противном случае предполагает, что x
является действительным числом и возвращает его квадратный корень:
julia> sqrt_second(x) = try
sqrt(x[2])
catch y
if isa(y, DomainError)
sqrt(complex(x[2], 0))
elseif isa(y, BoundsError)
sqrt(x)
end
end
sqrt_second (generic function with 1 method)
julia> sqrt_second([1 4])
2.0
julia> sqrt_second([1 -4])
0.0 + 2.0im
julia> sqrt_second(9)
3.0
julia> sqrt_second(-9)
ERROR: DomainError with -9.0:
sqrt was called with a negative real argument but will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]
Обратите внимание, что символ, следующий за catch
, всегда будет интерпретироваться как имя для исключения, поэтому будьте осторожны при написании выражений try/catch
в одной строке. Следующий код не будет работать для возврата значения x
в случае ошибки:
try bad() catch x end
Вместо этого используйте точку с запятой или вставьте перенос строки после catch
:
try bad() catch; x end
try bad()
catch
x
end
Сила конструкции try/catch
заключается в возможности немедленно развернуть глубоко вложенные вычисления на гораздо более высокий уровень в стеке вызывающих функций. Существуют ситуации, когда ошибка не произошла, но возможность развернуть стек и передать значение на более высокий уровень является желательной. Julia предоставляет функции rethrow
, backtrace
, catch_backtrace
и current_exceptions
для более продвинутой обработки ошибок.
else
Clauses
Эта функция требует как минимум Julia 1.8.
В некоторых случаях может возникнуть необходимость не только правильно обработать случай ошибки, но и выполнить некоторый код только в том случае, если блок try
выполнен успешно. Для этого можно указать оператор else
после блока catch
, который выполняется, когда ранее не было выброшено никаких ошибок. Преимущество по сравнению с включением этого кода в блок try
заключается в том, что любые дальнейшие ошибки не будут тихо перехвачены оператором catch
.
local x
try
x = read("file", String)
catch
# handle read errors
else
# do something with x
end
Классы try
, catch
, else
и finally
каждый вводят свои собственные блоки области видимости, поэтому если переменная определена только в блоке try
, она не может быть доступна из класса else
или finally
:
julia> try
foo = 1
catch
else
foo
end
ERROR: UndefVarError: `foo` not defined in `Main`
Suggestion: check for spelling errors or missing imports.
Используйте local
keyword вне блока try
, чтобы сделать переменную доступной из любой точки внешней области видимости.
finally
Clauses
В коде, который выполняет изменения состояния или использует ресурсы, такие как файлы, обычно требуется работа по очистке (например, закрытие файлов), которая должна быть выполнена, когда код завершен. Исключения могут усложнить эту задачу, поскольку они могут привести к тому, что блок кода завершится до достижения его нормального конца. Ключевое слово finally
предоставляет способ выполнить некоторый код, когда данный блок кода завершается, независимо от того, как он завершается.
Например, вот как мы можем гарантировать, что открытый файл будет закрыт:
f = open("file")
try
# operate on file f
finally
close(f)
end
Когда управление покидает блок try
(например, из-за return
или просто завершения нормально), будет выполнен close(f)
. Если блок try
завершится из-за исключения, исключение продолжит распространяться. Блок catch
может быть объединен с try
и finally
. В этом случае блок finally
будет выполнен после того, как catch
обработает ошибку.
Tasks (aka Coroutines)
Задачи — это функция управления потоком, которая позволяет приостанавливать и возобновлять вычисления гибким образом. Мы упоминаем их здесь только для полноты; для полного обсуждения см. Asynchronous Programming.