逐步学习Go-sync.RWMutex(读写锁)-深入理解与实战

news2025/5/26 10:27:42

概述

在并发编程中,我们经常会遇到多个线程或协程访问共享资源的情况。为了保护这些资源不被同时修改,我们会用到"锁"的概念。

Go中提供了读写锁:sync.RWMutex。
sync.RWMutex是Go语言提供的一个基础同步原语,它是Reader/Writer Mutual Exclusion Lock的缩写,通常被称为"读写锁"。
读写锁允许多个读锁同时拥有者,但在任何时间点只允许一个写锁拥有者,或者没有锁拥有者。

这让读多写少的场景获得了更高的并发性能。

应用场景

  1. 典型应用场景就是读多写少
  2. 一写多读

提供的方法

sync.RWMutex提供了以下方法:

type RWMutex
// 获取写锁,有读锁或者写锁被其他goroutine使用则阻塞等待
func (rw *RWMutex) Lock()
// 尝试获取写锁,获取到则返回true,没有获取到则为false
func (rw *RWMutex) TryLock() bool
// 释放写锁
func (rw *RWMutex) Unlock()
// 获取读锁,
func (rw *RWMutex) RLock()
// 尝试获取读锁,获取到则返回true,没有获取到则为false
func (rw *RWMutex) TryRLock() bool
// 释放读锁
func (rw *RWMutex) RUnlock()

// 返回Locker
func (rw *RWMutex) RLocker() Locker

COPY

注意

使用RWMutex的时候,一旦调用了Lock方法,就不能再把该锁复制到其他地方使用,否则可能会出现各种问题。这是由于锁的状态(被哪个协程持有,是否已经被锁定等)是存储在RWMutex的结构体中,如果复制了RWMutex,那么复制后的RWMutex就会有一个全新的状态,锁的行为就会变得不可预测。
RWMutex和Mutex一样,一旦有了Lock调用就不能到处copy了,否则出现各种问题。

源码实现

RWMutex结构体

让我们一起深入Go的源码,看看RWMutex是如何实现的。
RWMutex 的结构体主要包括五个主要的字段,这些字段描述了锁的当前状态和持有者信息:

type RWMutex struct {
   // Mutex,互斥锁。写者互斥锁,所有的写者加锁都调用w.Lock或者w.TryLock
    w           Mutex   

    // 写者信号量。当最后一个读者释放了锁,会触发一个信号通知writerSem
    writerSem   uint32  

    // 读者信号量。当写者释放了锁,会触发一个信号通知readerSem
    readerSem   uint32      

    // readerCount 记录当前持有读锁的协程数量。如果为负数,表示有写者在等待所有读者释放锁。如果为0,表示没有任何协程持有锁
    readerCount atomic.Int32 

   // readerWait 记录写者需要等待的读者数量。当一个写者获取了锁之后,readerWait会设置为当前readerCount的值。当读者释放锁时,readerWait会递减1
    readerWait  atomic.Int32 
}

COPY

读者加锁RLock()

加读锁时非常简单,就是将结构体中的readerCount加1,如果+1后为负数表示有写者等待则等待写者执行完成。

实现代码

func (rw *RWMutex) RLock() {
    // 读者数量+1
    if rw.readerCount.Add(1) < 0 {
        // 加1以后如果readerCount是负数表示有写者持有了互斥锁
        // 读者等待信号量释放
        // 此时读锁已经加上了,等待写者释放信号量就可以了
        runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)
    }
}

COPY

读者RTryLock()

这个函数是RWMutex中的TryRLock方法,它试图以非阻塞的方式获取读锁。让我们一步一步地看它是如何工作的。
先看图:

a771641e0c3056616025f39cfa7076b2.png

实现代码


func (rw *RWMutex) TryRLock() bool {
    for {
        // 查看当前读者数量
        c := rw.readerCount.Load()
        if c < 0 {
            // 小于0表示有写者已经Penging,加锁失败
            return false
        }
        // 读者数量+1,加读锁成功
        if rw.readerCount.CompareAndSwap(c, c+1) {
            return true
        }
    }
}

COPY

读者释放读锁RUnlock()

RUnlock方法用于释放读锁。 当一个读者完成读操作并想要释放锁时,就可以调用这个方法。

