Go语言基础学习 (九) - 典型并发任务

113 阅读2分钟

前言

在极客上看了蔡超老师的Go语言课程 随手记下来的一些随笔,Go的基础应用及实例, 系列内容比较偏基础,推荐给想要入门Go语言开发者们阅读。

目录如下

Go语言基础学习 (一) - 变量 常量已经与其他语言的差异
Go语言基础学习 (二) -Go语言中的类型转与Go语言中的数组切片
Go语言基础学习 (三) - Go语言内的 Map声明使用与工厂模式
Go语言基础学习 (四) - Go语言函数简单介绍
Go语言基础学习 (五) - 面向对象编程
Go语言基础学习 (六) - 编写一个好的错误处理
Go语言基础学习 (七) - 包(package)
Go语言基础学习 (八) - 并发编程

1. 只执行一次

  • 比如PHP内的单例模式
    
         class Singleton
         {
          //创建静态私有的变量保存该类对象
          static private $instance;
    
          //防止使用new直接创建对象
          private function __construct(){}
    
          //防止使用clone克隆对象
          private function __clone(){}
    
          static public function getInstance()
          {
              //判断$instance是否是Singleton的对象,不是则创建
              if (!self::$instance instanceof self) {
                  self::$instance = new self();
              }
              return self::$instance;
          }
    
          public function test()
          {
              echo "我是一个单例模式";
          }
         }
    
         $sing  = Singleton::getInstance();
         $sing->test()
    
  • 在GO里面的单例模式

    image-20220302174655115.png 代码实例

      type Singleton struct {
    
      }
    
    
      var singleInstance *Singleton
      var once sync.Once
    
    
      func GetSingletonObj() *Singleton  {
          once.Do(func() {
          fmt.Println("Create Obj")
          singleInstance = new(Singleton)
          })
      return singleInstance
      }
    
      func TestGetSingletonObj(t *testing.T)  {
          var wg sync.WaitGroup
          for i := 0;i<10;i++  {
              wg.Add(1)
              go func(i int) {
                  obj := GetSingletonObj()
                  fmt.Printf("%d\n",unsafe.Pointer(obj))
                  wg.Done()
             }(i)
          }
         wg.Wait()
      }
    
    
    === RUN   TestGetSingletonObj
      Create Obj
      19221888
      19221888
      19221888
      19221888
      19221888
      19221888
      19221888
      19221888
      19221888
      19221888
      --- PASS: TestGetSingletonObj (0.00s)
    

    结果可以看到,我们开了10个携程去实例化这个方法,但是方法构造只打印了一次,多次调用地址也是一样

2.仅需任意任务完成/全部任务完成

  • 在某种场景下,如百度搜索,我搜索了一个关键字,但是对于这个关键字的数据信息较多,避免浪费过多资源,我可以先把搜索出来的第一个信息给展示出来,等待用户选择或者进行下一步操作

     func runTask(i int)  string {
         time.Sleep(10 * time.Millisecond)
         return  fmt.Sprintf("The result is from %d",i)
     }
    
     func FirstResponse() string  {
         numOfRunner := 10
         ch := make(chan string)    // 创建通道
         for i:=0; i<numOfRunner;i++ {
             go func(i int) {
               ret := runTask(i) // 获取任务结果
               ch <- ret  // 将第一个输出的结果发送到channel内
               fmt.Println(<-ch)
             }(i)
         }
      return  <-ch
     }
    
     func TestFirtResponse(t *testing.T)  {
         t.Log(FirstResponse())
     }
    
       The result is from 8
       The result is from 4
       The result is from 3
       first_response_test.go:46: The result is from 8
       The result is from 6
       The result is from 7
       The result is from 9
       --- PASS: TestFirtResponse (1.01s)
       The result is from 2
       The result is from 0
       The result is from 5
       PASS
       The result is from 1
    

    从打印结果可以看出,第一个执行的结果是8,然后在发送给channel,由下方接收channel 输出第一个执行的结果

    输出全部结果

    
       func AllResponse() string  {
           numOfRunner := 10
           ch := make(chan string,numOfRunner)
           for i:=0; i<numOfRunner;i++ {
               go func(i int) {
               ret := runTask(i)
               ch <- ret  // 将第一个输出的结果发送到channel内
               }(i)
           }
           finalRet := ""
           for j:=0; j<numOfRunner; j++ {
               finalRet += <- ch+"\n"
           }
           return finalRet
       }
    
       func TestFirtResponse(t *testing.T)  {
           time.Sleep(time.Second * 1)
           t.Log(AllResponse())
       }
    
    === RUN   TestFirtResponse
    first_response_test.go:45: 
    The result is from 5
    The result is from 6
    The result is from 7
    The result is from 8
    The result is from 4
    The result is from 9
    The result is from 1
    The result is from 3
    The result is from 2
    The result is from 0
    

    使用For遍历出所有携程发送的channel

