内容简介:大家好,我是一名 Go 语言工程师。我平日里也在教课。前几天给了一节关于 Go 语言数据并行的讲座。我在这里整理成文。希望大家喜欢。在开发中,我们经常会将一个某种类型 T 的序列转化为类型 T2 的序列。最原始的方法就是使用 for loop。不过,for loop 不仅拗口,而且如果要做到良好的异步或者并发处理,就要写很多重复的代码。所以,在各个编程语言中,标准库都提供了接口稳定的
大家好,我是一名 Go 语言工程师。我平日里也在教课。前几天给了一节关于 Go 语言数据并行的讲座。我在这里整理成文。希望大家喜欢。
从 Map、Reduce 等函数式编程讲起
在开发中,我们经常会将一个某种类型 T 的序列转化为类型 T2 的序列。最原始的方法就是使用 for loop。不过,for loop 不仅拗口,而且如果要做到良好的异步或者并发处理,就要写很多重复的代码。
所以,在各个编程语言中,标准库都提供了接口稳定的 map, reduce, filter
等函数。即使连 JavaScript 社区也开发了 RxJS 这样的函数式编程类库。
然而,Go 语言在标准可层面并没有这样的类库。作为上层业务逻辑开发者,这是不太方便的。所以社区内也有人尝试写出好的 Map、Reduce、Filter 等类库。一个最大的动力和需求就源于 Go 语言极度擅长并发编程。而 Map、Filter、Fold这三个函数是完全可以数据并行的(Reduce 不太行,我们之后会讲)。在业界用 Go 语言来做 Data Pipeline 其实也是很好的方案。
不过,写出一个好的 Map、Reduce 类库是需要考虑很多设计问题的。本文旨在给大家详尽并且深入地讲解这个问题。所有地代码都可以在 github.com/CreatCodeBu… 的数据并行(data concurrency)目录下找到。
如何写一个 Map
让我们从最基本的 for loop 开始
func Map(data []int, mapper func(int) int) []int { results := make([]int, len(data)) for i, ele := range data { results[i] = mapper(ele) } return results } 复制代码
这是一个最简单的 Map 实现。将一个 []int Map 到另外一个 []int 中。用法就是
func TestMap(t *testing.T) { results := Map([]int{1, 2, 3}, func(x int) int { return x + 1 }) require.Equal(t, []int{2, 3, 4}, results) } 复制代码
如你所见, 1, 2, 3
变成了 2, 3, 4
, 因为我们的 lambda 为 x + 1
。
然而,这里这个实现有两个问题。
第一,我们定死了原始类型和目标类型。如果我们想从 int Map 到 string,就要新写一个函数。
Go 没有泛型,这是造成这个原因的其中一点。不过,这只是开发时中的问题。比如 C++ 有基于代码生成模板的泛型,那么其实是在编译时生成更多的、属于不同类型的代码。所以,不管你是手写、还是编译器生成,在运行时代码都是一样多的。当然,如果像 Java 那样,通过运行时类型检查来实现泛型、就是另外一回事了。
所以,我认为这顶多叫做麻烦,而不是问题。
第二,更大的问题来自运行时
这个实现最大的问题,就是定死了原始数据序列和目标数据序列的内存模型。为什么必须要是 Slice 这种数据结构呢?Slice 所带来的一个副作用就是,数据最终会用 Array 存起来。而 Array 是连续的内存。然而,Map 这个函数从逻辑上根本没有要求数据要在物理上连续。Map 甚至都没有要求数据在逻辑上是连续的。
我为甚么不能从一个 Slice 映射到一个队列呢?为什么不能从一个 channel 映射到一个文件流呢?Map 的本质就是抽象的序列(数据流)之间的映射。所以,我们的实现应该表现出这一点,并且同时不要带来内存连续这样的副作用。我不是说内存连续不好,而是说不必要。 优秀的设计反应事物的本质。优秀的实现没有不必要的东西。
一个更好的 Map
先解决第二个问题
// producer 是一个数据生产者。Next 会迭代并返回序列中的下一个元素。 // 返回 io.EOF 表示穷尽了序列。其他错误值表示 producer 本身遇到了错误。 type producer interface { Next() (string, error) } // consumer 是数据消费者。Send 会读入新的数据。 type consumer interface { Send(int64) } // 返回错误如果 string 不能表示 int。比如 "xxx" 不是一个正确的 int 表示形式。 type mapper func(string) (int64, error) func BetterMap(p producer, c consumer, mapper mapper) error { for { next, err := p.Next() if err != nil { if err == io.EOF { break } else { return err // 生产者本身遇到错误,终止 Map。 } } datum, err := mapper(next) if err != nil { return err // mapper 出了问题,终止 Map。 } c.Send(datum) } return nil } type StringProducer struct { index int data []string } func (ip *StringProducer) Next() (string, error) { if ip.index < len(ip.data) { defer func() { ip.index++ }() return ip.data[ip.index], nil } return "", io.EOF } type OutputConsumer struct{} func (c OutputConsumer) Send(ele int64) { fmt.Println(ele) } 复制代码
我们完成了一个很大的提升,将数据的生产者和消费者的具体实现交给了 Map 的调用者,而不是 Map 自己来定义。Map 只定义 2 个大家都同意的接口。
用起来也非常方便
func ExampleBetterMap() { BetterMap(&StringProducer{data: []string{"1", "10", "11"}}, OutputConsumer{}, func(str string) (int64, error) { // 这里的 lambda 将字符串以二进制形式转为整数 return strconv.ParseInt(str, 2, 64) }) // Output: 1 // 2 // 3 } 复制代码
如你所见,我们可以随意地使用我们自己的实现。 consumer
甚至可以将结果 IO 出去,而不是存在内存里。这样 Map 函数就没有影响程序的内存效率。调用者代码和 Map 自己的权责分明了。同样的道理, producer
也可以将数据从其他源流读进来,而不是一次性地全部存在自己内部。
以上所述就是小编给大家介绍的《Go语言:完全深入理解数据并行和函数式 Map 1》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 一个函数搞定并行,为Pandas提速!为我Kaggle摘银
- sqltoy-orm-4.17.6 发版,支持 Greenplum、并行查询可设置并行数量
- PostgreSQL并行查询介绍
- nodejs“并行”处理尝试
- 并行python迭代
- Golang 多核并行
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
分布式服务架构:原理、设计与实战
李艳鹏、杨彪 / 电子工业出版社 / 2017-8 / 89.00
《分布式服务架构:原理、设计与实战》全面介绍了分布式服务架构的原理与设计,并结合作者在实施微服务架构过程中的实践经验,总结了保障线上服务健康、可靠的最佳方案,是一本架构级、实战型的重量级著作。 《分布式服务架构:原理、设计与实战》以分布式服务架构的设计与实现为主线,由浅入深地介绍了分布式服务架构的方方面面,主要包括理论和实践两部分。理论上,首先介绍了服务架构的背景,以及从服务化架构到微服务架......一起来看看 《分布式服务架构:原理、设计与实战》 这本书的介绍吧!