淘宝移动网站建设,徐州网站建设电话,天津网站建设设计开发公司,多后缀域名查询网站并发编程
1.1 并发与并⾏ 并⾏与并发是两个不同的概念#xff0c;普通解释#xff1a;
并发#xff1a;交替做不同事情的能⼒并⾏#xff1a;同时做不同事情的能⼒
如果站在程序员的⻆度去解释是这样的#xff1a;
并发#xff1a;不同的代码块交替执⾏并⾏#xf…并发编程
1.1 并发与并⾏ 并⾏与并发是两个不同的概念普通解释
并发交替做不同事情的能⼒并⾏同时做不同事情的能⼒
如果站在程序员的⻆度去解释是这样的
并发不同的代码块交替执⾏并⾏不同的代码块同时执⾏
并发和并⾏都是为了提⾼机器硬件利⽤率提升应⽤运⾏效率。并⾏和并发就是为达⽬的的两个⼿段。 并⾏可以提升CPU核⼼数并发则是通过系统设计⽐如分时复⽤系统多进程多线程的开发⽅法 不过在对并发的⽀持上Go语⾔是天⽣最好的因为它设计的理念就是并发⽽且是在语⾔层⾯实现的 并发。
1.2 Goroutine 如果对线程和进程有所了解的话我们可以这样给进程和线程下⼀个专业点的定义。
线程是最⼩的执⾏单位进程是最⼩的资源申请单位
在Go语⾔当中有⼀个存在是⽐线程还要⼩的执⾏单位那就是Goroutine翻译上习惯叫例程或协 程不过我们遵循原汁原味还是直接⽤Goroutine来称呼它。Go语⾔开发者为了实现并⽀持 Goroutine特意花了⼤⼒⽓来开发⼀个语⾔层⾯的调度算法。 具体调度算法详情介绍可以查看英⽂原⽂调度算法链接
简单理解的话⾸先要明确Go语⾔调度算法中提到的三个字⺟MPG分别代表线程上下⽂以 及Goroutine。 在操作系统层⾯线程仍然是最⼩的执⾏单位在进程调度的时候CPU需要在不同进程间切换此时需 要保留运⾏的上下⽂信息也就是前⾯提到的P⾄于G则是代表了Go语⾔当中的Goroutine。 每个线程M有⼀个⾃⼰的上下⽂P同⼀时刻每个线程内部可以执⾏⼀个Groutine代码如上图所 示每个线程有⼀个⾃⼰的调度队列这个队列⾥存放的就是若⼲个Goroutine。 当线程发⽣系统调⽤时该线程会被阻塞此时为了提⾼运⾏效率Goroutine调度算法会将该阻塞的 线程Goroutine队列转移到其他线程上。当线程执⾏完系统调⽤后⼜会从其他线程的队列中“借”⼀些 Goroutine过来。
1.3 Goroutine启动 在⼀台主机上线程启动的数量是有上限的这个上限并⾮可以启动的上限⽽是不影响系统性能的上 限。Goroutine在并发上则没有这⽅⾯的顾虑可以随⼼所欲的启动不⽤担⼼数量的问题当然这归 功于Go语⾔的调度算法。 那么Goroutine如何启动呢其实在我们之前写的代码中都存在⼀个Goroutine就是我们的主函 数我们习惯叫他main-goroutine。如果我们想再启动⼀个Goroutine也⾮常容易直接关键字go再 加上函数调⽤就⾏了。
go call_func()package mainimport fmtfunc main() {fmt.Println(begin call goroutine)//启动goroutinego func() {fmt.Println(I am a goroutine!)}()fmt.Println(end call goroutine)
}
执⾏这个代码我们⼤概率是看不到“I am a goroutine!”这句话的因为go func()这⾥创建的Goroutine 或者尚未创建成功时main-goroutine已经结束执⾏了main-goroutine结束执⾏也就代表着进程 退出那么不会有任何代码被执⾏了那么⼤家考虑⼀下如何解决这个问题呢
1.5 Go语⾔运⾏时 所谓运⾏时就是运⾏的时刻Go语⾔的运⾏时就是描述与进程运⾏相关的信息我们可以使⽤ runtime包来显示⼀些运⾏时的信息。
1.5.1 GOMAXPROCS
func GOMAXPROCS(n int) int
//当n1时查看当前进程可以并⾏的goroutine最⼤数量CPU核⼼数
//当n1时代表设置可并⾏的最⼤goroutine数量这个函数可以帮我们查看或设置当前进程的最⼤CPU核⼼数。
2. 同步 同步在不同的语境代表不同的含义在数据库中是指数据的同步在分布式系统中是指系统内的数据⼀ 致⽽在语⾔层⾯的同步是指运⾏时步调⼀致避免竞争有先有后。
2.1 如何做到同步 为了提⾼CPU的使⽤效率我们需要启动多个Goroutine⽽多个Goroutine⽐线程的颗粒度还⼩他 们之间必然存在争抢同⼀资源的现象就像我们在线程中要控制同步⼀样多个Goroutine在访问同⼀ 共享资源时我们仍然要控制同步。
如何做到最直接的同步可以借鉴我们⽣活中的例⼦在⽕⻋上我们去卫⽣间的时候都会把⻔锁上 这样别⼈就没法进来了。在这个例⼦中我们和其他⼈就是Goroutine⽽卫⽣间就是那个共享资源 我们不允许发⽣⼤家⼀起进⼊使⽤的情况⽽解决这个问题的关键就是锁 那么Go语⾔给我们提供了哪些同步机制呢主要有如下⽅式
WaitGroup 计数等待法Once 执⾏⼀次Mutex 互斥锁RWMutex 读写锁Cond 条件变量channel 通道Go语⾔的最⼤特性
在上述⽅法中除了channel其余的同步⽅式都在Go语⾔的sync包当中。
2.2 WaitGroup Go语⾔的同步⽅式很多WaitGroup的实现⽅式很巧妙主要只有3个API。
//增加计数
func (wg *WaitGroup) Add(delta int)
//减少计数
func (wg *WaitGroup) Done()
//阻塞等待计数变为0
func (wg *WaitGroup) Wait()它的核⼼思想是当启动⼀个Goroutine时使⽤Add添加⼀个计数器⽽Wait的功能是阻塞等待计数器 归0Done的作⽤则是Goroutine运⾏结束后执⾏此句话清掉计数器的⼀个计数。 利⽤WaitGroup的特性我们可以优雅的实现⼀个例⼦启动10个Goroutine让他们顺序退出 main-goroutine等待所有Goroutine退出后才可退出。
package mainimport (fmtsynctime
)var w sync.WaitGroupfunc main() {for i : 0; i 10; i {w.Add(1) //添加⼀个要监控的Goroutine数量go func(num int) {time.Sleep(time.Second * time.Duration(num))fmt.Printf(I am %d Goroutine\n, num)w.Done() //释放⼀个}(i)}w.Wait() //阻塞等待
}
I am 0 Goroutine
I am 1 Goroutine
I am 2 Goroutine
I am 3 Goroutine
I am 4 Goroutine
I am 5 Goroutine
I am 6 Goroutine
I am 7 Goroutine
I am 8 Goroutine
I am 9 Goroutine
2.3 Mutex互斥锁 提到互斥锁我们通常会提到⼀个临界区的概念Goroutine在准备访问共享数据时我们就认为它进 ⼊了临界区。 使⽤互斥锁的核⼼思想就是进⼊临界区之前先要申请锁申请到锁的Goroutine继续执⾏⽽没有申请 到的Goroutine则阻塞等待别⼈释放这个mutex这样就可以有效的控制Goroutine之间竞争的问题。 下述代码就是⼀个存在数据修改竞争的例⼦循环1000次对⼀个数据⾃增⽬标的输出结果是1000 但执⾏下⾯的代码很难获得1000。
package mainimport (fmtsync
)var x 0func increment(wg *sync.WaitGroup) {x x 1wg.Done()
}
func main() {var w sync.WaitGroupfor i : 0; i 1000; i {w.Add(1)go increment(w)}w.Wait()fmt.Println(final value of x, x)
}
final value of x 974上述代码如果在进⼊临界区前使⽤mutex就可以很好的解决该问题。代码主要修改increment函数即 可
func increment(wg *sync.WaitGroup) {mutex.Lock() //上锁x x 1 //临界区mutex.Unlock() //释放锁wg.Done()
}修改后再执⾏代码就可以很好的看到效果。
final value of x 10002.4 RWMutex
RWMutex我们可以称其为读写锁它算是mutex的改进版因为mutex的特点是排他性只要有⼀个 上锁成功了其余⼈都不可以使⽤。但在实际开发过程中经常会出现多个Goroutine去访问相同的共 享资源只不过这些Goroutine中有些是读数据有些是写数据。开发者肯定明⽩读数据不会对数据 造成影响这样理论上来说⼀个读的Goroutine上锁了其余的读Goroutine理应也可以访问这样 就出现了读写锁。对于读写锁来说关键是掌握它的原则
读共享写独占写优先级⾼
package mainimport (fmtsynctime
)var rwlock sync.RWMutex
var wg sync.WaitGroup
var x 0func go_reader(num int) {for {rwlock.RLock()fmt.Printf(I am %d reader goroutine x %d\n, num, x)time.Sleep(time.Millisecond * 2)rwlock.RUnlock()}wg.Done()
}
func go_writer(num int) {for {rwlock.Lock()x 1fmt.Printf(I am %d writer goroutine x %d\n, num, x)time.Sleep(time.Millisecond * 2)rwlock.Unlock()}wg.Done()
}
func main() {wg.Add(10)for i : 0; i 7; i {go go_reader(i)}for i : 0; i 3; i {go go_writer(i)}wg.Wait()
}
2.5 Once 在很多时候我们会有⼀个需求那就是虽然多个Goroutine都要运⾏⼀段代码但我们却希望这段代 码只能被⼀个Goroutine运⾏也就是说只被允许运⾏⼀次。在Go语⾔当中就给我们提供了这样的机 制 – Once。
package mainimport (fmtsync
)func main() {var count intvar once sync.Oncevar wg sync.WaitGroupfor i : 0; i 100; i {wg.Add(1)go func() {once.Do(func() {count 1 //确保只执⾏⼀次})wg.Done()}()}wg.Wait()fmt.Printf(Count is %d\n, count)
}
Count is 1