对于并发这个概念,我想大家都对它不会陌生,今天就从简单的火车站卖票问题出发,来谈谈并发。
首先声明本文的代码是golang(因为最近开始用的就是golang),对于其他的语言其实也是相通的,那么正式开始正题吧,首先我们来看看,卖一张票,总票数就减一,一般来说我们会这么写:
package main
import (
“fmt”
“math/rand”
"time"
)
var totle_tickets int = 1000
func sell_tickets() {
for {
if totle_tickets > 0 {
time.Sleep(time.Duration(rand.Intn(5)) * time.Millisecond)
totle_tickets–
} else {
break
}
}
}
func main() {
t := time.Now()
rand.Seed(time.Now().Unix())
sell_tickets()
t2 := time.Since(t)
fmt.Println(t2)
}
执行耗时大概1-2秒,那么,我们来试试用协程加锁的方式:
// bingfa_anquan project main.go
package main
import (
“fmt”
“math/rand”
“runtime”
“sync”
“time”
)
var totle_tickets int = 1000
var ch chan bool
var mutex sync.Mutex
func sell_tickets(i int) {
for {
mutex.Lock()
if totle_tickets > 0 {
time.Sleep(time.Duration(5) * time.Millisecond)
//rand.Intn(5))
totle_tickets–
mutex.Unlock()
} else {
mutex.Unlock()
ch <- true
}
}
}
func main() {
t := time.Now()
ch = make(chan bool)
runtime.GOMAXPROCS(4)
rand.Seed(time.Now().Unix())
for i := 0; i < 10; i++ {
go sell_tickets(i)
}
for i := 0; i < 10; i++ {
<-ch
}
t2 := time.Since(t)
fmt.Println(t2)
}
执行结果:耗时大概5秒多!很奇怪是不是,为什么协程执行却耗时更久了。下面我们来分析分析:
第一个程序:
Main函数调用卖票函数,将1000张票卖完
第二个程序:
Main函数中创建5个协程,将1000张票卖完
按照这个逻辑一个人卖票肯定是没有5个人卖票快的,但是这有个问题,那就是协程都是在操作tickets这个变量,而且该变量加锁了,那么这就意味着任意一个协程中,只要我在操作卖票这个操作,那么其他协程是不能去操作这个变量的,这样一来就变成哪个协程”运气好”轮到他,同时票已经没有锁定的时候,他才可以卖票,这个执行的时间=创建协程的时间+协程卖票的时间+通知主线程(票已经卖完了,可以结束了),所以时间反而变长了。
那么现在我们来思考下,如果开协程反而更慢了,那为什么还说多线程(协程)处理比单线程快呢?根源就在于这个例子有个不合理的地方,那就是事务并不复杂,同时协程(不管创建了多少个)等待条件不合理。一般来说,我们在使用协程处理服务器端的事务的时候,都会有很多操作,有些协程处理完自己的任务就把结果发走了,并不需要等待,有些协程等其他协程把结果给过来,一拿到就处理,处理完又把结果发走,也就是说大家(协程)做着自己的工作,并没有出现大家都要操作一个对象(变量),所有人都要等别人unlock的情况。
语言组织不是很好,但是感觉已经把想要表达的说清楚了,并不是所有的情况都适合并发,适合并发的情况可能是下面这样(举一个栗子帮助理解):
这里代码部分需要说明下,有些人可能会说,才1000张票就用并发,完全就是没意义,但是我这里只是举个例子,读者可以自己吧上面代码修改为一千万或是一个亿,但是,程序执行时间太长,没意义。这里读者可以把第二个程序加锁代码去掉,然后修改票数为一千万张你会发现,第二个程序执行的时间比第一个快大约10倍,协程开100个,执行速度就比第一个快大概100倍!但是结果会出现你所不希望看到的(自己运行看看)。