终于来到的 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 几个相关的函数
- runtime.NumCPU() // 当前 CPU 内核数
- runtime.GOMAXPROCS(2) // 设置运行时最大可执行 CPU 数,默认与 CPU 内核数相同
- 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
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。