目录
1、select
select关键字,可以同时响应多个通道的操作
select {
case <-chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}
package main
import (
"fmt"
"time"
)
func main() {
// 创建2个管道
int_chan := make(chan int, 1)
string_chan := make(chan string, 1)
go func() {
//time.Sleep(2 * time.Second)
int_chan <- 1
}()
go func() {
string_chan <- "hello"
}()
//1、select可以同时监听一个或多个channel,直到其中一个channel ready
//2、多个channel同时ready,则随机选择一个执行
select {
case value := <-int_chan:
fmt.Println("int:", value)
case value := <-string_chan:
fmt.Println("string:", value)
}
//3、可以用于判断管道是否存满
output3 := make(chan string, 10)
go write(output3)
//取数据,会一直运行下去
for s := range output3 {
fmt.Println("res:", s)
time.Sleep(time.Second)
}
}
func write(ch chan string) {
for {
select {
// 写数据
case ch <- "hello":
fmt.Println("write hello")
default:
fmt.Println("channel full")
}
time.Sleep(time.Millisecond * 500)
}
}
2、并发安全和锁(会涉及内核态和上下文切换,代价较高)
并发访问临界资源需要用到锁
互斥锁
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
for i := 0; i < 5000; i++ {
lock.Lock() // 加锁
x = x + 1
lock.Unlock() // 解锁
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
多个goroutine同时等待一个锁时,唤醒的策略是随机的
读写互斥锁
可以同时读,读时不可写,写时不可写不可读,适合读多写少的场景
var (
x int64
wg sync.WaitGroup
lock sync.Mutex
rwlock sync.RWMutex
)
func write() {
// lock.Lock() // 加互斥锁
rwlock.Lock() // 加写锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
rwlock.Unlock() // 解写锁
// lock.Unlock() // 解互斥锁
wg.Done()
}
func read() {
// lock.Lock() // 加互斥锁
rwlock.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwlock.RUnlock() // 解读锁
// lock.Unlock() // 解互斥锁
wg.Done()
}
func main() {
start := time.Now()
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 1000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}
3、Sync实现同步
基本用法
sync.WaitGroup是一个结构体,传递的时候要传递指针。
var wg sync.WaitGroup
func hello() {
defer wg.Done()
fmt.Println("Hello Goroutine!")
}
func main() {
wg.Add(1)
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
wg.Wait()
}
sync.Once
sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。
//用法:func (o *Once) Do(f func()) {}
var icons map[string]image.Image
var loadIconsOnce sync.Once
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 是并发安全的
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}
Go语言中内置的map不是并发安全的
Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。
package main
import (
"fmt"
"strconv"
"sync"
)
var m = sync.Map{}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n)
value, _ := m.Load(key)
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}
4、原子操作(atomic包)(仅在用户态,代价比加锁低)
atomic包
原子操作(atomic包) · Go语言中文文档 (topgoer.com)
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
var x int64
var l sync.Mutex
var wg sync.WaitGroup
// 普通版加函数
func add() {
// x = x + 1
x++ // 等价于上面的操作
wg.Done()
}
// 互斥锁版加函数
func mutexAdd() {
l.Lock()
x++
l.Unlock()
wg.Done()
}
// 原子操作版加函数
func atomicAdd() {
atomic.AddInt64(&x, 1)
wg.Done()
}
func main() {
start := time.Now()
for i := 0; i < 10000; i++ {
wg.Add(1)
// go add() // 普通版add函数 不是并发安全的
// go mutexAdd() // 加锁版add函数 是并发安全的,但是加锁性能开销大 4.755ms
go atomicAdd() // 原子操作版add函数 是并发安全,性能优于加锁版, 4.6828ms
}
wg.Wait()
end := time.Now()
fmt.Println(x)
fmt.Println(end.Sub(start))
}
5、GMP 原理与调度
GMP 原理与调度 · Go语言中文文档 (topgoer.com)
6、爬虫小案例
案例:并发下载图片第5页 | 必应每日高清壁纸 - 精彩,从这里开始 (ioliu.cn)
没实现图片的下载,发现找到的路径不是下载路径,放弃了,大概是这么个意思
package main
import (
"fmt"
"io/ioutil"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"time"
htmlquery "github.com/antchfx/htmlquery"
)
var wg sync.WaitGroup
var wgd sync.WaitGroup
// 存放图片链接的数据管道
var chanImageUrls chan string
//并发爬取壁纸
//https://bing.ioliu.cn/?p=1
func HandleError(err error, why string) {
if err != nil {
fmt.Println(why, err)
}
}
func getImgUrls(url string) {
defer wg.Done()
//爬取每一页的图片链接
urls := getImgs(url)
//遍历切片里所有链接,存入数据管道
for _, url := range urls {
chanImageUrls <- url
}
fmt.Printf("%s 完成了爬取任务\n", url)
}
func getImgs(url string) (urls []string) {
pageStr := getPage(url)
//1、使用正则匹配
//<a class="mark" href="/photo/AstoriaBridge_ZH-CN5052905610?force=home_2"></a>
var reImg = `"/photo/(.*?)"`
re := regexp.MustCompile(reImg)
results := re.FindAllString(pageStr, -1)
baseurl := "https://bing.ioliu.cn"
//不知道为什么匹配结果都带“”,不应该只出现()里的内容吗??
for _, result := range results {
//fmt.Println(i, result)
urls = append(urls, baseurl+result[1:len(result)-1])
}
//fmt.Println(urls)
return
}
func getImgsXpath(url string) {
//2、使用xpath匹配
//go get github.com/antchfx/htmlquery
resp, err := htmlquery.LoadURL(url) //Load HTML document from URL.
HandleError(err, "htmlquery.LoadURL")
//defer resp.Body.Close() //怎么关闭
results := htmlquery.Find(resp, "/html/body/div[3]/div/div/a[@href]")
for _, n := range results {
fmt.Println(htmlquery.SelectAttr(n, "href")) // output @href value
}
}
func getPage(url string) (pageStr string) {
resp, err := http.Get(url)
HandleError(err, "http.Get url")
defer resp.Body.Close()
pageBytes, err := ioutil.ReadAll(resp.Body)
HandleError(err, "ioutil.ReadAll")
pageStr = string(pageBytes)
return pageStr
}
func DownloadImg() {
defer wgd.Done()
for url := range chanImageUrls {
filename := GetFilenameFromUrl(url)
ok := DownloadFile(url, filename)
if ok {
fmt.Printf("%s 下载成功\n", filename)
} else {
fmt.Printf("%s 下载失败\n", filename)
}
}
}
// 下载图片保存的名字
func GetFilenameFromUrl(url string) (filename string) {
// 返回最后一个/的位置
lastIndex := strings.LastIndex(url, "/")
// 切出来
filename = url[lastIndex+1:]
// 时间戳解决重名
timePrefix := strconv.Itoa(int(time.Now().UnixNano()))
filename = timePrefix + "_" + filename
return
}
// 下载图片
func DownloadFile(url string, filename string) (ok bool) {
resp, err := http.Get(url)
HandleError(err, "http.get.url")
defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body)
HandleError(err, "resp.body")
filename = "D:/tempdownforgo/img/" + filename
// 写出数据
err = ioutil.WriteFile(filename, bytes, 0666)
if err != nil {
fmt.Println(url)
return false
} else {
return true
}
}
func main() {
//1、测试爬取
// pageStr := getPage("https://bing.ioliu.cn/?p=1")
// fmt.Println(pageStr)
// getImgs("https://bing.ioliu.cn/?p=1")
// getImgsXpath("https://bing.ioliu.cn/?p=2")
//1、初始化管道
chanImageUrls = make(chan string, 1000)
// 2、并发爬取
for i := 1; i < 5; i++ {
wg.Add(1)
go getImgUrls("https://bing.ioliu.cn/?p=" + strconv.Itoa(i))
}
wg.Wait()
//3、并发下载
for i := 0; i < 3; i++ {
wgd.Add(1)
go DownloadImg()
}
wgd.Wait()
}