itsmikej 发布的文章

由并发引出的话题

并发是一个超大的话题,本文做一些简单的记录和讨论。

基本概念

有必要先了解下与并发密切相关的几个概念。

Critical Section 临界区

所谓临界区,就是在程序中一段代码不能被同时执行的部分。比如,两个线程不能同时去访问的程序代码,这段代码就是临界区。在临界区中,通常会有对共享变量读取或者写入的操作。之前我文章中提到的 就是用来保证只有一个线程处于临界区的一种机制,它保证了多个线程对共享变量的读写正常。

Thread Safe 线程安全

wikipedia中的定义:

指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

Concurrency vs Parallelism 并发 vs 并行

两个概念相似,但不同。并发是指,在同一时刻,只有一个任务在运行,只是系统通过某种调度算法,同时调度、来回切换运行两个任务,让它们“看起来”是同时在运行。并行则是说,同一时刻,两个任务“真正的”同时进行。

举个栗子,老板布置给你一些任务,很复杂,但你不能一个人同时做。所以你把任务拆分成一些小活儿,交替着干,这是并发。不过你也可以将任务分给你的同事,这样就能真正的同时做了,这是并行。

体现在计算机系统中,当多个任务执行时,单核cpu通过切换时间片(时间片就是cpu分配给各个任务的时间),将计算资源合理的分配(轮转调度)给各个任务,当时间片结束,或者任务阻塞,则cpu进行切换(好处是不会阻塞其他任务,提升了程序运行效率)。这样多个任务看起来就是同时在进行,这是并发。系统将多个任务分配到多核cpu上,这时多个任务就可以真正的同时运行,这是并行

Golang 中的并发

Golang中通过goroutine实现了并发。如下的几个函数,实现了对goroutine的调度。

  • runtime.Gosched() 出让cpu时间片
  • runtime.NumCPU() 返回当前机器cpu数量
  • runtime.GOMAXPROCS() 设置同时最大的CUP可用核数
  • runtime.Goexit() 退出当前goroutine

下面是官方的一个示例程序,会将hello和world交替输出,因为在say函数中runtime.Gosched()会主动让出时间片,将cpu资源让给另外一个goroutine,这就实现了并发。如果去掉runtime.Gosched()这一行,两个goroutine将会按顺序执行,因为执行时间很短,goroutine不会主动让出时间片。(本地测试发现,在go1.9.2环境下输出并不是完全交替,有时甚至不会输出world,说明最新的编译器的实现有了些变化?)

package main

import (
    "fmt"
    "time"
    "runtime"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        runtime.Gosched()
        // 如果将手动切换时间片的操作改成sleep操作,输出效果一样
        // 因为sleep操作也会触发切换时间片
        // time.Sleep(time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    // 这里如果强制将最大可用cpu核数设置成1
    // 则hello和world完全交替输出
    // 说明在最新版的golang中,GOMAXPROCS默认已经不是1了 ?
    // runtime.GOMAXPROCS(1)
    go say("world")
    say("hello")
}

再看第二个示例,去除了runtime.Gosched(),同时将最大可用cpu核数设置成了当前系统cpu的核数runtime.GOMAXPROCS(runtime.NumCPU())(我本地是4核),输出结果会出现交替的情况,但不绝对,因为程序执行太快退出了,调大循环次数就能看到效果。这时两个goroutine分别在不同的cpu核心上运行,实现了并行

package main

import (
    "fmt"
    "runtime"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        fmt.Println(s)
    }
}

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())
    go say("world")
    say("hello")
}

stackoverflow上的一个问题,对golang的中的并发与并行的一些知识点和原理解释得很清楚,值得一看。

实际应用

如果使用Golang,通常我们可以将一个复杂的任务拆分成n个相关性不大的个小任务,然后分别用goroutine去执行,使用channel来进行数据通信,这样就实现了并发。如果拆分得好,我们可以的得到n倍的一个执行效率。至于并行和并发,就交给runtime去管理吧。

当多个goroutine执行时,要注意在临界区我们要使用锁来保证代码的正确执行,这样才能保证线程安全

参考

2017年终总结

马上17年和18年就快交接了,这个时间点,还略有意义,所以记一篇流水账,总结过去,展望未来。

去年的计划

记得年初给自己定了两个小目标,第一是学英文,第二是健身。现在来看。。。

学英文的话,是啃了两本英文书,坚持了半年每天晚上背单词,但几乎完全没有成效。其实我分析,原因也很简单,所谓学以致用,只是做了“学”的工作,太脱离实际了,所以很投入产出比很小。试想一下,如果能去美帝,游学个一两年,顺便感受下资本主义的腐朽气息(真想闻闻),不会说就没饭吃,那学不会都难吧。