1af56f016db14721529ee7dd77dfb6bb.png

实现代码


func (rw *RWMutex) RUnlock() {
    // 释放锁就是-1,
    // 如果readerCount小于0表示有写者Pending
    // 进入rUnlockSlow
    if r := rw.readerCount.Add(-1); r < 0 {
        rw.rUnlockSlow(r)
    }
}

func (rw *RWMutex) rUnlockSlow(r int32) {
    // 边界问题处理
    // r+1 ==0 表示没有读者加锁,却调用了释放读锁
    // r+1 == -rwmutexMaxReaders表示没有读者加锁,有写者持有互斥锁却释放读锁
    if r+1 == 0 || r+1 == -rwmutexMaxReaders {
        race.Enable()
        fatal("sync: RUnlock of unlocked RWMutex")
    }

    // 这表示这是最后一个读者了,最后一个读者要发送信号量通知写者不用等了
    if rw.readerWait.Add(-1) == 0 {
        // The last reader unblocks the writer.
        runtime_Semrelease(&rw.writerSem, false, 1)
    }
}

COPY

写者加锁Lock()

实现代码


const rwmutexMaxReaders = 1 << 30

func (rw *RWMutex) Lock() {
    // 先持有互斥锁,已经有其他写者持有了互斥锁则等待
    rw.w.Lock()

    // rw.readerCount.Add(-rwmutexMaxReaders)这个表示先将readerCount设置为负数表示有写者在等待
    // 再+rwmutexMaxReaders是为了求出当前reader的数量
    r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders

    // 将当前reader的数量加到readerWait表示要等待的读者完成的个数
    if r != 0 && rw.readerWait.Add(r) != 0 {
        // 阻塞等待万有的读者完成释放信号量了
        runtime_SemacquireRWMutex(&rw.writerSem, false, 0)
    }
}

COPY

写者加锁TryLock()

实现代码


func (rw *RWMutex) TryLock() bool {
    // 调用互斥锁的TryLock,互斥锁TryLock返回false这儿也直接返回false
    if !rw.w.TryLock() {
        return false
    }

    // 加锁成功后
    // 如果当前还有写者,CompareAndSwap就返回失败
    if !rw.readerCount.CompareAndSwap(0, -rwmutexMaxReaders) {
        // 返回失败就释放互斥锁
        rw.w.Unlock()
        // 加锁失败
        return false
    }
    // 加锁成功
    return true
}

COPY

写者解锁Unlock()

实现代码


func (rw *RWMutex) Unlock() {
    // 这里是对Lock readerCount的逆向操作
    // 在Lock的时候对readerCount减去了rwmutexMaxReaders,这次加回来;这样就还原了readerCount,即使在Lock之后依然有读者加锁
    r := rw.readerCount.Add(rwmutexMaxReaders)
    if r >= rwmutexMaxReaders {
        race.Enable()
        fatal("sync: Unlock of unlocked RWMutex")
    }

    // 然后循环看当前有多少读者正在等待信号,就释放多少次心血号
    for i := 0; i < int(r); i++ {
        runtime_Semrelease(&rw.readerSem, false, 0)
    }
    // Allow other writers to proceed.
    rw.w.Unlock()
}

COPY

测试


package mutex_test

import (
    "sync"
    "testing"
    "time"

    "github.com/stretchr/testify/assert"
)

// 测试读写互斥锁在正常读锁定和解锁情况下的成功执行
func TestRWMutex_ShouldSuccess_WhenNormalReaderLockAndUnLock(t *testing.T) {
    // 初始化一个读写互斥锁
    rwmutex := sync.RWMutex{}
    // 获取读锁
    rwmutex.RLock()
    // 设置成功标志为true,使用defer确保在函数结束时释放读锁
    isSuccess := true
    defer rwmutex.RUnlock()
    // 记录日志表示测试成功
    t.Log("success")
    // 断言成功标志为true
    assert.True(t, isSuccess)
}

// 测试RWMutex的写锁功能是否正常
func TestRWMutex_ShouldSuccess_WhenNormalWriterLockAndUnLock(t *testing.T) {
    rwmutex := sync.RWMutex{} // 创建一个sync.RWMutex类型的变量
    rwmutex.Lock()            // 获取写锁
    isSuccess := true         // 标记为成功状态
    defer rwmutex.Unlock()    // 确保在函数退出时释放锁,避免死锁
    t.Log("success")          // 记录测试日志
    assert.True(t, isSuccess) // 断言isSuccess为true,验证操作成功
}

