Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.1
并发是在同一时间处理(dealing with)多件事情。
并行是在同一时间做(doing)多件事情。
并发的目的在于把单个 CPU 的利用率使用到最高。并行则需要多核的支持。
单核CPU上运行的多线程程序, 同一时间只能一个线程在跑, 系统帮你切换线程而已(cpu时间切片), 系统给每个线程分配时间片来执行, 每个时间片大概10ms左右, 看起来像是同时跑, 但实际上是每个线程跑一点点就换到其它线程继续跑,效率不会有提高的,切换线程反倒会增加开销(线程的上下文切换),宏观的可看着并行,单核里面只是并发,真正执行的一个cpu核心只在同一时刻执行一个线程(不是进程)。
1、只跑一个线程,该线程顺序执行每个任务
2、跑n个线程(内核态线程),每个任务分配一个线程执行
3、跑n个协程(用户态线程)每个任务跑一个协程
时间最短的是1,其次是3,最后是2,对于1,os不需要分配资源给线程或者协程的调度没有浪费资,并发不能提高运行效率,并发的作用在于执行一个非常耗时的任务时,不需要一直等待其返回也能做别的任务。对于2和3,他们的区别在于线程包的实现,一个是内核态,一个是用户态
线程包的实现可以在内核态也可以在用户态,对于在用户空间实现的线程包,内核不知道线程的存在,线程的运行由进程决策且在该进程拥有CPU时间切片的情况下不可能切换到别的进程中的线程对于在内核实现线程包,内核知道线程的存在,且可以对线程进行调度而不考虑线程属于哪个进程,简单点老说,如果有进程A,B,进程A中有线程A1A2A,进程B中有线程B1B2,如果线程包实现在用户态,线程的调度只能是,A1到A2或者B1到B2,而如果线程包实现在内核态,线程的调度可以是A1到B2,B2到A2,线程的调度可以跨进程。
用户级和内核级线程的差别在于性能,用户级的线程切换只需要简单的机器指令,而内核级的线程切换需要完整的上下文切换,修改内存映射等非常耗时
Java线程在Windows及Linux平台上的实现方式,是内核线程的实现方式。在Linux平台上当thread.run就会调用一个fork产生一个线程,这种方式实现的线程,是直接由操作系统内核支持的——由内核完成线程切换,内核通过操纵调度器(Thread Scheduler)实现线程调度,并将线程任务反映到各个处理器上。而Go语言中的goroutine是用户态的线程切换
在go语言中创建一个goroutine只需加上go关键字即可
func main() {
fmt.Println("主线程1,主线程1")
go func() {
fmt.Println("子线程,子线程")
}()
fmt.Println("主线程2,主线程2")
time.Sleep(time.Second * 2)
}
channel可分为无缓存和有缓存两种
func main() {
done := make(chan struct{})
fmt.Println("主线程阻塞")
go func() {
fmt.Println("子线程运行")
time.Sleep(time.Second * 2)
done <- struct{}{} //发送数据
close(done)//关闭channel
}()
<-done //阻塞
fmt.Println("主线正常运行")
}
x,ok:=<-done
fmt.Println("222",x,ok)
如果你在main goroutine 中持续去接收一个channel,但是该channel被不会被写入,那么会导致all goroutines are asleep - deadlock! 的错误,比如下面的代码
func main() {
sizes := make(chan int64)
for i := 0; i < 10; i++ {
go func() {
sizes <- 1
fmt.Println("正在执行。。。")
}()
}
var total int64
for size := range sizes {
time.Sleep(time.Second * 1)
fmt.Println("从chan中拿出数据 ", size)
total += size
}
fmt.Println("total---> ", total)
}
上面的例子是多个goroutine 使用同一个channel向main goroutine 中发送数据,main goroutine 一直在接收,当所有的线程都发生完数据后,主线线程还在接收,会一直阻塞,最后error
要解决该问题,可以使用sync.WaitGroup,sync.WaitGroup提共一种特殊的计数器,该计数器可以在多个goroutine操作时做到安全并且提供提供在其减为零之前一直等待的一种方法,改写后的代码
func main() {
sizes := make(chan int64)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done() //该goroutine执行完以后减一
sizes <- 1
fmt.Println("正在执行。。。")
}()
}
// close
go func() {
wg.Wait() //计数器不为0则一直阻塞
fmt.Println("这里要关闭chan")
close(sizes)
}()
var total int64
for size := range sizes {
time.Sleep(time.Second * 1)
fmt.Println("从chan中拿出数据 ", size)
total += size
}
fmt.Println("total---> ", total)
}
func main() {
chan1 := make(chan string, 3)
chan2 := make(chan string, 5)
time.Sleep(time.Second * 5)
for x := 0; x < 100; x++ {
go func() {
chan1 <- "我是A,我生产了一个cake"
fmt.Println("我是A,我生产了一个cake")
}()
go func() {
s := <-chan1
chan2 <- s + " 我是B,我装饰了一个cake"
fmt.Println("我是B,我装饰了一个cake")
}()
go func() {
s := <-chan2
fmt.Println(s + " 我是C,我卖出了一个cake")
}()
}
time.Sleep(time.Second * 10)
}
考虑这么一个情况,如果有两个线程A、B通过不同的channel向同一个线程C发送数据,对于C只要从任意channel中取出数据就可以继续后面的步骤,我们无法做到从每一个channel中接收信息,如果我们这么做的话,如果第一个channel中没有事件发过来那么程序就会立刻被阻塞,这样我们就无法收到第二个channel中发过来的事件。
使用select则可以解决这样的问题
func main() {
chan1 := make(chan int)
chan2 := make(chan int)
go func() {
time.Sleep(time.Second * 2)
chan1 <- 1
}()
go func() {
time.Sleep(time.Second * 2)
chan2 <- 2
}()
select {
case <-chan1:
fmt.Println("从channe 1中拿数据")
case <-chan2:
fmt.Println("从channe 2中拿数据")
}
fmt.Println("继续执行")
}
数据竞争会在两个以上的goroutine并发访问相同的变量且至少其中一个为写操作时发生。
根据上述定义,有三种方式可以避免数据竞争
使用sync.Mutex互斥锁保证最多只有一个goroutine在同一时刻访问一个共享变量。Go的互斥量不能重入
对一个已经锁上的mutex来再次上锁–这会导致程序死锁
var (
mu sync.Mutex
balan int
)
func deposit(amount int){
mu.Lock()
balan+=amount
mu.Unlock()
}
func getBalance()int{
defer func() {mu.Unlock()}()
mu.Lock()
return balan
}
sync.RWMutex允许多个只读操作并行执行,但写操作会完全互斥
var(
mu sync.RWMutex
balance int
)
func Deposit(amount int){
mu.Lock()
balance+=amount
mu.Unlock()
}
func Balance()int{
mu.RLock()
defer mu.Unlock()
return balance
}
“同步”不仅仅是一堆goroutine执行顺序的问题;同样也会涉及到内存的问题。(可视性,新的视图能被其他线程获得,而不是得到旧值)
sync.Once用于解决多线程访问一个只需初始化一次的资源(类似于Java中支持并发操作 的单例模式),且sync.Once能保证可视性
一次性的初始化需要一个互斥量mutex和一个boolean变量来记录初始化是不是已经完成了;互斥量用来保护boolean变量和客户端数据结构。
var icons map[string]image.Image
var loadIconsOnce sync.Once
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}
func loadIcons() {
icons = make(map[string]image.Image)
icons["spades.png"] = loadIcon("spades.png")
icons["hearts.png"] = loadIcon("hearts.png")
icons["diamonds.png"] = loadIcon("diamonds.png")
icons["clubs.png"] = loadIcon("clubs.png")
}
func loadIcon(iconName string) image.Image {
//该函数未加载图片
img:=new(image.Image)
return img
}