Go语言中的锁与管道的运用

news2025/1/16 20:06:05

目录

1.前言

2.锁解决方案

3.管道解决方案

4.总结


1.前言

在写H5小游戏的时候,由于需要对多个WebSocket连接进行增、删、查的管理对已经建立连接的WebSocket通过服务端进行游戏数据交换的需求。于是定义了一个全局的map集合进行连接的管理,让所有的协程共享操作同一个map集合,进行各种WebSocket连接的操作。由于多个协程操作共享同一块内容,这时候就会遇到数据竞争和并发访问。

H5小游戏介绍:基于WebSocket通信的H5小游戏总结-CSDN博客

解决并发问题的常见方法有两种:

  1. 在结构体中增加 sync.RWMutex字段,每一个协程操作map集合的时候进行加锁操作,操作结束后进行解锁操作,保证同时只有一个协程操作map,避免并发问题。但是频繁的加锁和解锁操作会成为后期的性能瓶颈。
  2. 使用管道进行通信,由于管道本身就是线程安全的,所以我们在操作层面无需进行加锁和解锁操作,只需要另启一个协程进行管道的读取,如果有数据写入则进行map操作。我们在需要对map进行操作的时候向管道中写入数据即可。

由于第一次在项目中遇到并发问题,一开始没有意识到多个协程对同一个map进行操作需要保证线程安全。在老师查看代码后,说出map是线程不安全的时候,才意识到需要进行加锁操作或者其他方案来保证线程安全。

2.锁解决方案

第一版本的代码——加锁,解锁保证线程安全

在结构体中的ClientsMap进行操作的时候进行加锁和解锁的操作,保证线程安全。

// HupCenter ---使用锁,操作一个多线程共享的Map---//
type HupCenter struct {
	//第一个string-roomId 第二个string-userId
	ClientsMap map[string]map[string]*Client `json:"-"` 
	mutex      sync.RWMutex
}

// JoinHub  写操作 --将连接加入中心 前提RoomId不为空, 加入房间的时候需要检测当前房间里面的人数
func (h *HupCenter) JoinHub(c *Client) (flag bool) {
	h.mutex.Lock()
	defer h.mutex.Unlock()

	//先查询是否存在此一个roomId key
	if myMap, ok := c.Hub.ClientsMap[c.User.RoomId]; ok { //有,加入房间
		//检测人数
		if len(myMap) == 1 {
			myMap[c.User.UserId] = c
			flag = true
		}
	} else { //没有,创建房间
		myMap := make(map[string]*Client)
		myMap[c.User.UserId] = c                //userId
		c.Hub.ClientsMap[c.User.RoomId] = myMap //roomId
		flag = true
	}
	return
}

// DeleteFromHub 写操作 --逻辑删除 将传入的参数c从hub连接池中删除
func (h *HupCenter) DeleteFromHub(c *Client) {
	h.mutex.Lock()
	defer h.mutex.Unlock()

	if c.User.RoomId == "" {
		return
	}
	if value, ok1 := c.Hub.ClientsMap[c.User.RoomId]; ok1 {
		if _, ok2 := value[c.User.UserId]; ok2 {
			delete(value, c.User.UserId)
		}
	}
	if len(c.Hub.ClientsMap[c.User.RoomId]) == 0 {
		delete(c.Hub.ClientsMap, c.User.RoomId)
	}
}

// QueryOtherUser 读操作 -- 根据当前用户寻找另一位用户,返回user对象
func (h *HupCenter) QueryOtherUser(c *Client) *Client {
	if roomMap, ok := h.ClientsMap[c.User.RoomId]; ok { //room
		for userId, user := range roomMap {
			if userId != c.User.UserId {
				return user
			}
		}
	}
	return nil
}

3.管道解决方案

使用锁是能够基本解决问题的,但是对于读写较为频繁的场景,读写锁可能会成为性能瓶颈,再加上自己对管道的运用不是很熟练,就开始思考如何使用channel去解决这一个并发的问题,代码如下:

type HupCenter struct {
	ClientsMap map[string]map[string]*Client `json:"-"` //第一个string-roomId 第二个string-userId
	Register   chan *Client
	UnRegister chan *Client
}

// NewHub 初始化一个hub
func NewHub() *HupCenter {
	return &HupCenter{
		ClientsMap: make(map[string]map[string]*Client),
		Register:   make(chan *Client, 1),
		UnRegister: make(chan *Client, 1),
	}
}

// Run 用户向hub中的逻辑注册、删除、心跳检测全方法,在代码执行后,开始协程去执行Run方法
func (h *HupCenter) Run() {
	checkTicker := time.NewTicker(time.Duration(pkg.HeartCheckSecond) * time.Second)
	defer checkTicker.Stop()

	for {
		select {
		case client := <-h.Register:
			//先查询是否存在此一个roomId key
			if myMap, ok := client.Hub.ClientsMap[client.User.RoomId]; ok { //有,加入房间
				//检测人数
				if len(myMap) == 1 {
					myMap[client.User.UserId] = client
				}
			} else { //没有,创建房间
				myMap := make(map[string]*Client)
				myMap[client.User.UserId] = client                //userId
				client.Hub.ClientsMap[client.User.RoomId] = myMap //roomId
			}
			fmt.Println("有人加入房间:", client.Hub.ClientsMap)
		case client := <-h.UnRegister:
			client.User.Close()
			if value, ok1 := client.Hub.ClientsMap[client.User.RoomId]; ok1 {
				if _, ok2 := value[client.User.UserId]; ok2 {
					delete(value, client.User.UserId)
				}
			}
			if len(client.Hub.ClientsMap[client.User.RoomId]) == 0 {
				delete(client.Hub.ClientsMap, client.User.RoomId)
			}
		case <-checkTicker.C:
			for _, roomMap := range h.ClientsMap {
				for _, client := range roomMap {
					if client.User.HealthCheck.Before(time.Now()) {
						h.UnRegister <- client
					}
				}
			}
			fmt.Println(time.Now().Format(time.DateTime), h.ClientsMap)
		}
	}
}

// QueryOtherUser 读操作 -- 根据当前用户寻找另一位用户,返回user对象
func (h *HupCenter) QueryOtherUser(c *Client) *Client {
	if roomMap, ok := h.ClientsMap[c.User.RoomId]; ok { //room
		for userId, user := range roomMap {
			if userId != c.User.UserId {
				return user
			}
		}
	}
	return nil
}

在代码中,我们在结构体中定义了两个管道,一个管道接收注册的客户端对象(原JoinHub方法),另一个管道接收注销的客户端对象(原DeleteFormHub方法);

在Run方法中,我们创建了一个10秒的ticker对象,来进行客户端连接的心跳检测。之后使用for循环来执行select来监听多个管道,并执行对应的分支操作。select会随机挑选一个可执行的case语句,如果没有可执行的case,则进行等待。在本代码中如果没有注册、注销的操作,会每隔10秒进行一次心跳检测,并打印当前存活的客户端对象集合。

4.总结

在使用锁解决并发问题的时候,一定要使用延迟函数解锁,防止出现死锁问题;

在使用管道解决并发问题的时候,设计好管道的缓冲区和管道的关闭操作,防止出现死锁和向已经关闭的管道中写入数据,发生panic异常。

结语:学会一个知识点最好的方法就是在项目、实战中去应用它。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1513455.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HarmonyOS NEXT应用开发之异常处理案例

介绍 本示例介绍了通过应用事件打点hiAppEvent获取上一次应用异常信息的方法&#xff0c;主要分为应用崩溃、应用卡死以及系统查杀三种。 效果图预览 使用说明&#xff1a; 点击构建应用崩溃事件&#xff0c;3s之后应用退出&#xff0c;然后打开应用进入应用异常页面&#x…

常青内容与病毒式内容——哪个更适合 SEO?

常青内容是经得起时间考验的内容&#xff0c;而病毒式内容则是利用特定时代潮流的内容。 如果你曾经考虑过为网站添加内容&#xff0c;你可能听说过常青内容和病毒式内容这两个词。这两个词涵盖了网站所需的基本内容类型。 那么&#xff0c;这两者之间有什么区别&#xff1f;…

2024年软考计划开始了,你准备好了吗?

目录标题 2024年度计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试工作计划计算机软考中级科目哪个含金量最高&#xff1f;报考流程和说明 2024年度计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试工作计划 一、2024年度计算机软件资格考试(初级…

4款好用的AI写作软件推荐,让你成为写作大神

写作已经成为我们日常生活和工作中必不可少的一部分&#xff0c;当我们在还绞尽脑汁的去想如何写作的时候&#xff0c;别人已经弯道超车用上了人工智能写作软件&#xff0c;今天&#xff0c;小编想为大家推荐4款好用的AI写作软件&#xff0c;让你在几秒钟内生成高质量文章的同时…

IDEA启动时,电脑非常的卡

选择Help -> Change memory Settings 把启动内存调大一点就行了&#xff0c;反正要超过你平时使用IDEA时使用到的内存大小就行。 原因解释&#xff1a; JVM在运行时会回收新生代和老年代的垃圾&#xff0c;新生代无法回收的对象&#xff0c;比如&#xff1a;回收15次都没有…

【一】【设计模式】类关系UML图

1. 继承&#xff08;Generalization&#xff09; 继承是对象间的一种层次关系&#xff0c;允许子类继承并扩展父类的功能。 UML线&#xff1a;带有空心箭头的直线&#xff0c;箭头指向基类&#xff08;父类&#xff09;。 class Parent {public void parentMethod() {System.…

Android Studio下运行java main 方法

方法一 修改项目的.idea中的gradle.xml文件&#xff0c;在GradleProjectSettings标签下添加一行代码 <option name"delegatedBuild" value"false" />方法二 main方法上右键选择Run ‘xxx’ with Coverage

提高螺栓连接强度——SunTorque智能扭矩系统

螺栓连接是工程中常见的一种连接方式&#xff0c;其强度对于设备的稳定性和安全性具有至关重要的影响。然而&#xff0c;由于各种因素的影响&#xff0c;螺栓连接在使用过程中往往会出现松动、断裂等问题&#xff0c;导致设备故障和安全隐患。因此&#xff0c;提高螺栓连接的强…

【大模型API调用初尝试一】智谱AI 通义千问

大模型API调用初尝试一 调用大模型API能干什么智谱AI大模型API调用的过程获取API_KEYGLM_4同步调用GLM_4异步调用文生图大模型API调用 阿里云通义千问API调用过程单轮会话多轮会话 调用大模型API能干什么 大模型的参数非常庞大&#xff0c;功能非常强大&#xff0c;但是训练成…

MySQL中ON DUPLICATE KEY UPDATE的介绍与使用、批量更新、存在即更新不存在则插入

文章目录 一、ON DUPLICATE KEY UPDATE的介绍二、ON DUPLICATE KEY UPDATE的使用2.1、案例一&#xff1a;根据主键id进行更新2.2、案例二&#xff1a;根据唯一索引进行更新&#xff08;常用&#xff09;2.3、案例三&#xff1a;没有主键或唯一键字段值相同就插入2.4、案例四&am…

C#、C++、Java、Python 选择哪个好?

作者&#xff1a;网博汇智 链接&#xff1a;https://www.zhihu.com/question/298323023/answer/2789627224 来源&#xff1a;知乎 著作权归作者所有。商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处。 一个好的程序员不能把自己绑定在一种语言上&#xff0c;不…

element ui 中文离线文档(百度云盘下载)

一般内网开发上不了网&#xff0c;用离线版本比较方便&#xff0c;下载地址&#xff1a; https://download.csdn.net/download/li836779537/88355878?spm1001.2014.3001.5503 下载后里面有个 index.hrml 双击打开就可以用 效果如下&#xff1a;

Android基础开发-选择图片,发送彩信

发送图片彩信案例&#xff1a; 按住加号&#xff0c;选择相册&#xff0c;把相册选择的图片加载的应用中&#xff0c;点击发送彩信&#xff0c;选择短信&#xff0c;发送彩信。 代码如下&#xff1a; package com.example.client;import androidx.activity.result.ActivityRe…

redis-操作数据库

0 序言 一个Redis服务器可以包含多个数据库。在默认情况下&#xff0c;Redis服务器在启动时将会创建16个数据库&#xff1a;这些数据库都使用号码进行标识&#xff0c;其中第一个数据库为0号数据库&#xff0c;第二个数据库为1号数据库&#xff0c;而第三个数据库则为2号数据库…

Prompt进阶3:LangGPT(构建高性能质量Prompt策略和技巧2)--稳定高质量文案生成器

Prompt进阶3:LangGPT(构建高性能质量Prompt策略和技巧2)–稳定高质量文案生成器 1.LangGPT介绍 现有 Prompt 创建方法有如下缺点&#xff1a; 缺乏系统性&#xff1a;大多是细碎的规则&#xff0c;技巧&#xff0c;严重依赖个人经验缺乏灵活性&#xff1a;对他人分享的优质 …

高铁列车员信息宣传向媒体投稿有哪些方法?

作为一名高铁列车工作人员,我肩负着传递高铁精神、展示列车员风采的重要使命。每月,我都要完成单位对外信息宣传的考核任务,通过媒体投稿来发表列车员的信息宣传文章。在这条信息宣传之路上,我经历了从摸着石头过河到智慧投稿的蜕变,其中的心酸与轻松交织,成为了我职业生涯中难…

Android 11 最终 Beta 版发布,正式版即将到来!

Beta 3 中的更新 本次更新包括针对 Pixel 设备和 Android 模拟器的 Android 11 发布候选版本。我们在 Beta 2 时就已经达到了平台稳定性里程碑&#xff0c;即所有面向应用的接口和行为都已敲定&#xff0c;包括 SDK 和 NDK API、面向应用的系统行为&#xff0c;以及对非 SDK 接…

【Linux】常用指令大全 [万字详解!建议收藏记忆!]

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

逆变器专题(21)-并网电压前馈(消除弱电网影响)(1)

在逆变器并网系统中&#xff0c;并网电压前馈是整个系统中较为重要的一环&#xff0c;一方面&#xff0c;他可以在一定程度上增加系统的快速响应能力&#xff0c;另一方面&#xff0c;其可以 消除电网电压中的低次谐波对并网输出电流的影响&#xff0c;使得整个系统的并网电能质…

spring boot 学习

目录 引言&#xff1a; 一、Spring Boot概述 二、Spring Boot的核心特性 1 自动配置 2 起步依赖 3 内嵌容器 4 监控与管理 三、Spring Boot的入门步骤 1 环境安装 2 创建项建 3 编写代码 1 启动类 2 控制器 3服务 4自动装配 5配置属性 6 JPA实体 4 运行与调试…