我搭建了个人博客主页, 欢迎访问: http://blog.joelzho.com/
Go 的多线程和通道我感觉还是比较好玩的, 特别是 Channel.
Channel 真是多线程通讯的利器, 就像 C 中多进程通讯的 pipe 一样.
我这里以网易2015 的一道多线程面试题为例子, 用 GoLang
来实现.
其中涉及到的知识有:
一个文件中有10000个数, 用Java实现一个多线程程序将这个10000个数输出到5个不用文件中(不要求输出到每个文件中的数量相同).
需求:
从面试题中可以出, 我们首先需要将一个有10000个数的文件读到内存中来.
下面列出了 Go 语言中最简单的读文件方法
// 读取文件
// 读取成功返回字节数组
// 读取失败返回nil
func ReadBytesFromFile(fileName string) []byte {
// 打开文件
fs, err := os.Open(fileName) //os 是系统包
if err != nil {
fmt.Println("无法读取输入文件.")
return nil
}
// 读取文件的所有数据到内存中
bytes, err := ioutil.ReadAll(fs) // ioutil 是系统包
if err != nil {
fmt.Println("输入文件读取出错.")
}
// 关闭文件对象, 忽略关闭错误
err = fs.Close()
return bytes
}
现在, 我们假设某个线程已经收集到了 10 个偶数或者是奇数, 现在将它写入到文件中
func WriteNumber2File(outputFile string, numbers []int) {
//将int数组拼接, 成字符串, 以换行符分隔
var strBuf string
for n := range numbers {
strBuf += fmt.Sprintf("%d\n", n)
}
// 字符串转字节数组
bytes := []byte(strBuf)
//输出到文件
err := ioutil.WriteFile(outputFile, bytes, os.ModeAppend)
if err != nil {
fmt.Printf("不能写入文件: %s\n", err)
}
}
既然要每组线程中要分别拿到奇数10个和偶数10个,且最多5个线程一起拿某一类数.
所以有了以下数据结构(或者说是类):
type NumberContainer struct {
oddNumbers []int //奇数数组
evenNumbers []int //偶数数组
oddNumbersIndex int //奇数数组当前下标
evenNumbersIndex int //偶数数组当前下标
oddNumbersLock sync.Mutex //奇数数组锁
evenNumbersLock sync.Mutex //偶数数组锁
}
此处提供两个线程安全的方法给来获取数字的线程调用,
接口定义如下:
type NumberContainerMethods interface {
GetOddNumbers(count int) []int //获取 count 个奇数
GetEvenNumbers(count int) []int //获取 count 个偶数
}
func (numberContainer *NumberContainer) GetOddNumbers(count int) []int {
var result []int = nil
numberContainer.oddNumbersLock.Lock()
more := len(numberContainer.oddNumbers) - numberContainer.oddNumbersIndex
if more >= count {
endIndex := numberContainer.oddNumbersIndex + count
result = numberContainer.oddNumbers[numberContainer.oddNumbersIndex : endIndex]
numberContainer.oddNumbersIndex += count
} else if more > 0 {
count = more
endIndex := numberContainer.oddNumbersIndex + count
result = numberContainer.oddNumbers[numberContainer.oddNumbersIndex : endIndex]
numberContainer.oddNumbersIndex += count
}
numberContainer.oddNumbersLock.Unlock()
return result
}
func (numberContainer *NumberContainer) GetEvenNumbers(count int) []int {
var result []int = nil
numberContainer.evenNumbersLock.Lock()
more := len(numberContainer.evenNumbers) - numberContainer.evenNumbersIndex
if more >= count {
endIndex := numberContainer.evenNumbersIndex + count
result = numberContainer.evenNumbers[numberContainer.evenNumbersIndex : endIndex]
numberContainer.evenNumbersIndex += count
} else if more > 0 {
count = more
endIndex := numberContainer.evenNumbersIndex + count
result = numberContainer.evenNumbers[numberContainer.evenNumbersIndex : endIndex]
numberContainer.evenNumbersIndex += count
}
numberContainer.evenNumbersLock.Unlock()
return result
}
注意
: 接口实现的数据结构一定要传 指针进来,
因为 Go 语言中的接口是值类型, 不是引用类型.
如果不是指针, 在方法内部的改动不会应用到结构内部.
这个 C++ 的对象差不多.
前面我们已经将文本读取到内存中, 它是一个 byte 数组.
现在我们需要将它转成文本, 然后封装成 NumberContainer
结构, 供多线程使用,
假设面试中提到的文本是以换行符分隔的, 那么有如下代码:
const inputFileName = "/Users/joel/Desktop/input.txt"
bytes := ReadBytesFromFile(inputFileName) //读取文件成byte 数组
if bytes == nil {
return
}
fileContent := string(bytes) //字节数组转字符串
lines := strings.Split(fileContent, "\n") //字符串分隔
oddNumbers, evenNumbers := GroupBy(lines)
var oddNumbersRdLock sync.Mutex
var evenNumbersRdLock sync.Mutex
// Go语言结构的初始化和 C++ 11 的语法很像.
// 按照成员的定义顺序, 或者像 swift 语言那样指定要赋值的成员.
numberContainer := NumberContainer {
oddNumbers,
evenNumbers,
0,
0,
oddNumbersRdLock,
evenNumbersRdLock,
}
其中 GroupBy
函数如下:
// 奇偶分组
func GroupBy(lines []string) (oddNumbers []int, evenNumbers []int) {
for line := range lines { //foreach 循环
num := int(line)
if num % 2 == 0 {
evenNumbers = append(evenNumbers, num)
} else {
oddNumbers = append(oddNumbers, num)
}
}
return oddNumbers, evenNumbers
}
通道, 形象化来说就像是我们生活中的水管, 要源头注入水, 末端才能喝到水.
如果你会Socket 编程的话, 那么这就更好理解什么是 channel
了.
在 Socket 编程中, 服务端启动一个 FD 进行阻塞监听, 客户端向socket FD写入一个 4 个字节的 int,
服务端按照顺序读出4个字节, 然后继续阻塞等待数组.
channel
也一样, 也需要有人充当客户端的角色, 需要有人充当服务端的角色.
//声名通道的语法语法
// chan 是关键字
var varible_name chan channel_type
哈哈, 很少见声名一个类型需要四部分的语言吧 ?
常用
语法如下://构建一个chan 实例
//make 是一个系统函数
var myChan chan int = make(chan int)
//常用简写
myChan := make(chan int)
也没见过函数里面填写 类似于两个参数
还不要逗号分隔的吧?
创建只读通道
myChan := make(chan <-int)
这语法看起来怪怪的…
这样看来 chain int
其实合起来是一个类型, 意思就类似于 Java 中的泛型.
如果要在Java 自己实现通道的话, 声名差不多是 : Channel
创建只写通道
myChan := make(<- chan int)
嗯… 这语法看起来更怪异.
上面我们提到了如何创建仅可读
或者仅可写
的通道(可读又可写的在6.2小节).
其实, 创建只有一个方向的通道没啥意义, 我写进去每人读, 我这通道有何用?
但是, 对于形参来说这是很重要的.
例如某个线程或者说某个方法, 它只需要对某个通道进行写入, 为了告诉调用者: 我在这个方法只写, 绝对不读它.
那么这个函数的声名应该如下:
//此函数接收一个int类型的只写通道
func MyFunc(wChan chan <- int)
同样的, 你应该会在形参上声名需要一个只写通道.
对于通道的读和写, 无非就是参数方向不一样, 看如下代码:
//声名string 类型的通道
myChan := make(chain string)
//将字符串写入到通道中
myChan <- "HelloWorld!"
//从通道中读取字符串
str := <- myChan
上面我们提到过用 Socket 来理解通道是最简单的,
其实 通道也是阻塞的,
如果你向一个没有写入任何数据的通道进行读取的话, 当前线程会阻塞,直到读到数据或报错为止.
就像 6.5
的示例, 如果把第三行代码与第二行换一个顺序, 会导致程序卡死.
我们知道socket 编程中, 是有内核缓冲区大小的,
如果客户端缓冲区满了, 操作系统会发送ACK信号告诉服务端慢点发消息, 我处理不过来了!
如果服务端继续发送数据的话就会堵塞(如果FD 设置了非阻塞,函数直接返回, 实际发送长度小于数据长度.)
Go 语言的通道也是有缓冲区的, 默认大小只有一个,
也就是说, 如果已经写入一个数据之后再写一次就会等待之前写入的数据被读出之后才能返回.
如何设置默认缓冲区:
//make的第二个参数指定缓冲区大小
myChan := make(chan int, 100)
上面我们已经讲了如何使用通道, 那么接下来我们需要继续完成面试题.
从面试题中得到, 我们的线程需要以下参数:
那么集合实际的代码, 我们的工作线程函数定义为如下
func WorkThread (
outputFile string, //输出文件
outChan chan <-int, // 线程退出通信通道
threadChan chan int, // 线程之间通信通道
threadNum int, // 线程编号
isOddWorker bool, //是否是奇数输出线程
numContainer *NumberContainer,
)
提示一下, goroutine 并不是真线程(自行百度), 不过我很乐意叫它线程.
就像Linux 下的线程也不是真线程(自行百度), 我们还是叫它线程.
Go 语言中启动线程和常见的语言不一样, 它是用一个关键字来完成的,
而这个关键字正是: go
语法如下:
//启动一个线程,这个线程没有参数
go functionName()
//启动一个需要参数的线程
//函数:
func functionName(a int)
//启动
var a int = 10
go functionName(a)
嗯… 这里涉及到一个问题, 我想没人想问: 一个有返回值的函数可以做线程函数吗?
其实是可以的, 不过这返回值… 我觉得你拿不到,我没深究这个问题.
最后, 我将此面试题的主函数里面的内容, 由于这篇博客已经很长了, 其他代码可以到我的 github 中查看.
当然, 我的代码是否符合网易出这道面试题的初衷就不得而知了, 哈哈~~.
func main() {
const inputFileName = "/Users/joel/Desktop/input.txt"
bytes := ReadBytesFromFile(inputFileName)
if bytes == nil {
return
}
fileContent := string(bytes)
lines := strings.Split(fileContent, "\n")
oddNumbers, evenNumbers := GroupBy(lines)
var oddNumbersRdLock sync.Mutex
var evenNumbersRdLock sync.Mutex
numberContainer := NumberContainer {
oddNumbers,
evenNumbers,
0,
0,
oddNumbersRdLock,
evenNumbersRdLock,
}
const threadCount = 10
//线程退出通讯通道
threadExitChan := make(chan int, threadCount)
const outputFilePre = "/Users/joel/Desktop/output/thread_grp_%d.txt"
for i := 0; i < threadCount; i += 2 {
//指定线程组的输出文件
outputFile := fmt.Sprintf(outputFilePre, i)
//创建当前线程工作的通讯通道
//写入 1 表示奇数线程工作
//写入 2 表示偶数线程工作
//写入 3 表示偶数线程退出
//写入 4 表示奇数线程退出
threadWorkChan := make(chan int, 1)
//偶数输出线程
go WorkThread(outputFile, threadExitChan, threadWorkChan, i, false, &numberContainer)
//奇数输出线程
go WorkThread(outputFile, threadExitChan, threadWorkChan, i + 1, true, &numberContainer)
//先让偶数线程开始工作
threadWorkChan <- 2
}
for j := 0; j < threadCount; j++ {
t := <- threadExitChan
fmt.Printf("thread %d exit\n", t)
}
fmt.Println("Done")
}
github 链接: https://github.com/joelcho/learn/tree/master/letsgo/02