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 := 50
    offset := 0
    maxCount := 20
    //create channel
    chs := make([]chan []*prd.Series, maxCount)
    selectCase := make([]reflect.SelectCase, maxCount)
    for i := 0; 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", "9200", "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 = 0
    this.total = 0
}
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 < 25 {
            //跳出,执行任务
            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(1)
    //减少计数器
    defer this.wg.Done()

    //加计数器
    lock.Lock()
    counter++
    fmt.Println("add task, coutner:", counter)
    lock.Unlock()

    //设置初始成功写入的数量
    succCount = 0

    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", 30)

    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 = 0
    this.lock = &sync.Mutex{}
}
func NewDB() (db *Mysql) {
    db = new(Mysql)
    db.New()
    return db
}
func (this *Mysql) GetTotal() (t int64) {
    t = 0
    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 < 0 {
        sr = 0
    }
    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[0:]
    //更改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", "", -1)
}
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语言还是很有帮助的。

 

转载于:https://www.cnblogs.com/liughost/p/5652877.html

<think>好的,我现在需要回答用户关于Go语言及其协程的问题。用户之前询问过RPC和HTTP的问题,现在转向Go语言,可能是在构建分布式系统或微服务,因此需要了解Go的并发特性。 首先,我需要确认自己对Go语言的了解是否全面。Go的协程(goroutine)是其并发模型的核心,还有通道(channel)用于协程间通信。用户可能想知道协程的优势、工作原理以及与其他语言线程的区别。 接下来,用户可能关心协程的实际应用场景,比如如何处理高并发、协程调度机制,以及如何避免常见问题,比如竞态条件或死锁。需要解释GMP模型,即Goroutine、M(机器线程)、P(处理器)的关系,说明Go运行时如何高效管理协程。 还要考虑用户可能的疑问,比如协程的资源消耗,为什么比线程轻量,以及如何通过同步机制如WaitGroup或Mutex来管理协程。需要举例说明,比如并发处理HTTP请求,或者并行计算。 此外,用户可能对协程的错误处理感兴趣,比如如何捕获协程中的panic,或者如何优雅地关闭协程。需要提及recover函数和context包的使用。 最后,确保回答结构清晰,分点说明,包括基本概念、底层原理、使用示例和注意事项。使用代码示例时,用简单的例子展示协程的启动和通信,比如计算斐波那契数列或并发获取个API数据。 需要避免过于技术化的术语,用通俗的语言解释,同时保持准确性。可能还需要对比其他语言的并发模型,比如Java的线程与Go的协程,突出Go的优势,如内存占用低、创建成本小、调度效率高等。 总结回答的结构:先介绍Go语言及其特点,然后重点讲解协程的定义、优势、工作原理,接着是使用示例和最佳实践,最后常见问题及解决方案。确保覆盖用户可能关心的各个方面,帮助用户全面理解Go协程的应用和原理。</think>以下是关于Go语言及其协程机制的详细说明: --- ### 一、Go语言核心特点 1. **C系血统** - 语法类似C,但去除指针运算、类继承等复杂特性 - 编译型语言,直接生成机器码(编译速度极快) 2. **并发原生支持** - 协程(goroutine)为语言级并发单元 - 通道(channel)实现CSP通信模型 3. **内存管理** - 自动垃圾回收(GC) - 无手动内存管理需求 4. **工具链完善** - 内置格式化工具(go fmt) - 依赖管理(go mod) - 测试框架(go test) --- ### 二、Go协程(goroutine)深度解析 #### 1. 基本特性 ```go func main() { go func() { // 使用go关键字启动协程 fmt.Println("goroutine执行") }() time.Sleep(100 * time.Millisecond) // 等待协程完成 } ``` - **轻量级**:初始栈仅2KB(线程通常MB级) - **低成本**:创建百万级协程无压力 - **快速切换**:用户态调度,无内核切换开销 #### 2. 调度模型(GMP) ![GMP模型示意图](https://example.com/gmp-model.png)*(图示:Go调度器工作原理)* - **G**(Goroutine):协程实体 - **M**(Machine):操作系统线程 - **P**(Processor):逻辑处理器(默认等于CPU核心数) | 组件 | 职责 | 数量关系 | |------|-----|---------| | G | 执行单元 | 动态变化 | | M | 系统线程 | 默认<=10000 | | P | 资源调度 | 默认=CPU核心数 | #### 3. 调度策略 - **工作窃取**(Work Stealing):空闲P从其他P队列偷G - **抢占式调度**:10ms强制切换(防止协程独占) - **系统调用优化**:分离网络轮询器(netpoller) #### 4. 并发控制 ```go var wg sync.WaitGroup func worker() { defer wg.Done() // 业务逻辑 } func main() { wg.Add(10) for i := 0; i < 10; i++ { go worker() } wg.Wait() // 等待所有协程完成 } ``` --- ### 三、协程 vs 线程对比 | 维度 | Go协程 | 系统线程 | |---------------|-------------------|------------------| | 创建成本 | 2KB初始内存 | 1MB以上 | | 切换开销 | 用户态200ns | 内核态1-2μs | | 调度方式 | Go运行时协作调度 | OS内核抢占调度 | | 并发规模 | 轻松百万级 | 通常数千级 | | 内存占用 | 动态伸缩(max1GB)| 固定栈大小 | --- ### 四、最佳实践与注意事项 1. **通道使用原则** - 优先使用`chan struct{}`作为信号通道 - 带缓冲通道不是队列,慎用`cap(chan) > 0` 2. **资源竞争防范** ```go var counter int var mu sync.Mutex func safeIncrement() { mu.Lock() defer mu.Unlock() counter++ } ``` 3. **协程泄漏检测** - 使用`runtime.NumGoroutine()`监控协程数量 - 确保所有协程都有退出路径 4. **性能调优技巧** - 设置`GOMAXPROCS`(通常无需调整) - 避免在热路径中使用`defer` - 使用`sync.Pool`减少内存分配 --- ### 五、典型应用场景 1. **高并发服务** ```go func handleConn(conn net.Conn) { defer conn.Close() // 处理连接 } func main() { ln, _ := net.Listen("tcp", ":8080") for { conn, _ := ln.Accept() go handleConn(conn) // 每个连接独立协程 } } ``` 2. **并行计算** ```go func parallelProcess(data []int) { ch := make(chan result) for _, d := range data { go func(v int) { ch <- compute(v) // 并行计算 }(d) } // 收集结果... } ``` 3. **定时任务调度** ```go func cronJob() { ticker := time.NewTicker(1 * time.Hour) defer ticker.Stop() for { select { case <-ticker.C: doTask() } } } ``` --- Go语言的协程机制彻底改变了并发编程范式,使得开发者可以用同步的方式编写异步代码,极大降低了并发编程的心智负担。其设计在云计算、微服务、分布式系统等领域展现出显著优势,成为现代后端开发的利器。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值