golang的多协程实践
go语言以优异的并发特性而闻名,刚好手上有个小项目比较适合。
项目背景:
公司播控平台的数据存储包括MySQL和ElasticSearch(ES)两个部分,编辑、运营的数据首先保存在MySQL中,为了实现模糊搜索和产品关联推荐,特别增加了ES,ES中保存的是节目集的基本信息。
本项目是为了防止实时同步数据出现问题或者系统重新初始化时的全量数据同步而做的。项目主要是从MySQL读取所有的节目集数据写入到ES中。
项目特点:
因为节目集数量较大,不能一次性的读入内存,因此每次读出一部分记录写入ES。ORM使用的是beego。为了提高性能使用了协程,其中读MySQL的部分最大开启20个协程,ES写入部分开启了15个协程(因为ES分片设置的问题,5个协程和15个协程性能映像不大)。
项目主要包括三个文件:
1、PrdES_v3.go,项目的入口,负责协调MySQL读取和ES写入。
package main import (
"./db"
"./es"
//"encoding/json"
"fmt"
"reflect"
"time"
) type PrdES struct {
DB *prd.Mysql
ES *es.Elastic
} // func (this *PrdES) Handle(result []*prd.Series) {
// // for _, value := range result {
// // this.DB.FormatData(value)
// // //json, _ := json.Marshal(value)
// // //fmt.Println(string(json))
// // }
// //写入ES,以多线程的方式执行,最多保持5个线程
// this.ES.DoBulk(result)
// }
func (this *PrdES) Run() {
count :=
offset :=
maxCount :=
//create channel
chs := make([]chan []*prd.Series, maxCount)
selectCase := make([]reflect.SelectCase, maxCount)
for i := ; i < maxCount; i++ {
offset = count * i
fmt.Println("offset:", offset)
//init channel
chs[i] = make(chan []*prd.Series)
//set select case
selectCase[i].Dir = reflect.SelectRecv
selectCase[i].Chan = reflect.ValueOf(chs[i])
//运行
go this.DB.GetData(offset, count, chs[i])
}
var result []*prd.Series
for {
//wait data return
chosen, recv, ok := reflect.Select(selectCase)
if ok {
fmt.Println("channel id:", chosen)
result = recv.Interface().([]*prd.Series) //读取数据从mysql
go this.DB.GetData(offset, count, chs[chosen]) //写入ES,以多线程的方式执行,最多保持15个线程
this.ES.DoBulk(result)
//update offset
offset = offset + len(result)
//判断是否到达数据尾部,最后一次查询
if len(result) < count {
fmt.Println("read end of DB")
//等所有的任务执行完毕
this.ES.Over()
fmt.Println("MySQL Total:", this.DB.GetTotal(), ",Elastic Total:", this.ES.GetTotal())
return }
}
} } func main() {
s := time.Now()
fmt.Println("start")
pe := new(PrdES) pe.DB = prd.NewDB()
pe.ES = es.NewES()
//fmt.Println("mysql info:")
//fmt.Println("ES info:")
pe.Run() fmt.Println("time out:", time.Since(s).Seconds(), "(s)")
fmt.Println("Over!") }
在run函数里可以看到使用了reflect.SelectCase,使用reflect.SelectCase的原因是读MySQL数据是多个协程,不可预计哪个会首先返回,selectCase是任何一个处理完毕reflect.Select函数就会返回,MySQL读取的数据放在channel中宕Select函数返回时chosen, recv, ok := reflect.Select(selectCase)判断ok是否未true chosen代表的是协程id通过result = recv.Interface().([]*prd.Series)获得返回的数据,因为MySQL读取的数据是对象的结果集,因次使用recv.Interface函数,如果是简单类型可以使用recv.recvInt(),recv.recvString()等函数直接获取channel返回数据。
这里通过counter控制协程的数量,也可以通过channel,用select的方式控制协程的数量,之所以用counter计数器的方式控制协程数量是我想知道同时有多少协程在运行。
注:此处channel可以不用创建数组形式,channel带回来的数据也没有顺序问题。
2、es.go,负责写入ES和es的写入协程调度
package es import (
"../db"
//"encoding/json"
"fmt"
elastigo "github.com/mattbaird/elastigo/lib"
//elastigo "github.com/Uncodin/elastigo/lib"
//"github.com/Uncodin/elastigo/core"
"time"
//"bytes"
"flag"
"sync"
//"github.com/fatih/structs"
) var (
//开发测试库
//host = flag.String("host", "192.168.1.236", "Elasticsearch Host")
//C平台线上
host = flag.String("host", "192.168.100.23", "Elasticsearch Host")
port = flag.String("port", "", "Elasticsearch port")
) //indexor := core.NewBulkIndexorErrors(10, 60)
// func init() {
// //connect to elasticsearch
// fmt.Println("connecting es")
// //api.Domain = *host //"192.168.1.236"
// //api.Port = "9300" // }
//save thread count
var counter int type Elastic struct {
//Seq int64
c *elastigo.Conn
lock *sync.Mutex
lockTotal *sync.Mutex
wg *sync.WaitGroup
total int64
} func (this *Elastic) Conn() {
this.c = elastigo.NewConn()
this.c.Domain = *host
this.c.Port = *port
//NewClient(fmt.Sprintf("%s:%d", *host, *port))
}
func (this *Elastic) CreateLock() {
this.lock = &sync.Mutex{}
this.lockTotal = &sync.Mutex{}
this.wg = &sync.WaitGroup{}
counter =
this.total =
}
func NewES() (es *Elastic) {
//connect elastic
es = new(Elastic)
es.Conn()
//create lock
es.CreateLock()
return es
}
func (this *Elastic) DoBulk(series []*prd.Series) {
for true {
this.lock.Lock()
if counter < {
//跳出,执行任务
break
} else {
this.lock.Unlock()
//等待100毫秒
//fmt.Println("wait counter less than 25, counter:", counter)
time.Sleep(1e8)
}
}
this.lock.Unlock()
//执行任务
go this.bulk(series, this.lock)
}
func (this *Elastic) Over() {
this.wg.Wait()
/*for {
this.lock.Lock()
if counter <= 0 {
this.lock.Unlock()
break
}
this.lock.Unlock()
}
*/
} func (this *Elastic) GetTotal() (t int64) {
this.lockTotal.Lock()
t = this.total
this.lockTotal.Unlock()
return t
}
func (this *Elastic) bulk(series []*prd.Series, lock *sync.Mutex) (succCount int64) {
//增加计数器
this.wg.Add()
//减少计数器
defer this.wg.Done() //加计数器
lock.Lock()
counter++
fmt.Println("add task, coutner:", counter)
lock.Unlock() //设置初始成功写入的数量
succCount = for _, value := range series {
//json, _ := json.Marshal(value)
//fmt.Println(string(json))
if value.ServiceGroup != nil {
fmt.Println("series code:", value.Code, ",ServiceGroup:", value.ServiceGroup) resp, err := this.c.Index("guttv", "series", value.Code, nil, *value) if err != nil {
panic(err)
} else {
//fmt.Println(value.Code + " write to ES succsessful!")
fmt.Println(resp)
succCount++
}
} else {
fmt.Println("series code:", value.Code, "service group is null")
}
} //计数器减一
lock.Lock()
counter--
fmt.Println("reduce task, coutner:", counter, ",success count:", succCount)
lock.Unlock() this.lockTotal.Lock()
this.total = this.total + succCount
this.lockTotal.Unlock()
return succCount
}
在es.go里有两把锁lock和lockTotal,前者是针对counter变量,记录es正在写入的协程数量的;后者为记录总共写入多少条记录到es而增加的。
这里必须要提到的是Over函数,初步使用协程的容易忽略。golang的原则是当main函数运行结束后,所有正在运行的协程都会终止,因袭在MySQL读取数据完毕必须调用Over函数,等待所有的协程结束。这里使用sync.waiGrooup。每次协程启动执行下面两个语句:
//增加计数器
this.wg.Add(1)
//减少计数器,函数结束时自动执行
defer this.wg.Done()
Over函数中调用wg.Wait()等待计数器为0时返回,否则就一直阻塞。当然读者也可以看到通过检查counter是否小于等于0也可以判断协程是否都结束(Over函数被注释的部分),显然使用waitGroup更优雅和高效。
3、db.go,负责MySQL数据的读取
package prd import (
"fmt"
"github.com/astaxie/beego/orm"
_ "github.com/go-sql-driver/mysql" // import your used driver
"strings"
"sync"
"time"
) func init() { orm.RegisterDataBase("default", "mysql", "@tcp(192.168.100.3306)/guttv_vod?charset=utf8", ) orm.RegisterModelWithPrefix("t_", new(Series), new(Product), new(ServiceGroup))
orm.RunSyncdb("default", false, false)
} type Mysql struct {
sql string
total int64
lock *sync.Mutex
} func (this *Mysql) New() {
//this.sql = "SELECT s.*, p.code ProductCode, p.name pName FROM guttv_vod.t_series s inner join guttv_vod.t_product p on p.itemcode=s.code and p.isdelete=0 limit ?,?"
this.sql = "SELECT s.*, p.code ProductCode, p.name pName FROM guttv_vod.t_series s , guttv_vod.t_product p where p.itemcode=s.code and p.isdelete=0 limit ?,?"
this.total =
this.lock = &sync.Mutex{}
}
func NewDB() (db *Mysql) {
db = new(Mysql)
db.New()
return db
}
func (this *Mysql) GetTotal() (t int64) {
t =
this.lock.Lock()
t = this.total
this.lock.Unlock()
return t
}
func (this *Mysql) toTime(toBeCharge string) int64 {
timeLayout := "2006-01-02 15:04:05"
loc, _ := time.LoadLocation("Local")
theTime, _ := time.ParseInLocation(timeLayout, toBeCharge, loc)
sr := theTime.Unix()
if sr < {
sr =
}
return sr
}
func (this *Mysql) getSGCode(seriesCode string) (result []string, num int64) {
sql := "select distinct ref.servicegroupcode code from t_servicegroup_reference_category ref "
sql = sql + "left join t_category_product cp on cp.categorycode=ref.categorycode "
sql = sql + "left join t_package pkg on pkg.code = cp.assetcode "
sql = sql + "left join t_package_product pp on pp.parentcode=pkg.code "
sql = sql + "left join t_product prd on prd.code = pp.assetcode "
sql = sql + "where prd.itemcode=?"
o := orm.NewOrm()
var sg []*ServiceGroup
num, err := o.Raw(sql, seriesCode).QueryRows(&sg) if err == nil {
//fmt.Println(num)
for _, value := range sg {
//fmt.Println(value.Code)
result = append(result, value.Code)
} } else {
fmt.Println(err)
}
//fmt.Println(result)
return result, num
} func (this *Mysql) formatData(value *Series) {
//设置业务分组数据
sg, _ := this.getSGCode(value.Code)
//fmt.Println(sg)
value.ServiceGroup = []string{}
value.ServiceGroup = sg[:]
//更改OnlineTime为整数
value.OnlineTimeInt = this.toTime(value.OnlineTime)
//分解地区
value.OriginalCountryArr = strings.Split(value.OriginalCountry, "|")
//分解二级分类
value.ProgramType2Arr = strings.Split(value.ProgramType2, "|")
//写入记录内容
value.Description = strings.Replace(value.Description, "\n", "", -)
}
func (this *Mysql) GetData(offset int, size int, ch chan []*Series) {
var result []*Series
o := orm.NewOrm()
num, err := o.Raw(this.sql, offset, size).
QueryRows(&result)
if err != nil {
fmt.Println("read DB err")
panic(err)
//return //err, nil
}
for _, value := range result {
this.formatData(value)
//json, _ := json.Marshal(value)
//fmt.Println(string(json))
//fmt.Println(value.ServiceGroup)
}
this.lock.Lock()
this.total += num
this.lock.Unlock() fmt.Println("read count :", num) //, "Total:", Total)
//return nil, result
ch <- result
}
从项目上看。go语言开发还是比较简洁的,多协程实现也相对容易,但要求开发者必须对概念非常清晰,像select和selectCase理解和defer的理解要很到位,个人层经做过多年的C/C++程序员,从经验上看,C/C++的经验(多线程的理解)对运用go语言还是很有帮助的。
golang的多协程实践的更多相关文章
- golang中最大协程数的限制(线程)
golang中最大协程数的限制 golang中有最大协程数的限制吗?如果有的话,是通过什么参数控制呢?还是通过每个协程占用的资源计算? 通过channel控制协程数的就忽略吧. 以我的理解,计算机资源 ...
- Golang 入门 : goroutine(协程)
在操作系统中,执行体是个抽象的概念.与之对应的实体有进程.线程以及协程(coroutine).协程也叫轻量级的线程,与传统的进程和线程相比,协程的最大特点是 "轻"!可以轻松创建上 ...
- Golang的goroutine协程和channel通道
一:简介 因为并发程序要考虑很多的细节,以保证对共享变量的正确访问,使得并发编程在很多情况下变得很复杂.但是Go语言在开发并发时,是比较简洁的.它通过channel来传递数据.数据竞争这个问题在gol ...
- Java协程实践指南(一)
一. 协程产生的背景 说起协程,大多数人的第一印象可能就是GoLang,这也是Go语言非常吸引人的地方之一,它内建的并发支持.Go语言并发体系的理论是C.A.R Hoare在1978年提出的CSP(C ...
- golang:Channel协程间通信
channel是Go语言中的一个核心数据类型,channel是一个数据类型,主要用来解决协程的同步问题以及协程之间数据共享(数据传递)的问题.在并发核心单元通过它就可以发送或者接收数据进行通讯,这在一 ...
- golang 并发之协程及通道
一.概述 在golang中,每个并发执行单元称为goroutine,当程序启动时,main函数在一个单独的goroutine中运行,(main goroutine).新的goroutine会用go语句 ...
- Golang等待一组协程结束
1. 利用waitgroup import ( "log" "sync" "sync/atomic" "time" ) ...
- golang中goroutine协程调度器设计策略
goroutine与线程 /* goroutine与线程1. 可增长的栈os线程一般都有固定的栈内存,通常为2MB,一个goroutine的在其声明周期开始时只有很小的栈(2KB),goroutine ...
- 写了一年golang,来聊聊进程、线程与协程
本文已收录 https://github.com/lkxiaolou/lkxiaolou 欢迎star. 进程 在早期的单任务计算机中,用户一次只能提交一个作业,独享系统的全部资源,同时也只能干一件事 ...
随机推荐
- ExtPager ,分页
package cn.edu.hbcf.common.vo; public class ExtPager { private Integer start; private Integer limit; ...
- AVCapture编程理解
AVCapture用于媒体采集,在媒体采集的流程中,会存在如下几个对象: AVCaptureDevice.这里代表抽象的硬件设备. AVCaptureInput.这里代表输入设备(可以是它的子类),它 ...
- shell常用的判断条件
.判断文件夹是否存在 if [ -d /home/q/www ];then echo "true"; else echo "false" ;fi (系统内存在文 ...
- hdu3879 Base Station 最大权闭合子图 边权有正有负
/** 题目:hdu3879 Base Station 最大权闭合子图 边权有正有负 链接:http://acm.hdu.edu.cn/showproblem.php?pid=3879 题意:给出n个 ...
- Make Docker Image On Ubuntu17.10
1.拉取基础镜像 docker pull ubuntu 2.查看镜像 docker images 3.启动一个容器 docker run -it ubuntu 4.查找运行的容器ID docker p ...
- redission计数器实现,redisTemplate计数器
在redission 2.9.0版本之前是有BUG,在实现下面代码时,第一次是成功的,但是在第二次就会失败: RedissonClient client;//client参考别的demo RMapCa ...
- 嵌入式开发之示波器----tektronix tds2024 的使用
http://jingyan.baidu.com/article/91f5db1bf715c01c7f05e39a.html http://cn.tek.com/learning/oscillosco ...
- Otter双A同步搭建入门教程
1.准备阶段 1.1 环境 虚拟机A :192.168.0.130 虚拟机B :192.168.0.131 系统:Ubuntu 16.04 LTS JRE:OpenJDK 1.8.0_151(A.B都 ...
- 房间WIFI信号不好怎么办?——无线路由桥接(WDS)
背景 1.无线路由在客厅,房间的WIFI信号很差,只有1-2格,虽说是100M的网速,但是从客厅到房间要经过3道墙!电脑的无线接收功能一般都很一般,网速不好理所当然. 2.弄一根网线从客厅连接起来很傻 ...
- Echoprint系列--编译
近期要做一个音乐相关的client.当中一个功能是音乐识别.搜索了一些资料选择Echoprint来开发.Echoprint是开源免费的,并且多种client都支持能节约非常多时间,今天主要下载和编译源 ...