OK,如果你还在为并发(concurrency)和并行(parallelism)这两个词的区别而感到困扰,那么这篇文章就是写给你看的。搞这种词语辨析到底有什么意义?其实没什么意义,但是有太多人在混用错用这两个词(比如遇到的某门课的老师)。不论中文圈还是英文圈,即使已经有数不清的文章在讨论并行vs并发,却极少有能讲清楚的。让一个讲不清楚的人来解释,比不解释更可怕。比如我随便找了个网上的解释:
前者是逻辑上的同时发生(simultaneous),而后者是物理上的同时发生.
并发性(concurrency),又称共行性,是指能处理多个同时性活动的能力,并发事件之间不一定要同一时刻发生。
并行(parallelism)是指同时发生的两个并发事件,具有并发的含义,而并发则不一定并行。
来个比喻:并发和并行的区别就是一个人同时吃三个馒头和三个人同时吃三个馒头。
看了之后,你懂了么?不懂,更晕了。写出这类解释的人,自己也是一知半解,却又把自己脑子里模糊的影像拿出来写成文章,让读者阅毕反而更加疑惑。当然也有可能他确实懂了,但是写出这种文字也不能算负责。至于本文,请相信,一定是准确的,我也尽量做到讲解清晰。
OK,下面进入正题,concurrency vs parallelism
让我们大声朗读下面这句话:
“并发”指的是程序的结构,“并行”指的是程序运行时的状态
即使不看详细解释,也请记住这句话。下面来具体说说:
并行(parallelism)
这个概念很好理解。所谓并行,就是同时执行的意思,无需过度解读。判断程序是否处于并行的状态,就看同一时刻是否有超过一个“工作单位”在运行就好了。所以,单线程永远无法达到并行状态。
要达到并行状态,最简单的就是利用多线程和多进程。但是 Python 的多线程由于存在著名的 GIL,无法让两个线程真正“同时运行”,所以实际上是无法到达并行状态的。
并发(concurrency)
要理解“并发”这个概念,必须得清楚,并发指的是程序的“结构”。当我们说这个程序是并发的,实际上,这句话应当表述成“这个程序采用了支持并发的设计”。好,既然并发指的是人为设计的结构,那么怎样的程序结构才叫做支持并发的设计?
正确的并发设计的标准是:使多个操作可以在重叠的时间段内进行(two tasks can start, run, and complete in overlapping time periods)。
这句话的重点有两个。我们先看“(操作)在重叠的时间段内进行”这个概念。它是否就是我们前面说到的并行呢?是,也不是。并行,当然是在重叠的时间段内执行,但是另外一种执行模式,也属于在重叠时间段内进行。这就是协程。
使用协程时,程序的执行看起来往往是这个样子:
task1, task2 是两段不同的代码,比如两个函数,其中黑色块代表某段代码正在执行。注意,这里从始至终,在任何一个时间点上都只有一段代码在执行,但是,由于 task1 和 task2 在重叠的时间段内执行,所以这是一个支持并发的设计。与并行不同,单核单线程能支持并发。
经常看到这样一个说法,叫做并发执行。现在我们可以正确理解它。有两种可能:
- 原本想说的是“并行执行”,但是用错了词
- 指多个操作可以在重叠的时间段内进行,即,真的并行,或是类似上图那样的执行模式。
我的建议是尽可能不使用这个词,容易造成误会,尤其是对那些并发并行不分的人。但是读到这里的各位显然能正确区分,所以下面为了简便,将使用并发执行这个词。
第二个重点是“可以在重叠的时间段内进行”中的“可以”两个字。“可以”的意思是,正确的并发设计使并发执行成为可能,但是程序在实际运行时却不一定会出现多个任务执行时间段 overlap 的情形。比如:我们的程序会为每个任务开一个线程或者协程,只有一个任务时,显然不会出现多个任务执行时间段重叠的情况,有多个任务时,就会出现了。这里我们看到,并发并不描述程序执行的状态,它描述的是一种设计,是程序的结构,比如上面例子里“为每个任务开一个线程”的设计。并发设计和程序实际执行情况没有直接关联,但是正确的并发设计让并发执行成为可能。反之,如果程序被设计为执行完一个任务再接着执行下一个,那就不是并发设计了,因为做不到并发执行。
那么,如何实现支持并发的设计?两个字:拆分。
之所以并发设计往往需要把流程拆开,是因为如果不拆分也就不可能在同一时间段进行多个任务了。这种拆分可以是平行的拆分,比如抽象成同类的任务,也可以是不平行的,比如分为多个步骤。
并发和并行的关系
Different concurrent designs enable different ways to parallelize.
这句话来自著名的talk: Concurrency is not parallelism。它足够concise,以至于不需要过多解释。但是仅仅引用别人的话总是不太好,所以我再用之前文字的总结来说明:并发设计让并发执行成为可能,而并行是并发执行的一种模式。
最后,关于Concurrency is not parallelism这个talk再多说点。自从这个talk出来,直接引爆了一堆讨论并发vs并行的文章,并且无一例外提到这个talk,甚至有的文章直接用它的slide里的图片来说明。比如这张:
以为我要解释这张图吗?NO。放这张图的唯一原因就是萌萌的gopher。
再来张特写:
之前看到知乎上有个关于go为什么流行的问题,有个答案是“logo萌”当时我就笑喷了。
好像跑题了,继续说这个 talk。和很多人一样,我也是看了这个 talk 才开始思考 concurrency vs parallesim 的问题。为了研究那一堆推小车的 gopher 到底是怎么回事,我花费了相当多的时间。实际上后来我更多地是通过网上的只言片语(比如SO的回答)和自己的思考弄清了这个问题,talk 并没有很大帮助。彻底明白之后再回过头来看这个 talk,确实相当不错,Andrew Gerrand 对这个问题的理解绝对够深刻,但是太不新手向了。最大问题在于,那一堆 gopher 的例子不够好,太复杂。Andrew Gerrand 花了大把时间来讲述不同的并发设计,但是作为第一次接触这个话题的人,在没有搞清楚并发并行区别的情况下就去研究推小车的 gopher,太难了。“Different concurrent designs enable different ways to parallelize” 这句总结很精辟,但也只有那些已经透彻理解的人才能领会,比如我和看到这里的读者,对新手来说就和经文一样难懂。总结下来一句话,不要一开始就去看这个视频,也不要花时间研究推小车的gopher。Gopher is moe, but confusing.
2015.8.14 更新
事实上我之前的理解还是有错误。在《最近的几个面试》这篇文章里有提到。最近买了《七周七并发模型》这本书,发现其中有讲,在此摘录一下(英文版 p3~p4):
Although there’s a tendency to think that parallelism means multiple cores,
modern computers are parallel on many different levels. The reason why
individual cores have been able to get faster every year, until recently, is that
they’ve been using all those extra transistors predicted by Moore’s law in
parallel, both at the bit and at the instruction level.
Bit-Level Parallelism
Why is a 32-bit computer faster than an 8-bit one? Parallelism. If an 8-bit
computer wants to add two 32-bit numbers, it has to do it as a sequence of
8-bit operations. By contrast, a 32-bit computer can do it in one step, handling
each of the 4 bytes within the 32-bit numbers in parallel.
That’s why the history of computing has seen us move from 8- to 16-, 32-,
and now 64-bit architectures. The total amount of benefit we’ll see from this
kind of parallelism has its limits, though, which is why we’re unlikely to see
128-bit computers soon.
Instruction-Level Parallelism
Modern CPUs are highly parallel, using techniques like pipelining, out-of-order
execution, and speculative execution.
As programmers, we’ve mostly been able to ignore this because, despite the
fact that the processor has been doing things in parallel under our feet, it’s
carefully maintained the illusion that everything is happening sequentially.
This illusion is breaking down, however. Processor designers are no longer
able to find ways to increase the speed of an individual core. As we move into
a multicore world, we need to start worrying about the fact that instructions
aren’t handled sequentially. We’ll talk about this more in Memory Visibility,
on page ?.
Data Parallelism
Data-parallel (sometimes called SIMD, for “single instruction, multiple data”)
architectures are capable of performing the same operations on a large
quantity of data in parallel. They’re not suitable for every type of problem,
but they can be extremely effective in the right circumstances.
One of the applications that’s most amenable to data parallelism is image
processing. To increase the brightness of an image, for example, we increase
the brightness of each pixel. For this reason, modern GPUs (graphics processing
units) have evolved into extremely powerful data-parallel processors.
Task-Level Parallelism
Finally, we reach what most people think of as parallelism—multiple processors.
From a programmer’s point of view, the most important distinguishing
feature of a multiprocessor architecture is the memory model, specifically
whether it’s shared or distributed.
最关键的一点是,计算机在不同层次上都使用了并行技术。之前我讨论的实际上仅限于 Task-Level 这一层,在这一层上,并行无疑是并发的一个子集。但是并行并非并发的子集,因为在 Bit-Level 和 Instruction-Level 上的并行不属于并发——比如引文中举的 32 位计算机执行 32 位数加法的例子,同时处理 4 个字节显然是一种并行,但是它们都属于 32 位加法这一个任务,并不存在多个任务,也就根本没有并发。
所以,正确的说法是这样:
并行指物理上同时执行,并发指能够让多个任务在逻辑上交织执行的程序设计
按照我现在的理解,并发针对的是 Task-Level 及更高层,并行则不限。这也是它们的区别。
本文是对 http://stackoverflow.com/questions/14132789/python-relative-imports-for-the-billionth-time#answer-14132912 这个 SO 答案的翻译。本人的翻译一向只追求含义准确而不追求字字对应,有些不好翻的术语或者固定说法就直接保留。
这个答案能解释大多关于 relative import,即相对导入的疑惑,讲解十分详尽清晰,算是 SO 上被低估的一个答案。
问题不翻译了,直接摘录下来:
The forever-recurring question is this: With Windows 7, 32-bit Python 2.7.3, how do I solve this "Attempted relative import in non-package" message? I built an exact replica of the package on pep-0328:
package/
__init__.py
subpackage1/
__init__.py
moduleX.py
moduleY.py
subpackage2/
__init__.py
moduleZ.py
moduleA.py
I did make functions named spam and eggs in their appropriate modules. Naturally, it didn't work. The answer is apparently in the 4th URL I listed, but it's all alumni to me. There was this response on one of the URLs I visited:
Relative imports use a module's name attribute to determine that module's position in the package hierarchy. If the module's name does not contain any package information (e.g. it is set to'main') then relative imports are resolved as if the module were a top level module, regardless of where the module is actually located on the file system.
The above response looks promising, but it's all hieroglyphs to me. So my question, how do I make Python not return to me"Attempted relative import in non-package"? has an answer that involves -m, supposedly.
Can somebody please tell me why Python gives that error message, what it Means by non-package!, why and how do you define a 'package', and the precise answer put in terms easy enough for a kindergartener to understand. Thanks in advance!
Edit: The imports were done from the console.
BrenBarn 的精彩答案(这个哥们可以算是 import 专家了,答了好多这方面的题)
简单地说,直接运行 .py 文件和 import 这个文件有很大区别。Python 解释器判断一个 py 文件属于哪个 package 时并不完全由该文件所在的文件夹决定。它还取决于这个文件是如何 load 进来的(直接运行 or import)。
有两种方式加载一个 py 文件:作为 top-level 脚本或者作为 module。前者指的是直接运行脚本,比如 python myfile.py
。如果执行 python -m myfile
,或者在其它 py 文件中用 import
语句来加载,那么它就会被当作一个 module。有且只能有一个 top-level 脚本,就是最开始执行的那个(比如 python myfile.py
中的 myfile.py
,译者注)。
当一个 py 文件被加载之后,它会被赋予一个名字,保存在 __name__
属性中。如果是 top-level 脚本,那么名字就是 __main__
。如果是作为 module,名字就是把它所在的 packages/subpackages 和文件名用 .
连接起来。
例如,moduleX
被 import 进来,它的名字就是 package.subpackage1.moduleX
。如果 import 了 moduleA
,它的名字是 package.moduleA
。如果直接运行 moduleX
或 moduleA
,那么名字就都是 __main__
了。
另一个令人担忧的问题是,一个 module 的名字取决于它是直接从它所在的文件夹 import 还是通过某个 package import 的。不过只有当你在某个路径中运行 Python 并试图从当前文件夹 import 一个 py 文件时,才需要关注它们的不同。例如,在路径 pacakge/subpackage1
中运行 python 解释器,然后脚本中有 import moduleX
这个语句,此时 moduleX
的名字正是 moduleX
,而不是 package.subpackage1.moduleX
。这是因为 Python 解释器在启动时把当前路径(这里答案写的不准确,其实加入的是 top-level 脚本的路径,因为两者在这种状况下相同,所以也并不算错。译者注)加入了它的搜索路径 (sys.path);如果发现要 import 的 module 就在当前路径,那么 Python 解释器就不知道当前路径是属于哪个 package 的,所以 pacakge 的信息就不会成为 module 的名字的一部分。
一个特例是直接运行 python REPL,这个 REPL 的 session 的名字是 __main__
。
关于你遇到的错误信息,关键点来了:如果一个 module 的名字中没有点(即 package.subpackage1 中的那个点,译者注),那么它就被认为不属于任何一个 package。文件在磁盘上的位置在哪里都不影响,唯一起决定作用的就是 module 的名字,而这又取决于它是如何被加载的。
先在我们看看你在问题中引用的这段话
Relative imports use a module's name attribute to determine that module's position in the package hierarchy. If the module's name does not contain any package information (e.g. it is set to'main') then relative imports are resolved as if the module were a top level module, regardless of where the module is actually located on the file system.
relative import 使用 module 的名字来决定它是否属于一个 package,属于哪个 package。当你使用这种 relative import from .. import foo
,其中的点的数量代表了 package 结构中的某个层次。例如,如果当前 module 的名字是 package.subpackage1.moduleX
,那么 ..moduleA
代表 package.moduleA
。为了让形如 from .. import
的这种导入能够正常工作,module 的名字里的点数量应当至少和 import 语句中一样多。
前面说了,如果 module 的名字是 __main__
,那么 Python 就不认为它属于某个 package。由于名字里不包含点,所以在这个 py 文件中 from .. import
语句无法正常工作。试图执行这条语句就会报 "relative-import in non-package" 错误。
你犯的错误可能是从命令行运行 moduleX
或类似的操作。当你执行这个操作,moduleX 的名字被设置成 __main__
,所以 relative imports 失败了,因为不包含 package 信息。正如前面说的,如果在同一个路径里 import 一个文件,这时 module 的名字就是文件名,不包含 package 信息,所以相对导入也会失败。
记住,因为 REPL session 的名字总是 __main__
,所以试图在 REPL 里执行 relative import 是不行的。relative import 应当只在 module 文件中被使用。
(无法相对导入的问题)有两个解决方法。如果你真的想直接运行 moduleX
,同时又希望它被当作所在 package 的一部分,可以这么做:python -m package.subpackage.moduleX
。-m
参数告诉 Python 解释器,把这个文件当作一个 module 载入,而不是 top-level 脚本。
如果你并不想直接运行 moduleX
,而是想在另一个文件比如 myfile.py
中使用 moduleX
中定义的函数,那么解决方法是把 myfile.py
文件挪到另一个地方,只要不在 moduleX
所属的 package 的文件夹里就行。然后在 myfile.py
中执行 from package.moduleA import spam
,就能正常工作了。
注意,不论哪种解决方案,都需要 package 的路径(上文中的 package
)在 python 的搜索路径也就是 sys.path
里。如果不在,那么就无法使用这个 package 中的任何文件。
(更严谨的说明:从 Python2.6 开始,在做 package-resolution 时,module 的 “名字” 并不完全等于 __name__
属性,还和 __package__
属性有关。这也是为什么上文中我一直尽量避免用 __name__
来指代 module 的名字。从 python2.6 开始,一个 module 的 “名字” 实际上是 __package__ + '.' + __name__
, 或者直接就是 __name__
,如果 __package__
是 None
的话)
文件上传是 Web 开发肯定会碰到的问题,而文件夹上传则更加难缠。网上关于文件夹上传的资料多集中在前端,缺少对于后端的关注,然后讲某个后端框架文件上传的文章又不会涉及文件夹。今天研究了一下这个问题,在此记录。
先说两个问题:
- 是否所有后端框架都支持文件夹上传?
- 是否所有浏览器都支持文件夹上传?
第一个问题:YES,第二个问题:NO
只要后端框架对于表单的支持是完整的,那么必然支持文件夹上传。至于浏览器,截至目前,只有 Chrome 支持。 Chrome 大法好!
不要期望文件上传这个功能的浏览器兼容性,这是做不到的。
好,假定我们的所有用户都用上了 Chrome,要怎么做才能成功上传一个文件夹呢?这里不用drop
这种高大上的东西,就用最传统的<input>
。用表单 submit 和 ajax 都可以做,先看 submit 方式。
<form method="POST" enctype=multipart/form-data>
<input type='file' name="file" webkitdirectory >
<button>upload</button>
</form>
我们只要添加上 webkitdirectory
这个属性,在选择的时候就可以选择一个文件夹了,如果不加,文件夹被选中的时候就是灰色的。不过貌似加上这个属性就没法选中文件了... enctype=multipart/form-data
也是必要的,解释参见这里
如果用 ajax 方式,我们可以省去<form>
,只留下<input>
就 OK。
<input type='file' webkitdirectory >
<button id="upload-btn" type="button">upload</button>
但是这样是不够的,关键在于 Js 的使用。
var files = [];
$(document).ready(function(){
$("input").change(function(){
files = this.files;
});
});
$("#upload-btn").click(function(){
var fd = new FormData();
for (var i = 0; i < files.length; i++) {
fd.append("file", files[i]);
}
$.ajax({
url: "/upload/",
method: "POST",
data: fd,
contentType: false,
processData: false,
cache: false,
success: function(data){
console.log(data);
}
});
});
用 ajax 方式,我们必须手动构造一个 FormData
Object, 然后放在 data 里面提交到后端。 FormData
好像就只有一个 append
方法,第一个参数是 key,第二个参数是 value,用来构造表单数据。ajax
请求中,通过 input 元素的 files 属性获取上传的文件。files
属性不论加不加 webkitdirectory
都是存在的,用法也基本一样。不过当我们上传文件夹时,files
中会包含文件相对路径的信息,之后会看到。
用 ajax 上传的好处有两点,首先是异步,这样不会导致页面卡住,其次是能比较方便地实现上传进度条。关于上传进度条的实现可以参考这里。需要注意的是contentType
和processData
必须设置成false
,参考了这里。
前端说完了,再说后端。这里以 flask 为例。
@app.route('/upload/', methods=['POST'])
def upload():
pprint(request.files.getlist("file"))
pprint(request.files.getlist("file")[2].filename)
return "upload success"
现在可以解释为什么说所有后端框架都支持文件夹上传了,因为在后端看来文件夹上传和选中多个文件上传并没有什么不同,而后者框架都会支持。flask 的 getlist
方法一般用来处理提交的表单中 value 是一个 Array 的情况(前端用name="key[]"
这种技巧实现),这里用来处理多个上传的文件。
我们选择了一个这样的目录上传
car/
|
+--+-car.jpeg
| +-download.jpeg
|
+--car1/
|
+-SS.jpeg
pprint(request.files.getlist("file"))
打出下面的结果:
[<FileStorage: u'car/car.jpeg' ('image/jpeg')>,
<FileStorage: u'car/download.jpeg' ('image/jpeg')>,
<FileStorage: u'car/car1/SS.jpeg' ('image/jpeg')>]
可以看到,相对路径被保留了下来。可以用filename
属性获取相对路径,比如request.files.getlist("file")[2].filename
的结果就是u'car/car1/SS.jpeg'
。接下来,该怎么保存文件就怎么保存,这就是各个框架自己的事情了。
EDIT
查了一下,确实文件夹上传模式和文件上传模式是不兼容的,参见 这里,引用关键部分:
We only propagate a single file chooser mode which could be one of: { OPENFILE, OPENMULTIFILE, FOLDER, SAVEASFILE }. Only one mode can be selected and they cannot be or'ed or combined. Therefore there's no chance to enable both mode.
EDIT 2
文件上传的程序给师兄和自己用了几个月,单文件上传一直很稳定。今天试图传一个包含很多文件(2000+)的文件夹上去,遇到了问题——进度条能够走到最后,但是随后就看到浏览器控制台里报错 net::ERR_EMPTY_RESPONSE
。看了下后端的输出,Flask 并没有接到请求。首先怀疑的是文件数量过多或者大小太大,查了下,浏览器有限制但似乎是 4G,我还远远没达到,所以不是这个原因。然后看有人提到 PHP 的 timeout,即一个请求如果持续太久后端就直接断掉它。不过 Flask 似乎也没有这种设置。偶然间看到这里提到 gunicorn 默认 timeout 是 30s,恍然大悟,原来是 gunicorn 导致的。
于是在 gunicorn 启动的时候加上 --timeout 120
,这次是 500,总算有点进展,因为请求至少发到后端去了。看了 flask 的 log,错误原因是 too many open files,然后 ulimit
一下果然只有 1024。Flask 上传文件时,500KB 以下的直接以 StringIO Object 的形式存在内存中,大于 500KB 的用 tempfile 存在磁盘上。所以我传的文件一多,很容易开的文件描述符就超了。改 ulimit 也遇到一点小问题,最后按照这里说的用
sudo sh -c "ulimit -n 65535 && exec su $LOGNAME"
解决。之后再次上传就没有问题了。
参考资料:
1. http://stackoverflow.com/questions/4526273/what-does-enctype-multipart-form-data-mean
2. https://developer.mozilla.org/en-US/docs/Web/Guide/UsingFormDataObjects
3. http://www.w3schools.com/jsref/propfileuploadfiles.asp
4. https://github.com/kirsle/flask-multi-upload
5. http://stackoverflow.com/questions/9622901/how-to-upload-a-file-using-jquery-ajax-and-formdata
资料4的那个demo给了我巨大帮助,没它的代码估计我会多花几倍时间,虽然它实现的是文件上传而非文件夹,但其实没什么不同。