// 函数测试了在正常情况下,
// 读写锁(RWMutex)的读锁(RLock)和写锁(Lock)的加锁与解锁操作是否成功。
func TestRWMutex_ShouldSuccess_WhenNormalReaderWriterLockAndUnLock(t *testing.T) {
    // 初始化一个读写锁
    rwmutex := sync.RWMutex{}
    // 尝试获取读锁并立即释放
    rwmutex.RLock()
    rwmutex.RUnlock()
    // 尝试获取写锁并立即释放
    rwmutex.Lock()
    rwmutex.Unlock()
    // 标记测试为成功
    isSuccess := true
    // 记录测试成功日志
    t.Log("success")
    // 断言测试结果为真
    assert.True(t, isSuccess)
}

// 测试读写锁在多协程情况下的读写互斥
func TestRWMutex_ShouldSuccess_WhenReaderAndWriterInDifferentRoutine(t *testing.T) {
    // 初始化一个读写锁和等待组,用于协调不同协程的操作。
    rwmutex := sync.RWMutex{}
    wg := sync.WaitGroup{}
    wg.Add(2) // 预期有两个协程完成操作

    // 启动一个协程作为读锁持有者
    go func() {
        rwmutex.RLock()   // 获取读锁
        println("reader") // 打印读操作标识
        rwmutex.RUnlock() // 释放读锁
        wg.Done()         // 表示读操作完成
    }()

    // 启动另一个协程作为写锁持有者
    go func() {
        rwmutex.Lock()    // 获取写锁
        println("writer") // 打印写操作标识
        rwmutex.Unlock()  // 释放写锁
        wg.Done()         // 表示写操作完成
    }()

    wg.Wait() // 等待所有协程完成操作
    isSuccess := true
    t.Log("success")          // 记录测试成功
    assert.True(t, isSuccess) // 断言测试结果为真
}

// 测试读写锁在多个读锁情况下的读写互斥
func TestRWMutex_ShouldBlockWriter_WhenMultipleReader(t *testing.T) {
    rwmutex := sync.RWMutex{}
    ch := make(chan bool)
    wg := sync.WaitGroup{}
    wg.Add(2)
    for i := 0; i < 2; i++ {
        go func(i int) {
            wg.Done()
            rwmutex.RLock()
            println("reader Locked", i)
            time.Sleep(10 * time.Second)
            rwmutex.RUnlock()
            println("reader UnLocked", i)
        }(i)
    }

    go func() {
        wg.Wait()
        println("writer try to accquire wlock")
        rwmutex.Lock()
        println("writer has accquired wlock")
        defer rwmutex.Unlock()
        ch <- true
    }()

    <-ch
    isSuccess := true
    t.Log("success")
    assert.True(t, isSuccess)
}

// 测试读写锁在多个写锁情况下的读写互斥
func TestRWMutex_ShouldBlockReaders_WhenWriterIsPresent(t *testing.T) {
    rwmutex := sync.RWMutex{}
    wg := sync.WaitGroup{}
    wg.Add(1)

    go func() {
        println("writer try to accquire wlock")
        rwmutex.Lock()
        println("writer has accquired wlock")
        wg.Done()
        time.Sleep(10 * time.Second)
        defer rwmutex.Unlock()
        println("writer has released wlock")
    }()

    wg.Wait()
    wg.Add(2)
    for i := 0; i < 2; i++ {
        go func(i int) {
            println("reader try to lock", i)
            rwmutex.RLock()
            println("reader Locked", i)
            rwmutex.RUnlock()
            println("reader UnLocked", i)
            wg.Done()
        }(i)
    }

    wg.Wait()
    isSuccess := true
    t.Log("success")
    assert.True(t, isSuccess)
}