健身,,,身材是保持得很不错的,腹肌没有明显变化,目测还是只有一块,增加10斤体重的计划也没成功。这个我分析想应该是基因问题,因为有跟朋友对比,光看食物的摄入量,包括质量和卡路里,我都比我的朋友要多,但真正比起长肉,那就不行了。但是我也不气馁,明年继续要健身,也许找个健身教练科学的练一把会更好。

工作

17年工作最大的改变就是转岗,从web后端转到了sre,sre其实是个很新的概念,叫做网站可靠性工程师,是由google提出的,sre对工程师的能力提出了很高的要求,包括算法,数据结构,编程能力,测试,架构,故障运维等软件开发的各个部分都要一定的掌握甚至是精通。国内可以说能达到这个高度的工程师没几个,大多数名义上是sre,但其实干的顶多就是devops的活儿吧。这样一说,,我感觉像是搬起石头砸了自己的脚,不过现状确实就是这样,咱得说实话对不。

但我们也没闲着,尽量的将一些好的理论实践落地了。比如我也参与开发的监控报警系统,基于prometheus构建,并提供了lua,php,golang,c/c++的接入SDK,对使用方很友好。目前覆盖了我们搜索大部分的业务线,同事们纷纷点赞,用了都说好。

转岗的好处是显而易见的,我给自己的定位是新人,新的岗位确实是能接触到更多的东西,收获也不少。当时之所以要转岗,是因为对重复的业务工作感到身心俱疲,感觉自身技术上的提升也有限。不过开始遇到这问题,我第一个念头是要离职,后来跟领导谈了之后,他建议我转岗,也提了很多建议。我后来也想,如果离职去新的公司,薪水是会提高,但能确定去了之后就做的不是同样的重复性劳动吗?很可能是!因为新公司最需要的是你去完成你最熟悉的工作。没有理由,也没有时间给一个新的岗位让你去练手(大神请忽略这句话lol)。所以,留在公司去新的岗位,是一个机会,也是正确的选择。

买房

买房,2017终于办了。户口,房子两件事儿,都在一周之内搞定,我自己都没完全想到会这么快。其实去年就想上车,结果因为各种事儿耽误了(主要是拖延),没有落实。今年倒好,房价翻番,就问你上不上车?周围很多声音跟你说,“再等等”,“年底房价会降”,“别做接盘侠”,“有点钱可以自己投资啊”。。。然后我自己也开始犹豫。

最后决定要买,不是因为想通了什么大道理,也不是预测到了房价的走势,只是抱着试试看的心态去看房,恰好又看到了一套自己喜欢,价格也能承受的房子。并且这套房子刚好是房东前一天刚挂出来,在链家网上连照片都没有,第二天就跟房主见了面,交了定金,一切都很顺利,并没有想象中的那么复杂。也没啥特别的想法,虽然一下子买了这么大一坨奢侈品,但它终究只是一个商品。

房主人很不错,是一个机关单位的退休干部,家里有两套房子,这套本来是要留给女儿的,结果女儿在国外定居不回来了,就打算卖掉这套,换成钱跟老伴儿环游世界去。他跟我爸聊得特别投机,最后还主动少了我们2万块钱。我朋友就没这么好运了,遇到个坑货房东,临时涨价,延迟交房,延迟下户口,有些事儿到现在都还没处理清楚。所以有时候真是机缘巧合。

另外是链家的服务确实很好,选房,贷款,交房,每个流程都都很有效率,也很专业。两个点的中介费是高了点,不过也值了。

现在想起来,当初选房的时候也许是有点冲动了,但我并不后悔,要对国家,对城市未来的发展有信心。

番外

工作之外

财务自由不是说你有非得有个几千万,上亿的资产,而是说,工作之外的收入也能养活你,并且活的不错。

17年,我开始尝试股票投资,抱的是玩玩,也有学习心态。上半年,入了福耀玻璃,信立泰,双汇发展几只股票,运气好赶上了白马股行情,小赚了20个点,但下半年出师不利,入了三峡水利和中国联通两个坑货,基本上又吐了回去。要说经验的话,就是如果你看好一支股票,那就长期持有,投机心态别太重,迟早会有回报的。如果我年中没有换股,那现在的收入应该是50个点了。看到一个统计是,中国股市25年的年化收益是16%,明显是高于银行存款和各种理财产品,那为什么还有这么多人亏损,甚至倾家荡产?投机心太重可以解释这个问题吗?涨了点之后就立马卖掉,跌了之后就开始骂,心里想的是涨回去再出手,但它就在骂声中偏偏一直跌,跌倒你把持不住,卖掉了,然后它就开始涨了,然后又被骂,所以不管股票涨还是跌,骂娘的人都不少。看雪球上的评论就能明显感受到这种风气。