3.对象池

  • 日常比较常见的对象池

    我们日常经常使用遇到的对象池有很多,比如 数据库链接,网络链接,等

    image-20220304145645409.png

    代码实例 创建对象池的一个包

       type ReuasbleObj struct { // 创建一个新的对象实例
       }
    
       type ObjPool struct {  // 定义使用channel 来存放对象池
           bufChan chan *ReuasbleObj
       }
    
    
       func NewObjPool(numOfObj int)*ObjPool  {
           objPool := ObjPool{}
           // 创建channel
           objPool.bufChan = make(chan *ReuasbleObj,numOfObj)
           //  初始化对象,有10个对象就初始化10个对象放在channel内
           for i:=0;i<numOfObj ;i++  {
                objPool.bufChan <- &ReuasbleObj{}
           }
           return &objPool
       }
    
       //  从channel 对象池内拿出一个对象使用
       func (p *ObjPool) GetObj(timeOut time.Duration) (*ReuasbleObj,error)  {
           select {
               case ret := <-p.bufChan:
                 return  ret,nil
               case <-time.After(timeOut):
                 return nil,errors.New("time out")
           }
       }
    
       //  使用完对象后返回一个到对象池内
       func (p *ObjPool) ReleaseObj(obj *ReuasbleObj) error  {
           select {
               case p.bufChan <- obj:
                 return nil
               default:
                 return errors.New("overflow")
           }
       }
    

    方法中调用

    func TestObjPool(t *testing.T)  {
      // 调用方法使用channel 来 先创建10个对象
      pool := obj_pool.NewObjPool(10)
    
      for i := 0;i<11 ;i++ {
        if v,err := pool.GetObj(time.Second * 1); err != nil {
          t.Error(err)
       }else {
          fmt.Println(&v)
          if err := pool.ReleaseObj(v); err != nil{
             t.Error(err)
          }
        }
      }
    }
    
    === RUN   TestObjPool
     0xc0000a0030
     0xc0000a0040
     0xc0000a0048
     0xc0000a0050
     0xc0000a0058
     0xc0000a0060
     0xc0000a0068
     0xc0000a0070
     0xc0000a0078
     0xc0000a0080
     0xc0000a0088
    

    这时候我们可以看到打印出来的各个对象的地址都是不同的,等于说是生成了10个不同的对象来处理业务

4. Sync.pool 对象缓存

  • 对象获取策略:

    1. 首先,尝试从私有对象获取。
    2. 其次,如果私有对象不存在,就尝试从当前Process的共享池获取
    3. 如果当前Process的共享池是空的,就尝试从其他Process的共享池获取。
    4. 如果所有Process的共享池都是空的,就从sync.pool指定的New方法中“New”一个新的对象返回。

  • sync.pool缓存对象的生命周期:

    每一次GC(垃圾回收)都会清除sync.pool的缓存对象。
    因此,对象缓存的有效期为下一次GC之前。

    代码实例

    func TestSyncPool(t *testing.T)  {
      pool := &sync.Pool{ // 创建一个新的pool 对象缓存
         New: func() interface{} { //  接收参数类型不定义
            fmt.Println("Create a new object")
            return 100
         },
      }
    
      v := pool.Get().(int)  // 取出参数
      fmt.Println(v)
      pool.Put(3)
      //runtime.GC() //  会清空sync.pool中的缓存的对象
      v1,_ := pool.Get().(int)
      fmt.Println(v1)
    
    }
    
    === RUN   TestSyncPool
    Create a new object
    100
    3
    

    当在调用对象的时候,对象创建的时候就会输出 Create a new object
    接着输出对象内放好的参数
    然后在回放一个对象进行输出,这时候输出的参数也就是我们刚才回放的对象参数

  • 使用多携程下的pool
    func TestSyncPoolInMultiGroutine(t *testing.T)  {
        pool := &sync.Pool{ // 创建一个新的pool 对象缓存
        New: func() interface{} { //  接收参数类型不定义
            fmt.Println("Create a new object")
            return 10
            }
         }
    
        pool.Put(100)
        pool.Put(100)
        pool.Put(100)
    
        var wg sync.WaitGroup
        for i:=0;i<10 ;i++  {
            wg.Add(1)
            go func(id int) {
                fmt.Println(pool.Get()) // 输出每次从pool里面get到的结果
                wg.Done()
            }(i)
        }
       wg.Wait()
    }
    
    === RUN   TestSyncPoolInMultiGroutine
     100
     100
     100
     Create a new object
     10
     Create a new object
     10
     Create a new object
     10
     Create a new object
     10
     Create a new object
     10
     Create a new object
     10
     Create a new object
     10
    

    可以看出,首先输出的是100 ,因为100是我们最先放进去的对象,接着对象获取完了之后,再去调用就会发现没对象了,这时候就回去调用创建对象的方法来创建新的对象

  • 总结:sync.pool的优点与问题

    优点:通过sync.pool降低复杂对象的创建和GC代价。

    问题:sync.pool会被GC回收,并且在并发使用中需要考虑加锁。因此,在程序中要做好取舍。(考虑是创建一个对象的代价大?还是用sync.pool加锁缓存复用的代价大?)