// 测试读写锁在多个写锁情况下的读写互斥
func TestRWMutex_ShouldBlockConcurrentWriters(t *testing.T) {
    rwmutex := sync.RWMutex{}
    var blockedWriter bool
    ch := make(chan bool)
    wg := sync.WaitGroup{}

    wg.Add(1)
    go func() {
        wg.Done()
        println("Writer 1 try to accquire wlock")
        rwmutex.Lock()
        println("Writer 1 has accquired wlock")
        defer rwmutex.Unlock()
        time.Sleep(15 * time.Second)
    }()

    go func() {
        wg.Wait()
        println("Writer 2 try to accquire wlock")
        rwmutex.Lock()
        println("Writer 2 has accquired wlock")
        ch <- true
        defer rwmutex.Unlock()
    }()

    select {
    case <-ch:
        blockedWriter = false
    case <-time.After(20 * time.Second):
        blockedWriter = true
    }
    assert.True(t, blockedWriter)
}

// 测试读写锁在多个读锁情况下的读写互斥
func TestRWMutex_ShouldLockSuccess_WhenTryingToReadLockTwice(t *testing.T) {
    rwmutex := sync.RWMutex{}
    writerWaitGroup := sync.WaitGroup{}
    writerWaitGroup.Add(1)

    go func() {
        rwmutex.RLock()
        println("readlock locked once")
        rwmutex.RLock()
        println("readlock locked twice")
        rwmutex.RUnlock()
        rwmutex.RUnlock()
        defer writerWaitGroup.Done()
    }()

    writerWaitGroup.Wait()
    isSuccess := true

    assert.True(t, isSuccess)
}

// 测试读写锁在多个写锁情况下的读写互斥
func TestRWMutex_ShouldBeBlocked_WhenTryingToWriteLockTwice(t *testing.T) {
    rwmutex := sync.RWMutex{}
    ch := make(chan bool)
    go func() {
        rwmutex.Lock()
        println("writelock locked once")
        rwmutex.Lock()
        println("writelock locked twice")
        rwmutex.Unlock()
        rwmutex.Unlock()
        ch <- true
    }()

    isBlocked := false

    select {
    case <-ch:
        println("should not execute this block")
        assert.False(t, isBlocked)
    case <-time.After(10 * time.Second):
        isSuccess := true
        println("executed timeout block")
        assert.True(t, isSuccess)
    }

}

// 测试读写锁在多个读锁情况下的读写互斥
func TestRWMutex_ShouldBeBlocked_WhenAccquireWriteLockThenReadLock(t *testing.T) {
    rwmutex := sync.RWMutex{}
    ch := make(chan bool)
    go func() {
        rwmutex.Lock()
        println("writelock locked once")
        rwmutex.RLock()
        println("readlock locked twice")
        rwmutex.RUnlock()
        rwmutex.Unlock()
        ch <- true
    }()
    isBlocked := false

    select {
    case <-ch:
        println("should not execute this block")
        assert.False(t, isBlocked)
    case <-time.After(10 * time.Second):
        isSuccess := true
        println("executed timeout block")
        assert.True(t, isSuccess)
    }

}

// 测试读写锁在多个读锁情况下的读写互斥
func TestRWMutex_ShouldBeBlocked_WhenAccquireReadLockThenWriteLock(t *testing.T) {
    rwmutex := sync.RWMutex{}
    ch := make(chan bool)
    go func() {
        rwmutex.RLock()
        println("readlock locked once")
        rwmutex.Lock()
        println("writelock locked twice")
        rwmutex.Unlock()
        rwmutex.RUnlock()
        ch <- true
    }()
    isBlocked := false

    select {
    case <-ch:
        println("should not execute this block")
        assert.False(t, isBlocked)
    case <-time.After(10 * time.Second):
        isSuccess := true
        println("executed timeout block")
        assert.True(t, isSuccess)
    }

}

// 测试读写锁在多个读锁情况下的读写互斥
func TestRWMutex_ShouldDeadlockOrBlocked_WhenLockOneGoroutineAccquiredLockAndAnotherGoroutineAccquireLockAgain(t *testing.T) {
    var rwmutex1, rwmutex2 sync.RWMutex
    wg := sync.WaitGroup{}
    wg1 := sync.WaitGroup{}
    ch := make(chan bool)

    wg.Add(1)
    wg1.Add(1)
    go func() {
        rwmutex1.Lock()
        println("rwmutex1 locked")
        wg.Done()
        wg1.Wait()
        println("rwmutex2 try to accquire lock")
        rwmutex2.Lock()
    }()
    go func() {
        wg.Wait()
        rwmutex2.Lock()
        println("rwmutex2 locked")
        wg1.Done()
        println("rwmutex1 try to accquire lock")
        rwmutex1.Lock()
        ch <- true
    }()
    isBlocked := false

    select {
    case <-ch:
        println("should not execute this block")
        assert.False(t, isBlocked)
    case <-time.After(10 * time.Second):
        isSuccess := true
        println("executed timeout block")
        assert.True(t, isSuccess)
    }

}

