Go 语言新手笔记(五)

终于来到的 Go 的网络编程了,来写一个 TCP 服务端与客户端的程序。要用到 Go 语言的  net 包,是一个标准的 Listen+Accept 结构, 下面是一个简单的 TCP Server/Client 端的例子,启动了 Server 端口,可以用 telnet 去连接,也可以用 client.go 来连接

server.go

go run server.go

启动后用 telnet localhost 9000 连接

或者用下面的 client.go 客户端来连接

在 server.go 代码中用到的协程,这是学习 Go 时跳过的内容,现在再返回去了解一下 Go 是怎么作并发处理的。

Go 使用协和,比线程还更轻量级,Go 的协程(goroutine) 是 Go 提供的一种用户态线程。协程由应用程序创建和管理,因此开销低(一般为 4KB)。系统线程与 goroutine 之前可以是 1:1, 1:n, 或 m:n。runtime  几个相关的函数

  1. runtime.NumCPU()                   // 当前 CPU 内核数
  2. runtime.GOMAXPROCS(2)    // 设置运行时最大可执行  CPU 数,默认与 CPU 内核数相同
  3. runtime.NumGoroutine()       // 当前正在运行的 goroutine 数

Go 启动一个 goroutine  非常容易,只要 go function 就行了,循着 Java 中怎么开启线程的方式来试试 Go 的协程

执行输出:

dispatched all tasks
slept 185 ms, done routine: 0
slept 242 ms, done routine: 1
slept 285 ms, done routine: 2
slept 344 ms, done routine: 4
slept 621 ms, done routine: 3

通道(channel) 用于进程内不同协程之间进行通信,而非用共享内存,这样能解决数据同步的问题,通道可以带缓冲或不带缓冲(默认不带缓冲),通道有三种,发送的,接收的,和同时发送和接收的。通道用 make() 创建,创建通道要给定通道传递的数据类型

recvChan := make(<-chan int)     // 接收通道,数据要从通道中读出, <-chan int, int 数据从通道左边出来
sendChan := make(chan<- int)    //  发送通道, 数据要写入通道,所以为 chan<- int,int 数据从右边放入通道
sendRecvChan := make(chan int)  // 不指定方向,则为可同时发送和接收的通道,一般用这个

上面创建的都是没有缓冲的通道,来测试一下用发送接收通道传递数据的效果

执行效果

send ready 0
sent 0
send ready 1
received 0
received 1
sent 1
send ready 2
sent 2
send ready 3
received 2
received 3
sent 3
send ready 4
sent 4
received 4

送一个,收一个,如果把上面的第四行 go recv(c) 注释掉,再跑一遍,只打印一行

send ready 0

也就是一个数据都无法发送,无缓冲的队列必须有接收方在线才能传递数据。这和 Java 长度为 0 的同步队列 SynchronousQueue 效果上是一样的。看 Java 中 SynchronousQueue 的用法

执行后效果为

Send ready: 1f125ab8-3a2e-48b1-9d60-08cb2f8fb058
Sent: 1f125ab8-3a2e-48b1-9d60-08cb2f8fb058
Take: 1f125ab8-3a2e-48b1-9d60-08cb2f8fb058
Send ready: e9596d7e-d494-4a29-920c-906ed48318ee
Take: e9596d7e-d494-4a29-920c-906ed48318ee
Sent: e9596d7e-d494-4a29-920c-906ed48318ee
Send ready: 5ad8870a-e561-4456-848c-bf718d4ec30b
Take: 5ad8870a-e561-4456-848c-bf718d4ec30b
Sent: 5ad8870a-e561-4456-848c-bf718d4ec30b
Send ready: e7a24bee-5684-4486-b900-b94b6374cc16
Take: e7a24bee-5684-4486-b900-b94b6374cc16
Sent: e7a24bee-5684-4486-b900-b94b6374cc16

如果同样把 new Thread(new Consumer(synchronousQueue)).start() 注释掉,不起动 Consumer,执行后效果如下

Send ready: 39511b79-9d3b-4f27-8fef-07e3d6e95948

也是一条数据也发送不出去,没有接收方在线什么事也做不了。简单测试就是

new SynchronousQueue<String>().offer("a");     //永远返回 false, 因为队列长度为 0

返回到前 Go 的代码,如果换成一个带缓冲的通道,即创建通道 c 时用

c := make(chan int, 4)    //缓冲区大小为 4

go send(c)go recv(c) 全开的情况,输出

send ready 0
sent 0
send ready 1
sent 1
send ready 2
sent 2
send ready 3
sent 3
send ready 4
sent 4
received 0
received 1
received 2
received 3
received 4

可以连续发送和接收

再把  go recv(c) 注释掉,只发送而不接收的情况,输出

send ready 0
sent 0
send ready 1
sent 1
send ready 2
sent 2
send ready 3
sent 3
send ready 4

缓冲满后就不能再发送数据了,这里的缓冲区大小为 4,第 5 个数据放不进去了。

通道关闭后不能再发送数据,再已在通道中的数据还能被接收。

协程间访问共享资源时用锁的方式

同时只有一个协程能拿到锁。

还有一个读写锁 sync.RWMutex, 用于读多写少的情况下,达到对读的做化,它有下列方法

func (*RWMutex) Lock
func (*RWMutex) Unlock
func (*RWMutex) RLock
func (*RWMutex) RUnlock

sync.WaitGroup 可用来等待所有的协程完成,它的功能和 Java 的 CountDownLatch 是一样的

如果没有 wg.Wait() 则主线程会立即退出导致程序的结果,协程都得不到执行。上面代码执行效果如下

done task: 4
done task: 3
done task: 1
done task: 0
done task: 2
exit

sync.Once.Do(f func()) 保证同一个 sync.Once 变量的  Do() 只会执行一次,不其中的函数是否变换了

Go 1.9 新增了同步的字典 sync.Map,在 sync/atomic 包还定义了更多的原子级操作,与锁不同的是这里边的原子操作是由底层硬件支持的,原子操作效率相比系统提供 API 实现的锁更高。

本文链接 https://yanbin.blog/go-language-notes-5/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments