在 Go 语言中,并发操作一个 map 需要采取一定的措施来确保并发安全。因为 map 在并发读写时会引发数据竞争,可能导致程序运行不正确或崩溃。
下面介绍两种常见的在并发环境下安全地操作 map 的方式:
下面是使用互斥锁来并发安全地操作 map 的示例:
import (
"sync"
)
var mu sync.Mutex
var m = make(map[string]int)
func writeToMap(key string, value int) {
mu.Lock()
defer mu.Unlock()
m[key] = value
}
func readFromMap(key string) int {
mu.Lock()
defer mu.Unlock()
return m[key]
}
下面是使用 sync.Map 进行并发安全操作的示例:
import (
"sync"
)
var m sync.Map
func writeToMap(key string, value int) {
m.Store(key, value)
}
func readFromMap(key string) (int, bool) {
return m.Load(key)
}
Go中的chan
在 Go 语言中,chan(通道)是用于协程之间进行通信的一种重要机制。可以通过 chan 进行数据的发送和接收,实现协程之间的同步和数据交换。
创建 chan 可以使用 make() 函数进行初始化,指定通道元素的类型。例如:
go ch := make(chan int) // 创建一个 int 类型的通道 ch := make(chan string)
创建一个 string 类型的通道 chan 的发送操作使用 <- 运算符,接收操作也使用 <- 运算符。例如:
ch <- data // 将 data 发送到 ch 通道
data := <-ch // 从 ch通道接收数据,并将其赋值给 data
发送和接收操作都会阻塞当前协程,直到发送或接收操作完成为止。这使得协程之间可以在通道上进行同步,确保数据按照特定顺序进行发送和接收。
对于无缓冲通道(即没有指定缓冲区大小的通道),发送和接收操作是同步的。发送操作会在有接收方准备好之前阻塞,而接收操作会在有消息可供接收之前阻塞。
使用带缓冲的通道时(通过指定缓冲区大小来初始化通道),当通道中已有缓冲的数据时,即使没有接收操作,发送操作也可以继续进行。只有当缓冲区已满时,发送操作才会阻塞。同样地,当缓冲区为空时,接收操作才会阻塞。
type Conn interface {
// Read reads data from the connection.
// Read can be made to time out and return an error after a fixed
// time limit; see SetDeadline and SetReadDeadline.
Read(b []byte) (n int, err error)
// Write writes data to the connection.
// Write can be made to time out and return an error after a fixed
// time limit; see SetDeadline and SetWriteDeadline.
Write(b []byte) (n int, err error)
在Go语言中,net.Conn接口表示一个通用的网络连接,用于进行数据的读写操作。Read方法是net.Conn接口的一部分,用于从连接中读取数据。下面是对Read方法的解读:
func (c net.Conn) Read(b []byte) (n int, err error)
函数签名中,net.Conn表示net.Conn类型的对象,b是用于存储读取数据的缓冲区(一个字节数组),函数的返回值为读取到的字节数和可能出现的错误。具体行为如下:
如果b的长度为0,那么Read会立即返回0和nil,不会阻塞并且不会进行任何读取。
如果b的长度大于0,但小于io.Reader接口内部的缓冲区大小(默认为4096字节),则Read尝试一次性读取全部的数据,不需要多次调用。
如果b的长度超过缓冲区的大小,那么Read会多次调用直到将b填满或者遇到错误为止。这种情况下,Read可能会阻塞等待更多的数据可供读取。
如果连接的对端关闭了连接,则Read会返回0和一个io.EOF错误。
需要注意的是,在网络传输中,读取操作可能会阻塞,并且如果没有数据可读,Read可能会一直阻塞等待。为了避免阻塞,可以使用超时控制、并发协程等方式来处理。另外,读取到的字节数n与缓冲区的长度不一定相等,这是因为网络中的数据可能会分块传输,需要多次读取才能获取完整的数据。因此,需要根据返回值的实际情况进行判断和处理。
select {
case <-ch1:
// 当ch1通道有值可读时执行的代码
case data := <-ch2:
// 当ch2通道有值可读时执行的代码,并将值赋给data变量
case ch3 <- value:
// 将value的值发送到ch3通道中
default:
// 当没有任何通道操作时执行的代码
}
以下是一些select语句的用法例子:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
ch3 := make(chan int)
go func() {
time.Sleep(2 * time.Second)
ch1 <- 1
}()
go func() {
time.Sleep(3 * time.Second)
ch2 <- 2
}()
go func() {
time.Sleep(4 * time.Second)
ch3 <- 3
}()
select {
case <-ch1:
fmt.Println("Received from ch1")
case <-ch2:
fmt.Println("Received from ch2")
case <-ch3:
fmt.Println("Received from ch3")
case <-time.After(5 * time.Second):
fmt.Println("Timeout")
}
}
在上面的例子中,使用select同时等待三个通道操作。根据每个通道操作的延迟时间,可能会收到不同的结果。
请注意,在select语句中可以包含default分支,用于处理当没有任何通道操作可用时的情况。如果所有的通道都未准备好,且没有default分支,那么select语句将会阻塞,直到至少有一个通道准备好。使用select语句的default分支可以避免程序阻塞。
3. case <-time.After(time.Second * 10)
是 select 语句中的一个 case 分支,它使用了 time.After() 函数来创建一个定时器。
在这个例子中,time.After(time.Second * 10) 返回一个通道(channel),并在指定的时间间隔后向该通道发送一个值。通过将这个通道放在 select 语句中的 case 分支中,我们可以等待指定的时间后执行相应的代码。
以下是一个示例:
package main
import (
"fmt"
"time"
)
func main() {
select {
case <-time.After(time.Second * 10):
fmt.Println("Timeout")
}
}
在上面的示例中,我们使用 select 语句和 time.After() 创建了一个 10 秒的定时器。当 10 秒钟过去后,将会从 time.After() 返回的通道中接收到一个值,然后执行相应的代码,在这里是打印 “Timeout”。
这种用法常用于超时控制,例如在并发操作中设置一个操作的最大执行时间。如果在指定的时间内没有完成操作,就可以执行相应的超时逻辑。
主要思路:
conn,err := listener.Accept().
连接成功后,err==nill
,conn有值。具体来说:listener.Accept() 是一个阻塞函数,用于等待客户端连接并接受连接。它会一直阻塞当前 goroutine 直到有新的客户端连接到服务器,然后返回一个表示客户端连接的 net.Conn 类型对象。用户client的模拟可以通过nc命令 : 连接到远程主机的特定端口:nc <目标IP> <端口号> 如 nc 127.0.0.1 8888
连接到了server。此时作为客户端,自己的ip就是主机ip,如果没有明确指定源端口,操作系统会自动分配一个可用的本地端口。
流程:
简单说:server监听client,一旦有用户上线,开一个协程执行handler方法。handler就是先把上线的user存到map表中(或者存到数据库中),然后再给server自己的通道写一条当前这个用户上线的消息。由于之前开启了一个协程,定期的检查server的channel有没有东西,有东西就发给map表中每个在线的用户,当然,由于data := <-ch // 从 ch 通道接收数据
这个操作是阻塞的操作,因此通道为空的话会一直阻塞在这一行代码中。
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", this.Ip, this.Port))
监听自己的ip和端口有没有人连接go this.ListenMessager()
conn, err := listener.Accept()
this.conn.Write([]byte(msg + "\n"))
,外部流程:
this.Message <- 消息
Message是chan,是server的成员属性msg := -< range this.Message
,之后遍历map(使用range),拿到map中的value即为user对象之前的代码版本已经实现:只要serverChannel中一有消息,有一个GoRoutine 会马上将该消息broadCast给每个用户。现在需要将一个用户的消息进行广播,那只需要将该消息放到serverChannel中即可。而且根据连接的断开,net.Conn.Read() 会返回一个读到的字节数n,如果n==0,说明一个用户连接已经断开了,可以将该用户从列表中移除。
思路简单,有一个小问题就是会把消息发给每个用户,包括当前用户,这个应该也可以传入当前用户的相关信息进行改进。
func (this *User) DoMessage(msg string) {
if msg == "who" {
//查询当前在线用户都有哪些
this.server.mapLock.Lock()
for _, user := range this.server.OnlineMap {
onlineMsg := "[" + user.Addr + "]" + user.Name + ":" + "在线...\n"
this.SendMsg(onlineMsg)
}
this.server.mapLock.Unlock()
} else if len(msg) > 7 && msg[:7] == "rename|" {
//消息格式: rename|张三
newName := strings.Split(msg, "|")[1]
//判断name是否存在
_, ok := this.server.OnlineMap[newName]
if ok {
this.SendMsg("当前用户名被使用\n")
} else {
this.server.mapLock.Lock()
delete(this.server.OnlineMap, this.Name)
this.server.OnlineMap[newName] = this
this.server.mapLock.Unlock()
this.Name = newName
this.SendMsg("您已经更新用户名:" + this.Name + "\n")
}
} else {
this.server.BroadCast(this, msg)
}
}
//当前handler阻塞
for {
select {
// 当isLive通道有值可读时执行的代码
case <-isLive:
//当前用户是活跃的,应该重置定时器
//不做任何事情,为了激活select,更新下面的定时器
case <-time.After(time.Second * 10):
//已经超时
//将当前的User强制的关闭
user.SendMsg("你被踢了")
//销毁用的资源
close(user.C)
//关闭连接
conn.Close()
//退出当前Handler
return //runtime.Goexit()
}
}