并发部分:(这部分内容 主要更多为了面试)
1、什么是线程安全?
线程安全,多线程的运行大大地提高运行速度,但与此同时会出现同一个变量进行操作时可能会出现争用的情况,比如针对的是数据而言,由于开辟多个线程执行时,面对同一块数据可能同时进行读和写,如果不加以控制,可能会出现数据读和写的结果不如预期设计所愿。所以通过数据加锁或者初始化等操作 来确保线程间相互运算时互不打扰同时能加快整个代码运行效率。
2、什么是锁? 怎么实现? 锁有哪些分类? 读写锁是什么东西? 死锁是怎么实现, 怎么判定有死锁.
锁:sync包中提供的一系列的锁来确保读写数据前保持逻辑上的串行性
实现:通常是在读与写的前后分别加锁与解锁
如下图所示:
var lock sync.Mutex
lock.Lock()
x = x + 1
lock.Unlock()
锁的类型:
1、互斥锁sync.Mutex
2、读写互斥锁sync.RWMutex
读写锁:sync.RWMutex
主要场合用于读的情况远大于写的情况,比如浏览新闻端网页,我们能观看新闻,同时能能别人在新闻底下留下的评论,但是该场合大多都是读远大于写,即很多人只会看一些新闻,不太会主动写评论。读写锁在该场合下比互斥锁效率更高。
死锁是怎么实现, 怎么判定有死锁.
死锁面试题 https://blog.csdn.net/hd12370/article/details/82814348
死锁deadlock
,理论上说是多个线程运行时,对于同一块资源争用,等待着资源释放不然就会等待suspend
,但是随着多个线程运行都争用这一块资源,而一直没有释放,会导致宕机,也就是死锁的产生。
3、代码实现: A B 两个线程同时执行 , 实现 A B 间隔打印
golang讨论区:https://studygolang.com/topics/4491
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(1)
c1 := make(chan struct{}, 1)
c2 := make(chan struct{}, 1)
for i := 1; i <= 10; i++ {
go func(i int) {
<-c1
fmt.Println("A")
c2 <- struct{}{}
}(i)
}
for i := 1; i <= 10; i++ {
go func(i int) {
<-c2
fmt.Println("B")
c1 <- struct{}{}
}(i)
}
c1 <- struct{}{}
c2 <- struct{}{}
time.Sleep(3 * time.Second)
}
4、实现生产者和消费者
参考网站:https://www.cnblogs.com/zhangweizhong/p/12056118.html
生产者消费者模型
生产者:发送数据端
消费者:接收数据端
缓冲区:
1. 解耦(降低生产者和消费者之间耦合度)
2. 并发(生产者消费者数量不对等时,能保持正常通信)
3. 缓存(生产者和消费者 数据处理速度不一致时,暂存数据)
如何实现?
生产者消费者模型是非常常见的并发模型,而且golang提供了chan类型,可以很方便的实现。
根据 golang的官方文档,使用chan就可以实现生产者和消费者之间的数据和状态同步。
- 通过chan在生产者和消费者之间传递数据(ch)和同步状态(done);
- chan作为参数传递时是引用传递,不需要使用指针;
- chan是协程安全的,多个goroutine之间不需要锁;
- chan的close事件可以被recv获取,close事件一定在正常数据读完之后,机制类似于read到EOF;
ackage main
import (
"fmt"
"math/rand"
"sync"
"time"
)
//使用goroutine和channel实现一个计算int64随机数各位数和的程序。
//1.开启一个goroutine循环生成int64类型的随机数,发送到jobChan
//2.开启24个goroutine从jobChan中取出随机数计算各位数的和,将结果发送到resultChan
//3.主goroutine从resultChan取出结果并打印到终端输出
type job struct {
value int64
}
type result struct {
job *job
sum int64
}
var jobChan = make(chan *job, 100)
var resultChan = make(chan *result, 100)
var wg sync.WaitGroup
func generate_num(ch1 chan<- *job) {
defer wg.Done()
for {
x := rand.Int63()
newJob := &job{
value: x,
}
ch1 <- newJob
time.Sleep(time.Millisecond * 500)
}
}
func summary_num(ch1 <-chan *job, ch2 chan<- *result) {
defer wg.Done()
for {
newjob := <-ch1
n := newjob.value
sum := int64(0)
for n > 0 {
sum += n % 10
n = n / 10
}
newResult := &result{
job: newjob,
sum: sum,
}
ch2 <- newResult
}
}
func main() {
//1.开启一个goroutine循环生成int64类型的随机数,发送到jobChan
wg.Add(1)
go generate_num(jobChan)
//2.开启24个goroutine从jobChan中取出随机数计算各位数的和,将结果发送到resultChan
wg.Add(24)
for i := 0; i < 24; i++ {
go summary_num(jobChan, resultChan)
}
//3.主goroutine从resultChan取出结果并打印到终端输出
for x := range resultChan {
fmt.Printf(" value : %d , sum : %d\n", x.job.value, x.sum)
}
wg.Wait()
}
简单说明:
1、首先创建一个双向的channel,
2.、然后开启一个新的goroutine,把双向通道作为参数传递到producer方法中,同时转成只写通道。子协程开始执行循环,向只写通道中添加数据,这就是生产者。
3、主协程,直接调用consumer方法,该方法将双向通道转成只读通道,通过循环每次从通道中读取数据,这就是消费者。
注意:channel作为参数传递,是引用传递。
缓冲区的好处
1:解耦
假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会直接影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合度也就相应降低了。
2:处理并发
生产者直接调用消费者的某个方法,还有另一个弊端。由于函数调用是同步的(或者叫阻塞的),在消费者的方法没有返回之前,生产者只好一直等在那边。万一消费者处理数据很慢,生产者只能无端浪费时间。
使用了生产者/消费者模式之后,生产者和消费者可以是两个独立的并发主体。生产者把制造出来的数据往缓冲区一丢,就可以再去生产下一个数据。基本上不用依赖消费者的处理速度。
其实最当初这个生产者消费者模式,主要就是用来处理并发问题的。
3:缓存
如果生产者制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。
【面试高频问题】线程、进程、协程
链接:https://zhuanlan.zhihu.com/p/70256971