参考

 逐步学习Go-sync.RWMutex(读写锁)-深入理解与实战 – 小厂程序员

 

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

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

相关文章

Python | Leetcode Python题解之第26题删除有序数组中的重复项

题目&#xff1a; 题解&#xff1a; class Solution:def removeDuplicates(self, nums: List[int]) -> int:if not nums:return 0n len(nums)fast slow 1while fast < n:if nums[fast] ! nums[fast - 1]:nums[slow] nums[fast]slow 1fast 1return slow

内存函数memcpy、mommove、memset、memcmp

目录 1、memcpy函数 memcpy函数的模拟实现 2、memmove函数 memmove函数的模拟实现 3、memset函数 4、memcmp函数 1、memcpy函数 描述&#xff1a; C 库函数 void *memcpy(void *str1, const void *str2, size_t n) 从存储区 str2 复制 n 个字节到存储区 str1。 声明&…

UnityShader学习计划

1.安装ShaderlabVS,vs的语法提示 2. 常规颜色是fixed 3.FrameDebugger调试查看draw的某一帧的全部信息&#xff0c;能看到变量参数的值

基于博客系统的功能测试和性能测试

目录 项目介绍 项目功能 设计测试用例 功能测试--自动化测试 测试代码 登录测试 博客详情页测试 发布博客测试 删除博客测试 退出账号测试 性能测试 项目介绍 1.博客系统采用前后端分离的方法来实现&#xff0c;同时使用了数据库来存储相关的数据&#xff0c;同时将…

Vue - 5( 16000 字 Vue2 入门级教程)

一&#xff1a;Vue 初阶 1.1 组件自定义事件 在 Vue 中&#xff0c;组件间通过自定义事件进行通信是一种常见的模式。自定义事件允许子组件向父组件发送消息&#xff0c;也可以在组件内部进行事件的绑定、触发和解绑。让我们详细讲解这些知识点。 1.1.1 组件自定义事件 在 …

CSS核心样式-02-盒模型属性及扩展应用

目录 三、盒模型属性 常见盒模型区域 盒模型图 盒模型五大属性 1. 宽度 width 2. 高度 height 3. 内边距 padding 四值法 三值法 二值法 单值法 案例 4. 边框 border 按照属性值的类型划分为三个单一属性 ①线宽 border-width ②线型 border-style ③边框颜色 bo…

开源项目one-api的k8s容器化部署(下)-- 部署至k8s

一、接着上文 本文讲述如何把上文制作好的docker镜像部署到K8S&#xff0c;会涉及以下部分&#xff1a; 健康检测应用程序的配置应用程序的端口日志路径 二、健康检测 1、健康状态 从官方的docker-compose.yml可以得知其健康检测方法 curl http://localhost:5175/api/statu…

正则表达式:特殊序列(五)

正则表达式中的特殊序列包括&#xff1a;1. \d&#xff1a;匹配任意数字字符&#xff0c;等同于[0-9]。2. \D&#xff1a;匹配任意非数字字符&#xff0c;等同于[^0-9]。3. \w&#xff1a;匹配任意字母、数字或下划线字符&#xff0c;等同于[A-Za-z0-9_]。4. \W&#xff1a;匹配…

电路知识分享

电路 1. 基尔霍夫 KCL 定律 基尔霍夫定律包括电流定律和电压定律 电流定律KCL&#xff1a;在电路中&#xff0c;任何时刻&#xff0c;对任一节点&#xff0c;所有流出节点的支路电流的代数和恒等于零。 电压定律KVL&#xff1a;在集总电路中&#xff0c;任何时刻&#xff0…

