我们为什么需要并发程序?
- 资源利用率:从整个程序的执行角度来看,程序执行时可以看作是对输入的数据进行计算处理然后输出到特定的设备中。如果这条流程线完全是串行执行的话,当其中的一个环节正在执行的时候其他环节就不能工作。这就意味着一旦输入阻塞,即IO等待读入数据那么已读入的数据也不能得到处理,已处理的数据也不能输出,这就造成了CPU的闲置。而如果这三个步骤可以并发执行的话即使IO在等待输入CPU仍然可以对已在内存中的数据做计算处理,结果也可以正常输出。这就提高了CPU的利用率,不会因为输入输出的阻塞导致CPU的计算能力被浪费。
- 时间:很多任务彼此之间并没有什么关联,当有充分的资源可以使用时,它们可以同时被执行,与串行地执行任务相比,这样既可以充分利用现有资源,提高资源的利用率,同时又可以减少任务完成的总时间,可以节省出更多的时间处理接下来的任务。
- 公平性:对于同优先级的任务来说,它们应该能够受到计算机资源的同等待遇。如果是串行执行的话就意味着有先有后,这就造成了任务处理的不公平性,并发就可以很好地解决这个问题,它们或是同时在不同的CPU上执行,或是在单个CPU上交替执行,保证了任务应该享有的公平性。
- 简便性:当有多种类型的任务执行时,为每种任务单独编写程序比编写混杂在一起的所有任务的处理程序要简单的多。试想当我们在处理多种事情时,把每种任务都分配给单独的人员比起把每种任务都平均分配然后让相应人员都处理所有种类的任务相比,效率肯定要高的多。一方面是因为一直做一件事会做的越来越熟,更重要的是可以专心做一件事而不用受到其他事情的干扰,这一点想必已经工作的朋友一定深有体会。
所有的高级编程语言在单核心的机器上运行。Go是现代编程语言,它能够是我们充分利用机器的所有内核。
- 什么时候适合使用并发编程呢?
任何事情的优势就决定了它在什么情况下适用。正如上面”为什么要使用并发编程”中所说,并发编程最大的优势就是提高程序的运行速度和资源利用率,而如果串行执行的程序在这两方面并不受到限制的话就没有必要使用并发编程了,而如果在这两方面遇到了瓶颈需要有所突破或者你就是个该死的极致性能追求者的话,就要考虑你的程序场景是不是符合下面这些并发编程的用武之地了:- 任务会阻塞线程,导致之后的代码不能执行:比如一边从文件中读取,一边进行大量计算的情况
- 任务执行时间过长,可以划分为分工明确的子任务:比如分段下载
- 任务间断性执行:比如上报crash,日志打印
- 任务本身需要协作执行:比如生产者消费者问题
- ……
- 在实际的并发编程时都需要注意什么呢?
上面所说的并发编程的风险大概就是并发编程的注意点,更具体一点,从并发的编程角度来看,我个人更愿意将并发编程分为三部分:- 多线程的并发执行:这是并发编程的核心,研究的是如何保证任务在不同线程中并发执行从而提高程序的运行速度
- 线程间的通信:线程的执行虽然是并发的,但是他们所执行的任务并不一定是独立的,它研究的就是如何实现任务所在线程之间的高效、可靠的通信
- 线程间对共享状态的同步与互斥:线程之间会共享一些对象,我们称之为状态,当多线程同时读写某个共享状态时可能会因不恰当的执行时序而造成程序逻辑的混乱,如何保证共享状态的互斥(即保证任意时刻某个共享状态只能由单个线程访问)和同步(当前线程的值都是上一线程执行完后的最新的值)之后的文章也是围绕这三方面来展开的,其中多线程的并发执行是基础,毕竟如果没有并发,也就谈不上线程间通信和共享状态了。然而就多线程并发执行单方面仅仅是解决性能的问题,而如果没有线程间通信和对共享状态的保护,恐怕连最最基本的正确性都不能保证了。因此,这三方面对于并发编程来说缺一不可,任何一项的短板都不能让我们成功编写优秀的并发程序。
引入依赖包
1 2 3 4 5 6 7 8 |
package main import ( "fmt" "io/ioutil" "os" "net/http" "time" ) |
- “fmt” is for I/O
- “io/ioutil” is for reading the HTTP response body
- “os” is for accesing command line arguments
- “time” is for printing time data
- “net/http” is for making HTTP requests
我们在写一个 叫做 MakeRequest 的 function 用来做http的并发请求
1 2 3 4 5 6 7 |
func MakeRequest(url string, ch chan<- string) { start := time.Now() resp, _ := http.Get(url) secs := time.Since(start).Seconds() body, _ := ioutil.ReadAll(resp.Body) ch <- fmt.Sprintf("%.2f elapsed with response length: %d %s", secs, len(body), url) } |
makerequest 需要2个参数,一个string类型的url,一个channel 类型的ch,我们都知道在Go 语言中go rotines 是通过channel进行通信的。我们这里为每一个url请求开启一个go routine,只要请求结束 go routine 将返回的数据写入到channel中去。在main 方法中会吧 channel中存储的数据打印出来。
1 2 3 4 5 6 7 8 9 10 11 |
func main() { start := time.Now() ch := make(chan string) for _, url := range os.Args[1:] { go MakeRequest(url, ch) } for range os.Args[1:] { fmt.Println(<-ch) } fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds()) } |
- Note down the start time
- Create a channel ch
- For each URL of command line arguments launch a go co-routine
- For each URL read the channel for result
- Calculate time difference between beginning and end
1 |
$ go build concurrent.go |
我们在使用Python3来顺序程序与go 的并发做一个比较
1 |
$ time python3 -c "import requests;print(len(requests.get('http://localhost:8000').text));print(len(requests.get('http://localhost:8000').text));print(len(requests.get('http://localhost:8000').text))" |
1 |
$ time ./concurrent http://localhost:8000 http://localhost:8000 http://localhost:8000 |
Time | Go 1.6 | Python 3 |
---|---|---|
real | 0.00s | 0.18s |
user | 0.00s | 0.02s |
cpu | 0.006s | 0.197s |
我们可以看到结果 Go 需要的时间比Python 少很多。
参考文献:http://blog.narenarya.in/concurrent-http-in-go.html
有疑问加站长微信联系(非本文作者)