另外是比特币,现在想起来真是,感觉错过了一个亿!早在16年的时候我就开始关注,那个时候价格还是2k人民币一个,但是嫌okcoin不支持支付宝,微信支付,只支持银行转账,觉得麻烦就没买。后来刷刷的涨到6k,这个时候买了点,但价格一直在6k-8k之间徘徊了很久,于是在7k的时候出手了。再往后价格就真的起飞了,今年上半年最高涨到了3w,这时国家关停了几家比特币交易平台,几个小时之内价格跌倒了1.8w,,这时所有人觉得比特币泡沫就要被刺破了,因为据统计,80%的比特币交易发生在中国。然而,往往大多数人都是错的,这时比特比开启了新一轮的暴涨,最高涨到了13w,目前在9w左右徘徊。现在当然是不敢入坑了,也没有可投入的本金,但我比较看好比特币的未来。

除了炒股和比特币,我还开始讲课了,跟慕课网合作,准备出一门关于golang的编程课,目前正在录制中,预计年前应该可以上线。其实技术人有一个通病,就是不爱沟通,或者说沟通能力不行。很明显,我就是这样,很多事情,你以为你说清楚了,但其实并没有,如何将复杂的问题说得很简单,或者将简单的问题说复杂,都是一门很深的学问,所以,我觉得录课这事儿值得做,并且要尽力好好做。

装备推荐

17年入手了很多装备,下面是可以推荐的列表

  • Apple Magic键盘
  • 罗技MX anywhere2鼠标(好用得不行)
  • 一批技术书籍
  • 一只猫以及配套设施

强烈推荐你也入手一只猫,这样的话,你就可以享受到以下生活之乐事,文末有图。

  • 每天早晚铲屎两次,感受猫屎的芬芳气息
  • 凌晨3点,温柔的踩踏提醒你该起床了
  • 发情的浪叫声让你欲罢不能
  • 偶尔还可以跟猫爪来一次亲密接触
  • 等等等

错事

太多了,说不完,发现有很多事情,道理都明白,但只有你亲身经历了,体会才更深刻,道理才真正明白。

计划

计划很简单,少说多做

  • 多看几本书
  • 多写几篇blog
  • 多去外边看看

猫片

cat

关于 Golang 中的锁

为什么需要锁

在进行并发编程的时候,通过共享内存的方式进行通信,这时可能就会导致资源的竞争,最终导致出现数据错误。

比如下面这段demo程序,两个goroutine同时运行,循环执行a自增1的操作100000次,正常情况下,a的最终值我们的期望是200000,但结果不是!输出的值会比200000小,这是因为两个goroutine通过共享内存(也就是变量a)的方式进行通信,当goroutine1取到a准备做自增,比如这时a的值是10,这时goroutine2刚好完成了一次自增(a变成11),但goroutine1并不能感知到goroutine2中的a值的变化。所以goroutine1自增完,a的值还是11。导致最终结果“错误”。我们要避免这种情况的话,就需要使用锁。

a := 0
// goroutine1
go func() {
   for i := 0; i < 100000; i++ {
      a += 1
   }
}()
// goroutine2
go func() {
   for i := 0; i < 100000; i++ {
      a += 1
   }
}()
time.Sleep(time.Second) // 等待goroutine执行完成
fmt.Println(a)

Golang 中是如何实现锁的

Golang 中有两种锁,分别是互斥锁(sync.Mutex)和读写锁(sync.RWMutex),我们分别来看。

互斥锁(sync.Mutex)

互斥锁比较简单,当我们对一个goroutine上了互斥锁之后,其他的goroutine就只能乖乖的等待这把锁解锁,使用方法如下:

var mutex sync.Mutex
mutex.Lock() // 加锁
// ...
mutex.UnLock() // 解锁

我们要解决上面demo的问题,就可以使用互斥锁,代码如下,这时输出结果就正确了。

var mutex sync.Mutex
a := 0
// goroutine1
go func() {
   for i := 0; i < 100000; i++ {
      mutex.Lock() // 上互斥锁
      a += 1
      mutex.Unlock() // 解互斥锁
   }
}()
// goroutine2
go func() {
   for i := 0; i < 100000; i++ {
      mutex.Lock()
      a += 1
      mutex.Unlock()
   }
}()
time.Sleep(time.Second)
fmt.Println(a)

读写锁(sync.RWMutex)

通常情况下,互斥锁能满足很多的应用场景了,不过,互斥锁比较耗费资源,会拖慢程序的执行效率。在一些读取操作大大的多于写入操作的场景下,互斥锁就不太适合了,这时我们就需要读写锁。读写锁的使用方式如下:

var rwmutex sync.RWMutex
rwmutex.Lock() // 加写锁
rwmutex.Unlock() // 解写锁
rwmutex.RLock() // 加读锁
rwmutex.RUnlock() // 解读锁

