那协程又是什么东西,以及与线程的差异性??
协程,可以看作是轻量级的线程。但与线程不同的是,线程的切换是由操作系统控制的,而协程的切换则是由用户控制的。
最早支持协程的程序语言应该是lisp方言scheme里的continuation(续延),续延允许scheme保存任意函数调用的现场,保存起来并重新执行。Lua,C#,python等语言也有自己的协程实现。
2. goroutine之间的通信是通过channel,而协程的通信是通过yield和resume()操作。
在Go里实现goroutine非常简单,只需要在函数的调用前面加关键字go即可,
go doSth()
下面的例子演示,启动10个goroutines分别打印索引。
package main
import (
"fmt"
"time"
)
func main() {
for i:=1;i<10;i++ {
go func(i int) {
fmt.Println(i)
}(i)
}
//暂停一会,保证打印全部结束
time.Sleep(1e9)
}
在java的世界里,并发主要是靠锁住临界资源(共享内存)来保证同步的。而channel则是goroutinues之间进行通信的利器。
channel可以形象比喻为工厂里的传送带,一头的生产者goroutine往传输带放东西,另一头的消费者goroutinue则从输送带取东西。channel实际上是一个有类型的消息队列,遵循先进先出的特点。
1. channel的操作符号
ch <- ele 表示ele被发送给channel ch;
ele2 <- ch 表示从channel ch取一个值,然后赋给ele2
2. 阻塞式channel
channel默认是没有缓冲区的,也就是说,通信是阻塞的。send操作必须等到有消费者accept才算完成。
举个栗子
package main
import "fmt"
func main() {
ch1 := make(chan int)
go pump(ch1) // pump hangs
fmt.Println(<-ch1) // prints only 0
}
func pump(ch chan int) {
for i:= 0; ; i++ {
ch <- i
}
}
上面代码pump()里的channel在接受到第一个元素后就被阻塞了,直到主goroutinue拿走了数据。最终channel阻塞在接受第二个元素,程序只打印 0
3 带有buff的channel
没有buff的channel只能容纳一个元素,而带有buff的channel则可以非阻塞容纳N个元素。发送数据到buffed channel不会被阻塞,除非channel已满;同样的,从buffed channel取数据也不会被阻塞,除非channel空了。这有点像java的ConcurrentLinkedQueue。
结合goroutine和channel,可以模拟出java处理并发情况的若干情景
1. 实现future
package main
import "fmt"
import "time"
func main() {
future := heavyCalculation()
fmt.Println(<-future)
}
func heavyCalculation() (chan int) {
future := make(chan int)
go func() {
//模拟耗时计算
time.Sleep(1e9)
future <- 666
}()
return future
}
2. 实现CountDownLatch
package main
import "fmt"
func main() {
nTask := 5
ch := make(chan int)
for i:=1;i<=nTask;i++ {
go doTask(ch)
}
for i:=1;i<=nTask;i++ {
<-ch
}
fmt.Println("finished all tasks")
}
func doTask(ch chan<- int) {
//doSth...
ch<- 0
}
3. 并发访问对象
Hashtable是线程安全的,意味着多条线程同时操作hashtable对象是不会引起状态不一致的。查看Hashtable源代码可知,其几乎全部方法都添加synchronized关键字,例如put(),remove()操作。在go里,我们可以在对象内部保存一个函数类型的channel,涉及对象状态的操作都放入channel里,对象初始化的时候开启一条goroutinue,不停地执行匿名函数。
package main
import (
"fmt"
"strconv"
"time"
)
type Person struct {
Name string
salary float64
chF chan func()
}
func NewPerson(name string, salary float64) *Person {
p := &Person{name, salary, make(chan func())}
go p.backend()
return p
}
func (p *Person) backend() {
for f := range p.chF {
f()
}
}
func (p *Person) AddSalary(sal float64) {
p.chF <- func() { p.salary += sal } // (ThreadSafe)
// p.salary += sal (NotThreadSafe)
}
func (p *Person) ReduceSalary(sal float64) {
p.chF <- func() { p.salary -= sal } // (ThreadSafe)
// p.salary -= sal (NotThreadSafe)
}
func (p *Person) Salary() float64 {
fChan := make(chan float64)
p.chF <- func() { fChan <- p.salary }
return <-fChan
}
func (p *Person) String() string {
return p.Name + " - salary is: " +
strconv.FormatFloat(p.Salary(), 'f', 2, 64)
}
func main() {
p := NewPerson("Kingston", 8888.8)
fmt.Println(p)
for i:=1;i<=500;i++ {
go func() {
p.AddSalary(1);
}()
}
for i:=1;i<=500;i++ {
go func() {
p.ReduceSalary(1);
}()
}
time.Sleep(3e9)
fmt.Println("After changed:")
fmt.Println(p)
}
4. 生产者消费者模式
每次涉及到并发情景,都喜欢用生产者消费者模式,因为它太经典啦
2个面包师同时生产面包,5个顾客同时取面包(尽管以下例子的打印不能说明并发的真实情况,因为channel的操作和打印的组合不是原子操作,但不影响程序的逻辑)
package main
import (
"fmt"
"time"
)
func main() {
bread := make(chan int,3)
for i:=1;i<=2;i++ {
go produce(bread)
}
for i:=1;i<=5;i++ {
go consume(bread)
}
time.Sleep(1e9)
}
func produce(ch chan<- int) {
for {
ch <- 1
fmt.Println("produce bread")
time.Sleep(100 * time.Millisecond)
}
}
func consume(ch <-chan int) {
for {
<-ch
fmt.Println("take bread")
time.Sleep(200 * time.Millisecond)
}
}