ConcurrencyVSParallelism在解释并发的概念时,总是牵扯到另外一个并行的概念。下面说一下并发和并行的区别。并发:在不同的时间点将任务交给处理器进行处理。在同一时间点,任务不会并发运行。并行性:将每个任务分配给每个处理器独立完成。在同一时间点,任务必须并发运行。并发不是并行。并行是在不同的物理处理器上同时执行不同的代码片段。并行的关键是同时做很多事情,并发是指同时管理很多事情。这些事情可能只做一半就暂停去做其他事情。很多时候,并发的效果比并行要好,因为操作系统和硬件的总资源一般很小,但是可以支持系统同时做很多事情。这种“doingmorewithless”的哲学也是指导Go语言设计的哲学。如果要并行化goroutine,则必须使用多个逻辑处理器。当有多个逻辑处理器(CPU)时,调度器将goroutine平均分配给每个逻辑处理器。这将使goroutine在不同的线程上运行。但是,为了真正达到并行的效果,用户需要在具有多个物理处理器的机器上运行他们的程序。否则即使Go语言在运行时使用了多线程,goroutines仍然会在同一个物理处理器上并发运行,无法达到并行的效果。下图显示了在一个逻辑处理器上并发运行goroutine与在两个逻辑处理器上同时运行两个并发goroutine之间的区别。调度器包含一些巧妙的算法,会随着Go语言的发布而更新和改进,因此不建议盲目修改语言运行时对逻辑处理器的默认设置。如果你真的认为改变逻辑处理器的数量可以提高性能,你也可以对语言运行时的参数做一些小的调整。并发与并行的区别Go可以充分发挥多核的优势,高效运行。Go语言在GOMAXPROCS个数等于任务个数时可以并行执行,但一般都是并发执行。目录1.1Goroutine1.2CSP1.3Channel1.4Lock1.5WaitGroup1.1谁创建了Goroutine?线程是操作系统分配给应用程序的独立执行单元,它们可以在多核处理器中并行执行。线程的调度是操作系统内核的职责,线程之间有独立的地址空间。协程由程序员编写为轻量级线程,由Go语言运行时管理。协程之间没有独立的地址空间,而是共享的地址空间。协程的调度由Go语言运行时负责,可以在单线程中并行执行。创建和销毁线程的开销比较高,而创建和销毁协程的开销很小。因此,在需要高并发的场景下,使用协程效率更高。尺码比较?线程栈由操作系统分配,通常有固定大小,在创建线程时分配。它存储线程状态信息和调用堆栈。线程栈的大小取决于操作系统的限制,一般在几百KB到几MB之间。协程的栈由Go语言运行时管理,默认大小通常较小,在创建协程时分配。它还存储协程的状态信息和调用堆栈。协程栈的大小可以通过Golang运行时包中的函数进行调整,一般在几KB到几MB之间。由于协程栈比线程栈小,所以可以创建的协程数量远大于线程。但是,由于协程栈比线程栈小,在调用深度较深的程序中,协程可能会爆栈。1.2CSPCSP:CommunicatingSequentialProcessesGo语言提倡:通过通信共享内存,而不是通过共享内存实现通信。BufferedChannels缓冲通道中的数字表示在没有接收器阻塞的情况下通道可以缓冲多少元素。连接容量为1,因此只能缓存一个元素。如果尝试将新元素发送到已满的通道,发送方将阻塞,直到接收方从通道中读取一个元素。阻塞并不一定意味着数据丢失,这取决于阻塞的原因和应用程序的设计:在Go语言中,通道是一种同步机制,发送方和接收方可以通过它进行通信。如果发送方试图将数据发送到一个完整的缓冲通道,发送方将阻塞直到缓冲区可用。同样,如果接收器尝试从空通道接收数据,则接收器将阻塞,直到通道上有可用数据。在这种情况下,数据并没有丢失,而是在缓冲区中等待被提取。无缓冲通道然而,如果通道是无缓冲的,那么发送方和接收方之间就会同步。如果发送方在接收方准备好之前发送数据,发送方将阻塞直到接收方准备好。如果接收器在数据可用之前开始接收,则接收器将阻塞直到数据可用。在这种情况下,如果发送方和接收方之间的时间差很大,可能会导致数据丢失。所以阻塞并不一定意味着数据丢失,而是取决于程序是否设计用于处理阻塞,以及阻塞的类型。这是一个示例代码,其中两个goroutines通过缓冲通道共享内存:packagemainimport("fmt")funcmain(){//创建一个缓冲通道ch:=make(chanint,1)//启动第一个goroutinegofunc(){对于我:=0;我<10;i++{ch<-i//发送数据}close(ch)//关闭通道}()//启动第二个goroutinegofunc(){fori:=rangech{fmt.Println(i)//接收数据和print}}()//等待所有goroutine完成fmt.Scanln()}执行效果:本例中,第一个goroutine会循环发送0到9,第二个goroutine接收并打印。两个goroutine将共享同一个通道来传递数据。注意,在生产环境中,通常需要使用同步机制来等待goroutine结束,而不是使用fmt.Scanln()。1.3Channelmake(chanelementtype,[buffersize])unbufferedchannelmake(chanint)synchronousbufferedchannelmake(chanint,2)asynchronousunbufferedchannel是发送方和接收方之间同步传输消息。发送方阻塞直到接收方准备好接收消息,接收方阻塞直到接收到消息。这样保证了消息的顺序,每条消息只接收一次。缓冲通道具有固定大小的缓冲区,发送方和接收方不再同步。如果缓冲区已满,发送方继续而不阻塞;如果缓冲区为空,则接收者继续执行而不会阻塞。这种方法可以提高程序的性能,但可能会导致消息丢失或重复。packagemainimport("fmt")funcmain(){//创建通道ch:=make(chanint)ch_squared:=make(chanint)//启动一个子协程gofunc(){fori:=0;我<10;i++{ch<-i}close(ch)}()//启动B子协程gofunc(){fori:=rangech{ch_squared<-i*i}close(ch_squared)}()//主协程输出结果fori:=rangech_squared{fmt.Println(i)}}执行效果:本程序中,A子协程循环发送0到9的数字,B子协程接收并计算数字的平方,最后主协程等待所有子协程完成并输出所有数字的平方。注意:在这个程序中我们使用两个通道ch,ch_squared来传输数据以避免数据丢失。最后输出结果时,主协程需要等待所有子协程完成,所以我们使用fori:=rangech_squared等待子协程完成在生产环境中,通常是必须使用同步机制来等待子协程结束,而不是使用fori:=rangech_squared。可以将ch_squared改成bufferedchannel,解决生产快于消费的执行效率问题。1.4并发安全锁在并发编程中,当多个goroutine同时访问共享资源时,可能会出现racecondition,导致数据不一致或错误。为了避免这种情况,我们可以使用Lock(锁)来保证并发安全。Lock是一种同步机制,可以防止多个goroutine同时访问共享资源。当一个goroutine获得锁时,其他goroutine将被阻塞,直到锁被释放。Go语言标准库中提供了sync.Mutex来实现锁。一个简单的例子:packagemainimport("fmt""sync")var(countintlocksync.Mutex)funcmain(){wg:=sync.WaitGroup{}fori:=0;我<10;i++{wg.Add(1)gofunc(){deferwg.Done()lock.Lock()deferlock.Unlock()count++fmt.Println(count)}()}wg.Wait()}执行效果:在上面的例子中,main函数中启动了10个goroutine,每个goroutine都会尝试去获取锁并修改共享变量count。在获取到锁之前不能进行修改,其他goroutine在等待锁的过程中会被阻塞。这样可以保证并发安全,使得共享变量count可以在多个goroutine之间安全访问。但是使用锁也需要注意避免死锁,需要在合适的时候释放锁。并发安全问题很难定位。1.5WaitGroupGo语言标准库提供了sync.WaitGroup来管理多个goroutine的执行。Add(deltaint):使用这个方法来增加等待组中的goroutines的数量。当我们需要等待一些goroutines完成时,我们可以使用这个方法来增加等待组中goroutines的数量。Done():使用此方法通知等待组一个goroutine已完成执行。当一个goroutine执行完毕,我们需要调用这个方法来通知等待组。Wait():使用该方法等待等待组中的所有goroutine执行完毕。当我们需要等待所有goroutines执行完毕时,可以使用该方法。下面是一个示例,演示如何使用sync.WaitGroup来管理多个goroutine的执行:packagemainimport("fmt""sync")funcmain(){varwgsync.WaitGroupwg.Add(3)//Increase3goroutinegofunc(){deferwg.Done()fmt.Println("Goroutine1")}()gofunc(){deferwg.Done()fmt.Println("Goroutine2")}()gofunc(){deferwg.Done()fmt.Println("Goroutine3")}()wg.Wait()fmt.Println("allgoroutineshavebeenfinished")}执行效果:在上面的代码中,我们使用了Async.WaitGroup用于管理三个goroutines的执行。我们首先使用wg.Add(3)来增加等待组中goroutines的数量。然后在每个goroutine中调用wg.Done()以通知等待组goroutine已完成执行。最后,使用wg.Wait()等待所有goroutines完成执行。注意:如果没有wg.Wait(),主协程可能会在其他协程执行完之前结束,所以其他协程的执行结果没有机会获取。如果Add的次数和done的次数不对应,wait永远不会返回,也叫死锁。支持在线运行上面分享的代码。访问下面的链接运行测试:https://1024code.com/codecubes/GB47x7u按照下面的二维码操作。转载本文请联系《程序员升级打怪之旅》公众号。