看如下的demo,我模拟了读多写少的场景,mutex函数使用互斥锁,rwmutex函数使用读写锁,其他的读写操作都一样。最终程序的输出了两个函数的执行时间:

mutex time: 109.784714ms
rwmutex time: 36.494956ms

可以看出读多写少的场景下,读写锁比互斥锁的效率更高。

package main

import (
   "fmt"
   "sync"
   "time"
)

func main() {
   mutex()
   rwmutex()
}

// 使用互斥锁
func mutex() {
   var mutex sync.Mutex
   var wg sync.WaitGroup

   bt := time.Now()
   a := 0
   b := 0
   wg.Add(9)
   // 读操作
   for i := 1; i < 10; i++ {
      go func() {
         for i := 0; i < 100000; i++ {
            mutex.Lock()
            b = a
            mutex.Unlock()
         }
         wg.Done()
      }()
   }

   wg.Add(1)
   // 写操作
   go func() {
      for i := 0; i < 100; i++ {
         mutex.Lock()
         a += 1
         mutex.Unlock()
      }
      wg.Done()
   }()

   wg.Wait()
   et := time.Now().Sub(bt)
   fmt.Println("mutex time:", et.String())
}

// 使用读写锁
func rwmutex() {
   var rwmutex sync.RWMutex
   var wg sync.WaitGroup

   bt := time.Now()
   a := 0
   b := 0
   wg.Add(9)
   // 读操作
   for i := 1; i < 10; i++ {
      go func() {
         for i := 0; i < 100000; i++ {
            // 上读锁,多个goroutine可同时获取读锁,这里不会阻塞
            // 但读锁会阻塞其他写锁,直到读锁释放
            rwmutex.RLock()
            b = a
            rwmutex.RUnlock()
         }
         wg.Done()
      }()
   }

   wg.Add(1)
   // 写操作
   go func() {
      for i := 0; i < 100; i++ {
         // 上写锁,会阻塞其他的读锁和写锁,直到写锁释放
         rwmutex.Lock()
         a += 1
         rwmutex.Unlock()
      }
      wg.Done()
   }()

   wg.Wait()
   et := time.Now().Sub(bt)
   fmt.Println("rwmutex time:", et.String())
}

参考

https://golang.org/pkg/sync/
http://legendtkl.com/2016/10/26/rwmutex/

Hadoop streaming 排序工具初探

通常使用 KeyFieldBasePartitionerKeyFieldBaseComparator 来对用户输出key进行排序和分桶。

基本概念

Partition:分桶,用户输出的key经过partition分发到不同的reduce里,因而partitioner就是分桶器,一般用平台默认的hash分桶,也可以自己指定。

Key:是需要排序的字段,相同Partition && 相同key的行排序到一起。

bin/hadoop streaming \
-input /tmp/comp-test.txt \
-output /tmp/xx -mapper cat -reducer cat \
-jobconf stream.num.map.output.key.fields=2 \
-jobconf stream.map.output.field.separator=. \
-jobconf mapred.reduce.tasks=5

上面案例中map输出中会按.分割,以前两列作为key,相同的key会分到同一个reduce中。

stream.num.map.output.key.fields 设置map输出的前几个字段作为key
stream.map.output.field.separator 设置map输出的字段分隔符

KeyFieldBasePartitioner

该配置主要用于分桶

map.output.key.field.separator 设置key内的字段分隔符
num.key.fields.for.partition 设置key内前几个字段用来做partition
mapred.text.key.partitioner.options 设置key内某个字段或者某个字段范围用做partition(优先级比num.key.fields.for.partition低)

KeyFieldBaseComparator

该配置主要用于排序

mapred.text.key.comparator.options 设置key中需要比较的字段或字节范围

hadoop-skeleton

最近工作中跑mapreduce的统计需求越来越多,而多个任务之间缺乏有效的组织管理,导致的结果就是目录结构散乱,大量的代码重复。所以我简单封装了下 hadoop streaming 任务流程,只需要做一些简单的配置即可跑一个新的任务。

项目地址:https://github.com/itsmikej/hadoop-skeleton

参考

http://www.dreamingfish123.info/?p=1102

<Head First C> 读书笔记

最近在重新学习 C 语言,本文是 Head First C 这本书里关于 C 的一些基础知识的总结笔记。

内存存储结构

  • :存储函数创建的变量值(局部变量)
  • :动态存储分配
  • 全局量区:存储函数定义的变量值(全局变量)
  • 常量区:存储只读数据
  • 代码段:存储代码段

指针

数组与指针

  • 数组变量可以被用作指针
  • 数组变量指向数组中的第一个元素
  • 如果把函数参数声明为数组,它会被当称指针处理
  • 指针变量也是变量。。保存的数字,也可以通过 & 获取它的地址

- 阅读剩余部分 -