3D场景编辑方法——CustomNeRF

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 摘要Abstract文献阅读&#xff1a;3D场景编辑方法——CustomNeRF1、研究背景2、提出方法3、CustomNeRF3.1、整体框架步骤3.2、对特定问题的解决 4、实验结果5、总结…

支持向量机模型

通过5个条件判定一件事情是否会发生&#xff0c;5个条件对这件事情是否发生的影响力不同&#xff0c;计算每个条件对这件事情发生的影响力多大&#xff0c;写一个支持向量机模型程序,最后打印5个条件分别的影响力。 示例一 为了计算每个条件对一件事情发生的影响力&#xff0c…

年龄与疾病c++

题目描述 某医院想统计一下某项疾病的获得与否与年龄是否有关&#xff0c;需要对以前的诊断记录进行整理&#xff0c;按照0-18岁、19-35岁、36-60岁、61以上&#xff08;含61&#xff09;四个年龄段统计的患病人数以及占总患病人数的比例。 输入 共2行&#xff0c;第一行为过…

JavaSE——常用API进阶二(3/8)-Date、SimpleDateFormat(构造器、常用的方法、用法示例、时间格式的常见符号)

目录 Date 构造器、常用的方法 用法示例 SimpleDateFormat 构造器、格式化时间的方法 时间格式的常见符号 用法示例 解析字符串时间成为日期对象 接下来会学习JDK8以前传统的日期、时间&#xff0c;以及JDK8开始新增的日期、时间&#xff1b;有部分项目还是有在使用JDK…

雷霆传奇H5_源码搭建架设_神魔之魔改龙珠2

本教程仅限学习使用&#xff0c;禁止商用&#xff0c;一切后果与本人无关&#xff0c;此声明具有法律效应&#xff01;&#xff01;&#xff01;&#xff01; 一. 效果演示 雷霆传奇H5_源码搭建架设_神魔之魔改龙珠2 联网环境&#xff1a; centos7.6 &#xff0c; 放开所有端口…

数据治理专家岗位的能力模型

数据治理专家的角色要求其具备全方位的专业素养与技能&#xff0c;不仅要有深厚的业务理解与数据技术功底&#xff0c;还需展现出卓越的领导力、团队协作与沟通能力&#xff0c;以驱动组织内部数据治理工作的高效运行与持续优化。以下是对数据治理专家各项能力的深入解读&#…

算法题 - 双指针

目录 125. 验证回文串392. 判断子序列167. 两数之和 Ⅱ - 输入有序数组11. 盛最多的水15. 三数之和 125. 验证回文串 LeetCode_link 如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后&#xff0c;短语正着读和反着读都一样。则可以认为该短语是一个 回文串 …

【问题篇】activiti工作流流程图更新后旧数据问题

互相学习交流 当我们使用activiti开发工作流时&#xff0c;项目上线后可能修改需求导致修改流程图也是很常见的情况。但是activiti更新流程图后&#xff0c;以前的流程实例并不会也跟着更新&#xff0c;activiti会保存每一份的流程图版本&#xff0c;只有新发起的流程实例才会…

架构设计-权限系统之通用的权限系统设计方案

一个系统&#xff0c;如果没有安全控制&#xff0c;是十分危险的&#xff0c;一般安全控制包括身份认证和权限管理。用户访问时&#xff0c;首先需要查看此用户是否是合法用户&#xff0c;然后检查此用户可以对那些资源进行何种操作&#xff0c;最终做到安全访问。身份认证的方…

spring boot集成logback到mysql 8

spring boot集成logback到mysql 8 依赖数据库准备创建log日志用户&#xff0c;并创建数据库执行建表sql 配置文件bugbug 1&#xff1a;Failed to instantiate type ch.qos.logback.classic.db.DBAppenderbug信息&#xff1a;解决&#xff1a; bug2: DBAppender cannot function…

开源博客项目Blog .NET Core源码学习(14:App.Hosting项目结构分析-2)

开源博客项目Blog的前台页面&#xff08;如下图所示&#xff09;的控制器类保存在App.Hosting项目的Controllers文件夹内&#xff0c;页面保存在Views文件夹内&#xff0c;网页中使用的图标、js、css文件等保存在wwwroot文件中。 前台各个页面、Controller文件夹中的控制器类及…