func main() {
var data int
go func() {
data++ //3行
}()
if data == 0{ //5行
fmt.Printf("the value is %v.\n",data) //6行
}
}
// the value is 0. 【结果】
以上代码有三种可能的结果: 1。 不打印任何东西。在这种情况下,第3行执行,接下来第5行; 2。 打印"the value is 0" . 第5行,6行在第3行之前执行; 3。 打印"the value is 1" . 第5行执行,接下来,第3行,接下来,第6行 引入数据竞争的原因是因为开发人员在用顺序性的思维来思考问题。 分析可能存在的运行的多种结果:有时候想象在两个操作之间会经过很长一段时间;如调用goruntine的时间和它运行的时间相差1个小时。 有些情况,有些开发人员在他们的代码中使用了很多这种休眠语句time.Sleep。这种方式只能辅助调试。 结论是:应该始终以逻辑正确性为目标。在代码中引入休眠可以方便调试,但是这种并不是一种解决方案。
原子性: 某些程序/东西 是原子性的,这意味着在它运行的 环境中/上下文,它是不可分割的 或者不可中断的。 在你的进程上下文中进行原子操作,在操作系统的上下文中可能不是原子操作;在操作系统中原子操作,在机器环境中可能就不是原子的。 大多数语句不是原子的,更不用说函数,方法和程序了。 在考虑原子性时,经常第一件需要做的事就是 定义/确定 它的上下文,然后再考虑这些操作是否是原子性的。 分析例子: i++ 它经历三个步骤 1。 检索i的值 2。 增加i的值 3。 存储i的值
临界区:
func main() {
var data int
go func() {
data++ //3行
}()
if data == 0{ //5行
fmt.Printf("the value is %v.\n",data) //6行
}else{
fmt.Printf("the value is %v.\n", data)
}
}
/**
临界区:critical section
程序中需要独占访问共享资源的部分。
data++ 正在增加数据变量。
if 语句,它检查数据的值是否为0
fmt.Printf语句, 在检索并输出数据的值。
*/
死锁:
type value struct {
mu sync.Mutex
value int
}
func main() {
var wg sync.WaitGroup
printSum := func(v1, v2 *value){
defer wg.Done()
v1.mu.Lock()
defer v1.mu.Unlock()
time.Sleep(2 * time.Second)
v2.mu.Lock()
defer v2.mu.Unlock()
fmt.Printf("sum=%v\n", v1.value + v2.value)
}
var a, b value
wg.Add(2)
go printSum(&a, &b)
go printSum(&b, &a)
wg.Wait()
}
//fatal error: all goroutines are asleep - deadlock! //运行时的图形表示。todo 图1-1 //出现死锁有几个必要条件/ Coffman 条件 /** 1。 相互排斥。;并发进程 拥有资源的独占权 2。 条件等待。 并发进程必须同时拥有一个资源,并等待额外的资源 3。 没有抢占。 并发进程拥有的资源只能被 该进程释放 4。 循环等待。 */
活锁:
//活锁
func main() {
cadence := sync.NewCond(&sync.Mutex{})
go func() {
for range time.Tick(1* time.Millisecond){
cadence.Broadcast()
}
}()
takeStep := func() {
cadence.L.Lock()
cadence.Wait() //接收其他 goruntine 做完的信号
cadence.L.Unlock()
}
//dirName 方向名称
//dir 该方向所在人数
//在该方向上只走一步,如果不能走,就退回原位
tryDir := func(dirName string, dir *int32, out *bytes.Buffer) bool {
fmt.Fprintf(out, "%v", dirName)
atomic.AddInt32(dir, 1) //原子加1 操作; 1毫秒时间足够保证两个人都执行了 加 1操作。
takeStep() //阻塞等待信号
if atomic.LoadInt32(dir) == 1 {
fmt.Fprint(out, ". Success!")
return true
}
//没走成功; 等一个时间间隔,往相反方向走一步
takeStep()
atomic.AddInt32(dir , -1) //原子减1操作
return false
}
var left, right int32
tryLeft := func(out *bytes.Buffer) bool {
return tryDir("left", &left, out)
}
tryRight := func(out *bytes.Buffer) bool {
return tryDir("right", &right, out)
}
walk := func(walking *sync.WaitGroup, name string) {
var out bytes.Buffer
defer func() {
fmt.Println(out.String())
}()
defer walking.Done()
fmt.Fprintf(&out, "%v is trying to scoot:", name)
for i:=0; i<5; i++ {
if tryLeft(&out) || tryRight(&out){
return
}
}
fmt.Fprintf(&out, "\n%v tosses her hands up in exasperation!", name)
}
var peopleInHallway sync.WaitGroup
peopleInHallway.Add(2)
go walk(&peopleInHallway, "Alice")
go walk(&peopleInHallway, "Barbara")
peopleInHallway.Wait()
}
活锁: 例子: 你走在走廊上 走向另一个人?她移动到一边让你通过,但是你也做了同样的事情。想象下这个情形永远持续走下去,这就是活锁。 出现活锁的原因:两个或两个以上的并发进程试图在没有协调的情况下防止死锁 活锁是 饥饿问题的子集
饥饿:
func main() {
var wg sync.WaitGroup
var sharedLock sync.Mutex
const runtime = 1*time.Second
//饥饿的goruntine
greedyWorker := func() {
defer wg.Done()
var count int
for begin := time.Now(); time.Since(begin) <= runtime; {
sharedLock.Lock()
time.Sleep(3*time.Nanosecond)
sharedLock.Unlock()
count++
}
fmt.Printf("Greedy worker was able to execute %v work loops\n", count)
}
politeWorker := func() {
defer wg.Done()
var count int
for begin := time.Now(); time.Since(begin) <= runtime;{
sharedLock.Lock()
time.Sleep(1 *time.Nanosecond)
sharedLock.Unlock()
sharedLock.Lock()
time.Sleep(1*time.Nanosecond)
sharedLock.Unlock()
sharedLock.Lock()
time.Sleep(1*time.Nanosecond)
sharedLock.Unlock()
count++
}
fmt.Printf("Polite worker was able to excute %v work loops\n",count)
}
wg.Add(2)
go greedyWorker()
go politeWorker()
wg.Wait()
}
//Greedy worker was able to execute 569243 work loops 【结果】
//Polite worker was able to excute 331233 work loops
在任何情况下,并发进程都无法获得执行工作所需的所有资源。 贪婪的worker 不必要地扩大其持有共享锁上的临界区,并阻止平和的worker 的 gorountine高效的工作。 识别饥饿 方法: metric: 通过记录来确定进程工作速度是否和你预期的一样高。 如果你使用了内存访问同步,你将不得不在粗粒度同步 和 细粒度同步之间找到一个平衡。 饥饿可以应用于 CPU, 内存, 文件句柄,数据连接;任何必须共享的资源都是饥饿的候选者。
如何确定并发安全:?
传统的情况: 如何利用并发编写代码,以及如何安全地使用代码并不总是那么明确。 注释可以带来一些改观。注释涵盖了这些内容: 谁负责并发? 如何利用并发原语解决这个问题? 谁负责同步? Go语言处理并发的方式实际上可以帮助更清楚的表达问题域。
GO面对复杂性的简单性 运行时和通信困难并不是Go语言解决的,但使他们变得非常容易。 Go 的 低延GC, 很出色,从Go1.8开始,GC暂停一般在10-100μs 之间 Go语言的运行时 也会自动处理并发操作到操作系统线程上, 并可在线程间均匀映射。 需求:我想编写一个Web服务器,希望每个连接都可以被其他连接同时处理。 传统方式: 在某些语言中,在Web服务器开始接受连接之前,你可能必须创建一个线程集合(通常称为线程池), 然后将传入连接映射到线程。在每个线程 上,你需要循环该线程上的所有连接,以确保他们都获得了一些CPU时间。
并发和并行的区别:?
并发和并行的区别: 并发属于代码;并行属于一个运行中的程序 首先,我们并没有编写并行的代码,只有我们希望可以并行执行的并发代码,另外,并行是我们程序运行时的属性,而不是我们的代码 传统的: Go语言之前,大部分主流编程语言都有一系列的抽象层。如果你想写并发代码,你需要对你的程序按照 线程以及对于内存访问之间使用 同步来建模。 Go语言并没有在 操作系统上增加了另外一层的抽象层, 我们取代了这些事物。线程依旧存在,但是我们发现几乎不再操作系统的线程层面来考虑我们的问题, 而是 在 goroutine 以及channel 的角度来思考问题,偶尔站在共享内存的角度来思考。 Go 语言并发原语的根基论文: Communicating Sequential Processes (通信顺序进程)
CSP 即 "Communicating Sequential Processes" (通信顺序进程): 既是一个技术名词,也是介绍这种技术论文的名字。(发表于 1978) Hoare 可能应该使用 "函数" 这个词汇 代替 "Processes" 更合适。 为了在进程之间通信,Hoare 创建了输入语输出的命令: !代表发送输入到一个进程; ? 代表读取一个进程的输出。 例子:需要构建一个端上请求字段的Web服务器, 在一个仅提供线程抽象的语言中,很可能需要思考下面问题: 1。 我的语言原生支持线程吗?还是我需要选择一个类库? 2。 我的进程限制边界应该是什么? 3。 线程在操作系统中的 权重? 4。 我的程序需要运行的操作系统(各种操作系统如 windows linux等) 处理这些线程的时候有什么不同? 5。 我需要创建一个工作线程池,该如何找到最佳的线程池大小? 某个语言有一个可以把并行抽象出来的框架,但并不意味着这种自然的方式(go 方式)对并发问题建模并不重要,总有人必须写这些框架,而你的代码 就需要编写在开发者 所构建的复杂的基础之上。复杂性在你编写代码的时候被隐藏了,并不意味着复杂性本身不存在,而复杂性正是滋生bug的温床。 Go 语言的 select 语句使你可以高效的等待事件,从一个竞争的channel中随机地选择一个消息,并在没有消息时继续等待。 Go语言的并发哲学 追求简洁,尽量使用channel,并且认为gorountine的使用是没有成本的。 使用通信来共享内存,而不是通过共享内存来通信。 Go语言还支持通过内存访问同步 和 遵循该技术的原语 来编写并发代码的传统方式。sync 与其他包中的结构体与方法可以让你执行锁,创建资源池取代 gorountine todo 图2-1;