Skip to content

Commit

Permalink
refactor docs
Browse files Browse the repository at this point in the history
  • Loading branch information
zh217 committed Aug 15, 2018
1 parent 931cec3 commit a58643a
Show file tree
Hide file tree
Showing 15 changed files with 2,674 additions and 2,408 deletions.
126 changes: 126 additions & 0 deletions doc/01_concurrency.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# What is concurrency"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"*Aiochan* is a python library for CSP-style concurrency in python. So the first questions to ask are: what is concurrency? Why do we need concurrency? Aren't we doing just fine writing non-concurrent python?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"By far the most important reason for want of concurreny is that *concurrency enables your program to deal with multiple (potentially different) things all at once*. For example, suppose you are writing a webserver. Let's say that your code goes like this:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"while True:\n",
" parse request from client\n",
" do something with the request (taking an unspecified amount of time)\n",
" return the result to the client\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This is all very good but if this is the *outmost loop* of your server, then it is easy to see that at most one client can be served at any one instant. If a user is doing an operation that takes, say, ten minutes to complete, then it is not too far-fetched to assume that other users will not be too happy.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Yes, I hear you say, \"I have been writing python webservers using non-concurrent codes not too different from above for a long time, and it is definitely *not* true that only one client is served at any one instant\". Well, most likely you are using some web frameworks and it is the framework that controls the outmost loop. In other words, your framework is managing all your concurrency whilst presenting a non-concurrent façade to you."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"There is one big caveat that we need to clarify. Above we have defined concurrency as the ability to \"deal with multiple things at once\". Let's say that you have all the things *you* need to do written on a to-do list. Concurrency only means that you do not need to take the first item off the list, do it *and wait for the result*, then start with the second item. Your first item might well be \"watch the news at 9am, then watch the news at 9pm, and find out what new things have happened since 9am\". In this case non-concurrent behaviour means sitting there doing nothing after watching the 9am news until 9pm. As long as you *switch context* and starting doing something after the morning news you are in the realm of concurrent behaviour. Note that *you need neither a group of cronies to whom you can delegate your news watching, nor the rather unusual ability to watch two programs at once and perfectly understanding both, to enable concurrent behaviour*. Having a group of cronies doing stuff for you is a form of *parallelism*. Concurrency *enables* parallelism (it is useless to have many cronies if you need to wait for any one of them to complete their work before assigning work to the next one), but parallelism is not *necessary* for concurrency. Parallelism usually (but not always) makes things go faster. Concurrency can also make things go faster *even without parallelism*. In the case of computers, you *do not* need to have multiple processors to benefit from concurrency (in the case of python, this point is actually quite acute--see later when we discuss the GIL)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now back to our webserver programs. How do you write a concurrent outmost loop then? By analogy with the todo list example, you are probably thinking along something like:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"while True:\n",
" parse request from client OR get the next thing to do \n",
" if request, put it into the things-to-do list\n",
" if you have a thing to do, try do it, \n",
" (oh, but not until completion, only until something we have to \"wait for\")\n",
" ???\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"It is not impossible to complete a program like this, but you are right to feel like juggling ten balls at once -- it is difficult, the solution is brittle, and you probably don't want to do this everyday (unless you are a professional juggler)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Here is the place for concurrent libraries, frameworks, or language constructs: as long as you follow certain rules, they enable you to have the benefit of concurrency without the need of professional juggling training. *Aiochan* is such a library."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To recap:\n",
"\n",
"* Concurrency enables you to deal with multiple things at once.\n",
"* Concurrency has the potential to decrease latency and increase throughput.\n",
"* Concurrency is not parallelism but enables it.\n",
"* Concurrency frameworks, libraries and language constructs enable *you* to take advantange of concurrency without writing complicated and brittle code."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.5"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
260 changes: 260 additions & 0 deletions doc/02_csp.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# What is CSP"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We have said that *aiochan* is a CSP-style concurrency library. It turns out we can go a long way towards understanding how to use *aiochan* by just understanding what CSP mean. CSP stands for *Communicating Sequential Processes*. The name CSP first appears in a paper by C.A.R. Hoare in 1978, and some ideas of CSP exist even before that. So CSP is actually quite an old idea."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's begin with the last word: *processes*. It seems we have immediately hit upon opportunities for great confusion: python has many packages that have \"process\" on their names, your operating system has something called \"processes\" or maybe not, depending on what OS you are using, etc. And they are not what we want to talk about now. *Process* is, unfortunately, one of thoses words in computing that is over-used in many subtly different ways. Here we shall be intentially vague: for us, a *process* is just a group of code that executes fairly independently from the outside world, a group of code that you can mentally think of as a whole entity. The quintessential example is that of a function: a function as a process goes from taking in arguments from the caller, and ends when returning to the caller."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"What is *sequential* process, then? If you read the word literally, it means that statements or expressions in the process are executed or evaluated in strict order, from top to bottom. In python this is fairly accurate, if special provision is made for control statements such as `while`, `for` and stuff. A better word might be *deterministic*: even in the presence of control statements, if you know all your variables, it is possible to predict precisely what the next statement or expression to be executed or evaluated is. An example is"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"33\n"
]
}
],
"source": [
"x = 10\n",
"x += 10\n",
"x *= 2\n",
"x -= 7\n",
"print(x)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The above calculates `((10 + 10) * 2) - 7 = 33` and is sequential. If your programming language instead calculates `((10 * 2) + 10) - 7 = 3` then your programming environment has some serious issues. So sequential programs are good, it is what we as humans expect."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"However, it is actually very easy to have non-sequential programs. Let's first refactor the above program:"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"we start with the value 10\n",
"33\n"
]
}
],
"source": [
"x = 10\n",
"\n",
"def f():\n",
" global x\n",
" print('we start with the value', x)\n",
" x += 10\n",
" x *= 2\n",
" x -= 7\n",
" return x\n",
"\n",
"print(f())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"So far so good. But suppose you have two instances of the `f` process executing concurrently. Then you will have troubles. It is actually possible to illustrate this with python's `threading` module, but for simplicity we will just give an illustration in pseudo code:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"x = 10\n",
"\n",
"def f1(): | def f2():\n",
" global x # x == 10 |\n",
" | global x # x == 10\n",
" x += 10 # x == 20 |\n",
" | x += 10 # x == 30\n",
" x *= 2 # x == 40 |\n",
" | x *= 2 # x == 80\n",
" x -= 7 # x == 73 |\n",
" return x |\n",
" | x -= 7 # x == 66\n",
" | return x\n",
"\n",
"print('f1 ->', f1())\n",
"print('f2 ->', f2())\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In the above snippet, for the two functions, the order of the execution of statements within the two functions are as listed: from top to down. Note that though within each function itself the sequence is the same as before, when the two functions are taken together statements are interleaved."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We will get the results:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"```\n",
"f1 -> 73\n",
"f2 -> 66\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now if you are only in control of `f1` you will be very much baffled. Worse, as you can try for yourself, by tweaking the order further you can get other results. The interleaving makes the execution, and hence the result, non-deterministic. Such processes are *not* considered sequential and not allowed (or, not encouraged) in CSP-style programming."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"As you probably can see, the fix is actually not that hard: *don't modify global variables*. Any modifications you do must be local to your process (or in our case, function). You should try and see for yourself that if you follow this advice, then whatever the order of interleaving of the two functions, the results are not affected: they are deterministic and are thus considered *sequential*."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In functional languages, it is sometimes enforced that you cannot make any modifications at all --- any computation you do just returns new values without stepping on the old values. However, in python, this is both unnecessary and unnatural. We only need to disallow operations that can interfere with other processes."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now, you ask, what disturbed minds would write something like our `f`? Well, be assured that that people who wrote `f` habour no ill intensions. The reason that they reach for global variables is most often because that they want to do some form of input/output (i.e., IO, note that the concept of IO is much broader than file or network accesses): we need to get stuff into our function to compute, and we need to notify others who are also computing what the results of our computations are."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If you think about it, IO is the whole point of computation: *we*, at our keyboards (or touch screens, or whatever your newest VR/AR interaction devices), input something for the computer to compute, the the computer returns the results to *us*. So programs without IO is pretty useless. And using global variables in this case is rather convenient: we just take something (input) that is put inside predetermined boxes (global variables), and when we are done, just put them back. Others, by inspecting the boxes, will know what we have done. By the way, at the lowest level, this is how our current computer architecture dictates. A \"pure\" function that \"returns\" something without reference to a box location *is* an illusion. But unfortunately, as we have seen, this crude arrangement results in people stepping on each other and chaos if there are no rules."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In the older mainstream languages, the solution is that we put stickers on the boxes when we want to operate on them: \"in use --- don't touch until I'm done!\" --- locks, semaphores, etc. This solves the problem, but using locks and similar *concurrency primitives* turn out to be rather delicate and error-prone. A classical example is that you and your friend both want to operate on two boxes A and B. You go forward and put your sticker on A, meanwhile your friend has already put his sticker on B. Now both of you are stuck: unless one of you back off, no one can go forward. Preventing such *deadlocks* means having a whole lot of disciplines and guidelines to follow --- more training to become professional jugglers!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Is there a way out of this? Is there a way to avoid arduous juggler training while still doing concurrency? Yes, and this is what the *Communicating* part of CSP says."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The basic idea is this: when doing computations that must involve read-write IO, we do away with boxes. Instead, we specify *meeting points*, or *points of rendezvous*, for which IO is done. For example, you and your friend both want a book. Instead of putting the book in a box so that both of you can do whatever you want with it whenever you want (and risking the book to be stolen), you just take the book away and do your processing with it. After you are satisfied, you and your friend *meet together* and you *hand off* the book to your friend. Once your friend has the book, she can do anything she wants with it, while you can no longer do anything with it at all. There is no longer any stepping over. If you *really* want your book again, you must arange with your friend for a hand-off again."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Such an arrangment is at least familiar (it is how private properties work). Note that it is principally different from putting stickers on boxes: you just take the book and go off! And the amazing thing is, this arrangement actually solves the majority of concurrency problems. To each *sequential process*, things look as if we are in a non-concurrent program: the only concurrent parts are when we want to do IO, we arrange for rendezvous points. No stickers. No locks. Much less opportunities for deadlocks."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Another very important benefit of communicating over rendezvous points is the respect of privacy, or abstraction barriers. Consider the box-book example again. If we want to use stickers to solve that problem successfully, you and your friend both have to agree on a strategy (for example always start with box A), in other words, you are both opening yourselves up to each other, letting the other know things about how *you* operate, which you may be reluctant to due to various reasons. By contrast, we will find that when using rendezvous points to communicate, often the only thing the other parties need to know is the existence of the rendezvous points. The abstraction barrier is respected!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The rest of this tutorial will go into much more details in how to go about setting up and honouring rendezvous appointments, which in the context of *aiochan*, is called a *channel*, or `Chan`. But first, some environment setups have to be done first, which we will deal with in the next section."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To recap, in the context of CSP (*Communicating Sequantial Processes*):\n",
"\n",
"* Processes are group of codes that can be considered as an independent entity.\n",
"* Sequential processes are processes that operate in deterministic order producing deterministic results, without danger of stepping over each other.\n",
"* Communicating sequantial processes are sequantial processes that do their (necessary) IO by using rendezvous points only.\n",
"* CSP style concurrency enables natural program logic resembling non-concurrent codes, respects abstraction barriers, while at the same time eliminating most dangers of deadlocks and stepping over."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.5"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

0 comments on commit a58643a

Please sign in to comment.