--
-- 来自USTC LUG
请使用gmail订阅,不要灌水。
更多信息more info:http://groups.google.com/group/ustc_lug?hl=en?hl=en
---
You received this message because you are subscribed to the Google Groups "USTC_LUG" group.
To unsubscribe from this group and stop receiving emails from it, send an email to ustc_lug+u...@googlegroups.com.
To post to this group, send email to ustc...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
创建
创建进程和线程的过程类似。
差别只是创建进程时会把相关的数据结构都复制一份,而创建线程时部分数据结构是共享的,如页表。因此,创建线程自然比创建进程快。
下面的第906行就是线程间共享内存的代码:
http://lxr.free-electrons.com/source/kernel/fork.c?v=3.17#L880
上下文切换
这里有篇旧文测试了进程和线程的切换开销:
进程上下文切换会更换页表(cr3),且导致相关缓存失效。线程上下文切换不需要。
共享内存的缓存一致性
原文:
In fact, if you are on a multi-processor system, not sharing may actually be beneficial to performance: if each task is running on a different processor, synchronizing shared memory is expensive.这块细节我不清楚,我说我的理解,如果有错误请指正。
如果一块内存被多个 CPU 频繁使用,它就会持续出现在多个 CPU 的 L1 Cache 中。如果其中某个 CPU 修改了这块内存,其他 CPU 中相应的 L1 Cache 会更新或失效以保持缓存一致性,这是有代价的。
因此,如果多个核频繁读写同一块内存会比独立的读写各自的内存慢。
如果只是多个 CPU 频繁读取一块内存,并不修改,情况似乎不一样。
根据 Cache_coherence 的 Reference 第30页底的描述,Nehalem 使用的同步协议是 MESIF。
如果一块内存是只读的(如 .text),它在缓存中应当是处于 Shared 状态。我猜测读取 Shared 状态的缓存是没有额外代价的。
因此,这个问题和进程或线程无关,只和是否使用可修改的共享内存有关。
总结
线程创建比进程快。
线程上下文切换比进程快。
共享内存的缓存一致性和线程或进程没有本质的关联。
所以,从这几方面看,线程比进程快。
--
865 static int copy_mm(unsigned long clone_flags, struct task_struct *tsk) 866 { 867 struct mm_struct *mm, *oldmm; 868 int retval; 869 870 tsk->min_flt = tsk->maj_flt = 0; 871 tsk->nvcsw = tsk->nivcsw = 0; 872 #ifdef CONFIG_DETECT_HUNG_TASK 873 tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw; 874 #endif 875 876 tsk->mm = NULL; 877 tsk->active_mm = NULL; 878 879 /* 880 * Are we cloning a kernel thread? 881 * 882 * We need to steal a active VM for that.. 883 */ 884 oldmm = current->mm; 885 if (!oldmm) 886 return 0; 887 888 /* initialize the new vmacache entries */ 889 vmacache_flush(tsk); 890 891 if (clone_flags & CLONE_VM) { 892 atomic_inc(&oldmm->mm_users); 893 mm = oldmm; 894 goto good_mm; 895 } 896 897 retval = -ENOMEM; 898 mm = dup_mm(tsk); 899 if (!mm) 900 goto fail_nomem; 901 902 good_mm: 903 tsk->mm = mm; 904 tsk->active_mm = mm; 905 return 0; 906 907 fail_nomem: 908 return retval; 909 }==========
1330 /* copy all the process information */ 1331 retval = copy_semundo(clone_flags, p); 1332 if (retval) 1333 goto bad_fork_cleanup_audit; 1334 retval = copy_files(clone_flags, p); 1335 if (retval) 1336 goto bad_fork_cleanup_semundo; 1337 retval = copy_fs(clone_flags, p); 1338 if (retval) 1339 goto bad_fork_cleanup_files; 1340 retval = copy_sighand(clone_flags, p); 1341 if (retval) 1342 goto bad_fork_cleanup_fs; 1343 retval = copy_signal(clone_flags, p); 1344 if (retval) 1345 goto bad_fork_cleanup_sighand; 1346 retval = copy_mm(clone_flags, p); 1347 if (retval) 1348 goto bad_fork_cleanup_signal; 1349 retval = copy_namespaces(clone_flags, p); 1350 if (retval) 1351 goto bad_fork_cleanup_mm; 1352 retval = copy_io(clone_flags, p); 1353 if (retval) 1354 goto bad_fork_cleanup_namespaces; 1355 retval = copy_thread(clone_flags, stack_start, stack_size, p); 1356 if (retval) 1357 goto bad_fork_cleanup_io;==========
另外,在我的机器上运行你的代码:$ time ./benchmark pProcess...real 0m1.164suser 0m0.012ssys 0m0.228s$time ./benchmark tThread...real 0m0.224suser 0m0.160ssys 0m0.076s然后我对你的代码做了一个小修改,在循环里不进行pthread_join/wait,而是分别新开一个循环来join/wait,结果为:$ time ./benchmark pProcess...real 0m0.344suser 0m0.004ssys 0m0.336s$ time ./benchmark tThread...real 0m0.118suser 0m0.212ssys 0m0.132s
--
非常感谢你的详细回复!现在有些明白了~在创建过程中进程的开销还是比较大的。从相关内核代码来看,新进程复制的数据结构的开销的确是几倍于新建线程。只是现在开始不解为什么之前那么多人都持linux下应该用多进程的观点。。是我的感受有误还是真的有这样的观点。。?
同时,从使用场景看,这里创建、调度、销毁10000个进程,也就花了不到1s的时间,这么小的开销,3倍5倍也没那么可怕,而且实际的应用场景下估计很少会有这样的情况。(我见过的都属于高性能服务方面的,比如某mongodb实例有上万个线程,varnish有上千个线程等等)。不过在高性能的场景下,我确实很少见到用进程的,我觉得主要因素可能不是进程调度的性能差(当然也是原因之一),可能主要的麻烦是进程/线程间通信的效率问题。一般来说进程间通信的开销会比线程要大。
--Cheng,Best Regards
前者,pthread是顺序执行,顺序回收(并且是一个回收了再执行下一个),后者,pthread是乱序执行,顺序回收(且乱序执行时是并发执行),后者比前者快,应该是快在并发执行上。前者,fork是顺序执行,顺序回收(同上),后者,fork是乱序执行,乱序回收(且乱序执行时是并发执行),后者比前者快,一方面在并发执行上,一方面在乱序回收的速度不受子进程调度顺序的影响。我没有找到简单的方法来测试pthread乱序回收的效果,所以没有测试。不过从第一组结果和第二组结果的比较来看,第一组测试的数据中,有大量的时间是消耗在调度上,而不是创建、销毁上的。我相信,第二组的时间里,应该还是有不少时间是花在调度上的。
--Cheng,Best Regards
http://www.ibm.com/developerworks/cn/linux/l-threading.html这个链接说明了为什么IBM和redhat的人为什么要给Linux加入原生线程特性,包括原生线程和copy-on-write进程的区别
2014-11-29 4:44 GMT+08:00 Yan Wang <gra...@gmail.com>:非常感谢你的详细回复!现在有些明白了~在创建过程中进程的开销还是比较大的。从相关内核代码来看,新进程复制的数据结构的开销的确是几倍于新建线程。只是现在开始不解为什么之前那么多人都持linux下应该用多进程的观点。。是我的感受有误还是真的有这样的观点。。?你在什么地方看到的 “Linux 下应该用多进程而非多线程“?前面的 StackOverflow 没有表达这个意思,只是说两个不同 CPU 上并行运行的线程同时修改一块共享内存,对 CPU 缓存的影响很大,但前面 Guo, Jiahua 已经指出这跟多线程还是多进程无关,进程之间也可以用 shared memory。
In the Unix experience, inexpensive process-spawning and easy inter-process communication (IPC) makes a whole ecology of small tools, pipes, and filters possible.
...
If an operating system makes spawning new processes expensive and/or process control is difficult and inflexible, you'll usually see all of the following consequences:
...
- Multithreading is extensively used for tasks that Unix would handle with multiple communicating lightweight processes.
我觉得对相同任务的性能而言,多进程不会比多线程有性能优势。选择多进程而非多线程的原因可能是简化编程和增强模块之间的隔离性。
多个并发任务之间的通信一般有两种方式:共享内存和消息传递。共享内存的范式初看起来更简单直接,但要保证一致性往往需要加锁,一方面很难保证加锁方式的安全性(即不会出现 race condition)和活跃性(即不会发生死锁);另一方面锁是阻塞的,忙等则浪费 CPU,任务切换则有额外开销。消息传递的范式则写起来比较费脑子,不过 Scala、Erlang、Go 等为高并发而生的语言都采用消息传递的范式,因为它更自然地匹配了计算机内部的事件驱动模型。
================顺便说一个多线程编程中的经典问题:false sharing。我们都知道 CPU 非对齐访问要访问两次总线,效率很低,因此编译器都知道把整数按照4字节对齐。但在多核多线程处理中,还有一种以 64 字节为单位的对齐。在我用的 Intel 处理器中 CPU 高速缓存是以 64 字节的行(cache line)为单位的,只要其中的一个字节缓存不命中,就要从内存中读取整行;只要其中的一个字节被写入,就要通知其他处理器把相同地址的这一行清出高速缓存(缓存失效)。如果我们有一个 16 线程的计算密集程序,每个线程 i = 0..15 用到一个 4 字节全局变量 ThreadGlobal[i],则每个线程写入全局变量时,其他处理器的全局变量缓存都要失效。本来各线程的全局变量互相不影响,可以分别缓存,但 CPU 缓存行的设计破坏了缓存,这就是 false sharing。避免 false sharing 的方法很简单:让每个线程私有的全局变量按 64 字节(或处理器架构上的缓存行宽度)对齐。遗憾的是,编译器并不能自动做到这一点,需要把线程全局变量封装在结构体中,自己添加 padding。例如struct ThreadGlobal {unsigned int data;unsigned int padding[15];}; // total 64 bytesstruct ThreadGlobal _TG[NUM_THREADS];
The Art Of Unix Programming 里的很多话是要反着看的……我第一次读这本书时也被它洗脑了,不过现在认为,UNIX 跟 Windows 的区别里,在不同的应用场景里各有各的好处,没有绝对的优劣之分。这书确实是 UNIX 特性很好的总结,不过它的观点不能全盘接受,因为它考虑的都是 hacker 如何使用计算机(小工具拼接起来做大事),没有考虑最终用户的体验,也没有考虑高并发、并行计算之类机器的时间比程序员的时间更重要的地方。
比如现在流行的 fastcgi 取代 cgi,node.js 之类单个持续运行的 daemon 取代 php 之类每个请求一个进程,就是反 UNIX 哲学的,目的是提高性能和编程的灵活性。还有 SOAP、CORBA、.NET、RESTful 之类基于对象的进程间通信框架,就是打了 UNIX “一切基于文本流”的脸,现在恐怕很少有人写程序还自定义一套对象序列化的格式。
--
The Art Of Unix Programming 里的很多话是要反着看的……我第一次读这本书时也被它洗脑了,不过现在认为,UNIX 跟 Windows 的区别里,在不同的应用场景里各有各的好处,没有绝对的优劣之分。这书确实是 UNIX 特性很好的总结,不过它的观点不能全盘接受,因为它考虑的都是 hacker 如何使用计算机(小工具拼接起来做大事),没有考虑最终用户的体验,也没有考虑高并发、并行计算之类机器的时间比程序员的时间更重要的地方。
比如现在流行的 fastcgi 取代 cgi,node.js 之类单个持续运行的 daemon 取代 php 之类每个请求一个进程,就是反 UNIX 哲学的,目的是提高性能和编程的灵活性。还有 SOAP、CORBA、.NET、RESTful 之类基于对象的进程间通信框架,就是打了 UNIX “一切基于文本流”的脸,现在恐怕很少有人写程序还自定义一套对象序列化的格式。
其实KISS原则也好,小工具拼接做大事也好,最大的优势是降低了修改、维护、增加功能的成本。现在大工具在开发过程中不也是讲究模块化么。
要是一个大工具,出了一点点bug就要检查几乎所有代码,要修改很多处零零散散的地方,这怎么也说不上方便吧。
各个不相关的功能相互独立,尽量降低模块之间的耦合,我倒是觉得这是UNIX的KISS原则最好的概括。