├── README.md
└── golang
├── 并发编程
├── 实用教程
│ ├── imgs
│ │ ├── 0
│ │ │ └── 1.png
│ │ └── 1
│ │ │ ├── DiningPhilosophers.png
│ │ │ ├── read_modify_write.png
│ │ │ └── shared_memory.png
│ ├── 用Go语言学习并发 - 0.概述.md
│ └── 用Go语言学习并发 - 1.锁的作用.md
└── 并发机制
│ ├── Go并发机制.md
│ └── imgs
│ ├── 1.png
│ ├── 2.png
│ └── 3.jpg
├── 应用程序
└── 用Go和FUSE自己的文件系统
│ ├── README.md
│ └── imgs
│ ├── FUSE.png
│ └── VFS.jpg
└── 标准库
└── http包基础
└── README.md
/README.md:
--------------------------------------------------------------------------------
1 | # blogpost
2 | 博客文章
3 |
4 | ### Go
5 | ##### 标准库
6 | [标准库net/http包使用及工作原理](https://github.com/k2huang/blogpost/blob/master/golang/%E6%A0%87%E5%87%86%E5%BA%93/http%E5%8C%85%E5%9F%BA%E7%A1%80/README.md)
7 | ##### 应用
8 | [用Go和FUSE写自己的文件系统](https://github.com/k2huang/blogpost/blob/master/golang/%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F/%E7%94%A8Go%E5%92%8CFUSE%E8%87%AA%E5%B7%B1%E7%9A%84%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F/README.md)
9 |
10 | ##### 并发系列
11 | [Go并发机制](https://github.com/k2huang/blogpost/blob/master/golang/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/%E5%B9%B6%E5%8F%91%E6%9C%BA%E5%88%B6/Go%E5%B9%B6%E5%8F%91%E6%9C%BA%E5%88%B6.md)
12 | [用Go语言学习并发 - 0.概述](https://github.com/k2huang/blogpost/blob/master/golang/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/%E5%AE%9E%E7%94%A8%E6%95%99%E7%A8%8B/%E7%94%A8Go%E8%AF%AD%E8%A8%80%E5%AD%A6%E4%B9%A0%E5%B9%B6%E5%8F%91%20-%200.%E6%A6%82%E8%BF%B0.md)
13 | [用Go语言学习并发 - 1.锁的作用](https://github.com/k2huang/blogpost/blob/master/golang/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/%E5%AE%9E%E7%94%A8%E6%95%99%E7%A8%8B/%E7%94%A8Go%E8%AF%AD%E8%A8%80%E5%AD%A6%E4%B9%A0%E5%B9%B6%E5%8F%91%20-%201.%E9%94%81%E7%9A%84%E4%BD%9C%E7%94%A8.md)
--------------------------------------------------------------------------------
/golang/并发编程/实用教程/imgs/0/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k2huang/blogpost/5225b76a86f855088cd7c40c44fde9949159c8f8/golang/并发编程/实用教程/imgs/0/1.png
--------------------------------------------------------------------------------
/golang/并发编程/实用教程/imgs/1/DiningPhilosophers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k2huang/blogpost/5225b76a86f855088cd7c40c44fde9949159c8f8/golang/并发编程/实用教程/imgs/1/DiningPhilosophers.png
--------------------------------------------------------------------------------
/golang/并发编程/实用教程/imgs/1/read_modify_write.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k2huang/blogpost/5225b76a86f855088cd7c40c44fde9949159c8f8/golang/并发编程/实用教程/imgs/1/read_modify_write.png
--------------------------------------------------------------------------------
/golang/并发编程/实用教程/imgs/1/shared_memory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k2huang/blogpost/5225b76a86f855088cd7c40c44fde9949159c8f8/golang/并发编程/实用教程/imgs/1/shared_memory.png
--------------------------------------------------------------------------------
/golang/并发编程/实用教程/用Go语言学习并发 - 0.概述.md:
--------------------------------------------------------------------------------
1 | ### 写在前面的话
2 | **一直想写一个系列文章,让那些对并发编程感兴趣但又不是很熟悉的人可以看的懂,学的会,又能从原理上理解相关技术。特别像主流语言中提供的锁,条件变量,原子操作,常用并发安全的数据结构,线程池的使用及其原理都是通用的,如果能够从整体上理解这些技术,将对自己的并发编程能力打下坚实的基础。**
3 | 我希望自己可以写一个以实例与原理相结合的系列文章,不去过多地讲特别难理解的地方,如内存模型。借助Go语言,但希望讲出来的东西是common的,在其他语言中(如C/C++/Java)也同样适用。我相信如果这些基础真正掌握了,自己再去学习内存模型,free-lock数据结构等等并不会很困难。
4 |
5 | ### 并发与并行
6 | Go语言发明者之一 Rob Pike:
7 | - Concurrency is not Parallelism
8 | - Concurrency is about dealing with lots of things at once.
9 | - Parallelism is about doing lots of things at once.
10 |
11 | Erlang 的发明者 Joe Armstrong:
12 | 
13 |
14 | 从两位大神的说法中我们可以体会到并发(Concurrency)与并发(Parallelism)的不同。一般来说并发是一个更为通用的概念,它的外延包含并发,实际上我们会提到并行的场景并不是很多,主要集中在数据并行方面,如GPU计算,大数据处理等。当然我们也没有必要把两者完全区分开来,在现在的并发编程世界中,一般两者同时存在于同一个系统中。
15 |
16 | ### 多线程与多进程
17 | 我们在设计系统时,当有多个任务需要并发处理时,多线程与多进程是我们经常会选择的两种方式。多线程的方式:轻量,线程之间的通信简单但隔离性较差,任务一个线程挂掉都有可能把整个进程弄挂掉。多进程的方式:隔离性较好,但进程之间通信成本较大。在现代大型分布式环境中,两者经常组合使用,利用进程隔离服务之间的边界,利用线程提高每个服务的响应速度。
18 |
19 | ### 协程与异步
20 | 线程相比进程来说,对系统的开销已经很小了,但其上下文切换依然会消耗许多CPU资源,所以比线程更轻量级的协程应运而生,协程本质上是一种用户态线程(区别于OS线程,内核调度器只能看到OS线程),协程的上下文切换不需要OS内核的参与,因此开销更小。现在许多语言都或多或少的支持协程,如lua, python等等
21 | 异步主要用在网络IO方面,借助epoll/kqueue/iocp等技术,可以高效管理大量IO连接,是现在网络IO高并发编程中必不可少的一种手段了,常见的如node.js, java的netty等等,但异步编程的方式对普通后端开发人员来说很难适应。
22 | 有人说Go语言是为并发而生,而协程与异步编程模型又是当前高并发编程中的利器,Go语言的并发就是将这两门技术巧妙地融合到了Go的运行时系统中,即用协程取代OS线程,并实现自己的调度器来管理的协程的切换,并将epoll技术融入自己的网络库和运行时系统中,让用户用容易理解和使用的传统的同步的编程思维去处理网络IO,并享受到异步网络IO带来的高性能。详情可以参考我的这篇文章 - [Go并发机制](https://github.com/k2huang/blogpost/blob/master/golang/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/%E5%B9%B6%E5%8F%91%E6%9C%BA%E5%88%B6/Go%E5%B9%B6%E5%8F%91%E6%9C%BA%E5%88%B6.md)。
23 |
24 | ### 并发编程的难点
25 | 1. 上下文切换带来的不确定性和对系统性能的影响
26 | 我们知道操作系统内核调度器的主要任务就是调度各个线程在CPU上运行,如果我们的程序设计的不当,有可能会造成内核调度器频繁在多个线程之间做上下文切换。如果上下文切换消耗太多CPU时间的话,线程的执行时间就会减少,从而影响性能的影响。所以线程越多不一定完成任务的速度越快(单机C10K问题),我们应该用合适数量的线程,并尽量减少线程由于锁同步,IO等待等阻塞操作而被内核调度器做上下文切换。
27 | 2. 死锁导致的系统不可用
28 | 死锁也是在并发编程中常见的问题,它属于我们的程序逻辑错误,需要及时发现并改正。死锁常常出现在多个线程在多把锁之间的争夺过程中,或对一把锁递归使用(同一线程未解锁之前又一次上锁),我们后面还会做具体的介绍。
29 | 3. 资源限制情况下如何通过并发提供资源利用率和系统高可用
30 | 这是个很大的话题,在这里只做简单的介绍。资源限制主要来源于两个方面,1.硬件资源,如cpu,内存,网络带宽等;2.软件资源,如数据连接数和socket连接数等。
31 | 如何在软硬件资源限制情况下进行并发是一个需要长期实践和优化的过程,如通过分布式突破硬件资源的限制,通过连接池解决一些软件资源的限制等等。
--------------------------------------------------------------------------------
/golang/并发编程/实用教程/用Go语言学习并发 - 1.锁的作用.md:
--------------------------------------------------------------------------------
1 | #### 系列文章
2 | [用Go语言学习并发 - 0.概述](https://github.com/k2huang/blogpost/blob/master/golang/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/%E5%AE%9E%E7%94%A8%E6%95%99%E7%A8%8B/%E7%94%A8Go%E8%AF%AD%E8%A8%80%E5%AD%A6%E4%B9%A0%E5%B9%B6%E5%8F%91%20-%200.%E6%A6%82%E8%BF%B0.md)
3 |
4 | 现代编程语言大多支持线程,而多线程编程是并发编程中最常用的方法,Go语言通过Goroutine来支持并发编程,有关其原理可以看这篇[Go并发机制](https://github.com/k2huang/blogpost/blob/master/golang/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/%E5%B9%B6%E5%8F%91%E6%9C%BA%E5%88%B6/Go%E5%B9%B6%E5%8F%91%E6%9C%BA%E5%88%B6.md)。
5 | 本文主要从宏观上说说在并发编程中
6 | 1. 为什么需要锁?
7 | 2. 锁的作用是什么?
8 | 3. 死锁问题和常见解决方法
9 |
10 | #### 第一把锁
11 | 先看一个简单的程序:
12 | ```go
13 | func main() {
14 | var (
15 | counter int
16 | wg sync.WaitGroup // 用于统计正在运行Goroutine的数量
17 | )
18 |
19 | start := time.Now()
20 | for i := 0; i < 100; i++ {
21 | wg.Add(1)
22 | go func() {
23 | defer wg.Done()
24 | for i := 0; i < 10000; i++ {
25 | counter++
26 | }
27 | }()
28 | }
29 | wg.Wait() // 等待所以Goroutine都执行完
30 | elapsed := time.Since(start)
31 | fmt.Println("counter:", counter)
32 | fmt.Println("time consume:", elapsed.Seconds(), "s")
33 | }
34 | ```
35 | 某一次的运行结果(我的电脑是4核8线程的CPU):
36 | ```go
37 | counter: 233608 // 结果并不确定
38 | time consume: 0.0010018 s
39 | ```
40 | 上面的程序是用100个Goroutine同时对counter进行加1计数并且每个Goroutine加了10000,因此理论上counter的最终值应该是100 0000的,但现在这个结果并不对,到底发生了什么?
41 | 其实 **counter++** 这个操作看起来很简单,但其不是一个原子操作(可简单理解为不可分割,在执行的过程中不可中断的操作,以后还会具体介绍)。下面用两个线程同时加1的情况来说明问题:
42 | 
43 | 如上图所示,如果线程1在对变量i的自增进行到step2,线程2也开始对变量i执行自增(step1),等两个线程都完成自增操作(step3)之后,变量i的值相当于只加1了一次,推广到多个线程同时进行自增的情况,结果就更无法预料,正如上面的程序结果所示。
如果我们能够保证线程2的自增操作总是在线程1完成之后才开始,那应该就不会出错了,用锁来保护自增操作就可以做到如此:
44 | ```go
45 | ...
46 | go func() {
47 | defer wg.Done()
48 | for i := 0; i < 10000; i++ {
49 | mtx.Lock() // 上锁,mtx为 sync.Mutex 的实例
50 | counter++ // 临界区
51 | mtx.Unlock() // 解锁
52 | }
53 | }()
54 | ...
55 | ```
56 | 为Goroutine中的自增操作加上锁保护之后(完整代码在[这里](https://github.com/k2huang/goapps/blob/master/firstlock/firstlock.go)):
57 | ```go
58 | counter: 1000000
59 | time consume: 0.0691851 s
60 | ```
61 | 不管运行多少次,结果总是正确的,但总的运行时间要多了许多。因此,锁在带来程序正确性的同时也带来了性能上的代价,但正确性是必要的。
62 | 从上面的示例我们可以得出锁的第一个作用:**互斥访问共享资源**,也就是保证锁所保护的区域在同一时刻只能有一个线程访问,这样可以保证对于像 counter++ 这样的非原子操作完全执行完之后,另一个线程才能继续对 共享变量counter 执行操作。
63 | 其实锁还有另外一个作用:**保证内存可见性**,这个作用比较隐晦一点。在如下代码中即使Goroutine1在执行顺序上先与Goroutine2,如果两个Goroutine被调度到不同的cpu core上执行,那 j 的值有可能还是 0,而不是100。也就是说Goroutine2执行时间就算晚于Goroutine1,也不能保证Goroutine2拿到的i的值是Goroutine1更新之后的值100.
64 | ```go
65 | // Global
66 | var i = 0
67 |
68 | // Goroutine1
69 | i = 100
70 |
71 | // Goroutine2
72 | j := i
73 | ```
74 | 这是为什么呢?先上一张示意图:
75 | 
76 | 按正常的逻辑来理解,线程1在执行完 i = 100之后(step1),内存中变量i的值更新为100(step2),然后线程2在读取变量i的值时(step3),i==100。 但现实并不会如此美好,因为线程1在执行完step1之后,可能并不会**马上**执行step2导致之后的线程2读取的i仍然是旧值。 为什么执行完 i = 100 的赋值,内存中i的值可能并不会马上被更新呢?我们的代码在从 **源码 --(编译器)--> 可执行文件 --> CPU并序执行** 的过程中,编译器和CPU并序执行并不会按照人类的意愿(既快又正确的)去运行,它们只会在遵从语言规范(或内存模型)和CPU指令集的限制的情况下,让我们的程序以越快越好的方式运行完。在这种情况下,像 i = 100 这种赋值操作,可能会被优化,将i的最新值100暂时存在CPU的寄存器中,而先不同步到内存中,因为同步到内存中更耗时嘛,等待线程1执行完或被切换出CPU执行,才被更新到内存。这时锁的第二个作用(保证内存可见性)要发挥作用了。伪代码如下
77 | ```go
78 | // 线程1
79 | lock
80 | i = 100
81 | unlock
82 |
83 | // 线程2
84 | lock
85 | j = i
86 | unlock
87 | ```
88 | 在对变量i进行读写之前,先用锁保护起来,这样可以保证 **如果对变量的写操作发生在读操作之前, 读线程可以 看见 写线程对变量内存的更新。**。
89 |
90 | #### 多把锁与死锁问题
91 | 上面讲了锁的两个作用,现在来说说使用锁的另一个令人头疼的问题:既然锁这么有用,那我们只要涉及并发共享资源的问题就用锁来解决不就好了,说是可以这么说,但使用的方式还是有讲究的,不然将带来 死锁 问题。
92 | 考虑如下生活场景:现在有两个玩具A和B, 小红与小明都想要**同时**玩两个玩具,于是两人开始争抢了起来..., 用代码模拟(完整代码在[这里](https://github.com/k2huang/goapps/blob/master/deadlock/deadlock.go))如下:
93 | ```go
94 | var (
95 | lockA sync.Mutex
96 | toyA = "toyA"
97 | )
98 |
99 | var (
100 | lockB sync.Mutex
101 | toyB = "toyB"
102 | )
103 |
104 | go func() { //小红
105 | fmt.Println("小红抢:", toyA)
106 | lockA.Lock()
107 | {
108 | fmt.Println("小红拿到:", toyA)
109 | time.Sleep(100 * time.Millisecond) //模拟业务
110 | {
111 | fmt.Println("小红抢", toyB)
112 | lockB.Lock()
113 | {
114 | fmt.Println("小红拿到:", toyB)
115 | time.Sleep(100 * time.Millisecond) //模拟业务
116 | }
117 | lockB.Unlock()
118 | }
119 | }
120 | lockA.Unlock()
121 | }()
122 |
123 | go func() { //小明
124 | fmt.Println("小明抢:", toyB)
125 | lockB.Lock()
126 | {
127 | fmt.Println("小明拿到:", toyB)
128 | time.Sleep(100 * time.Millisecond) //模拟业务
129 | {
130 | fmt.Println("小明抢", toyA)
131 | lockA.Lock()
132 | {
133 | fmt.Println("小明拿到:", toyA)
134 | time.Sleep(100 * time.Millisecond) //模拟业务
135 | }
136 | lockA.Unlock()
137 | }
138 | }
139 | lockB.Unlock()
140 | }()
141 | ```
142 | 如果小红在抢到了玩具A的同时,小明抢到了玩具B,那两个人就会僵持住,谁都无法在不放弃自己的玩具的情况下得到另一方的玩具(由于两人都想同时拥有两个玩具)。以上代码运行结果如下:
143 | ```go
144 | 小明抢: toyB
145 | 小明拿到: toyB
146 | 小红抢: toyA
147 | 小红拿到: toyA
148 | 小红抢 toyB
149 | 小明抢 toyA
150 | fatal error: all goroutines are asleep - deadlock!
151 | ```
152 | 这种情况就是所谓的“死锁”问题。
从上面这个例子可以看出:当有多个不同的共享资源时,我们通常需要使用和共享资源同等数量的锁来一对一的保护共享资源。如果多个线程在访问共享资源的时候没有按照合理的方法,就有可能会造成死锁的问题。
153 | 那有什么方法可以避免死锁呢?
154 | 1. 资源排序
155 | 就是多个线程对共享资源的访问按照事先规定的顺序,而不是不同线程可以用不同的顺序访问。如上述小红与小明的问题,如果事先约定都是按照 从玩具A到玩具B的顺序 争抢,问题自然解决。关键代码如下:
156 | ```go
157 | go func() { //小明 - 与小红一样: 按照 A->B 的顺序抢玩具
158 | fmt.Println("小明抢:", toyA)
159 | lockA.Lock()
160 | {
161 | fmt.Println("小明拿到:", toyA)
162 | time.Sleep(100 * time.Millisecond) //模拟业务
163 | {
164 | fmt.Println("小明抢", toyB)
165 | lockB.Lock()
166 | {
167 | fmt.Println("小明拿到:", toyB)
168 | time.Sleep(100 * time.Millisecond) //模拟业务
169 | }
170 | lockB.Unlock()
171 | }
172 | }
173 | lockA.Unlock()
174 | }()
175 | ```
176 | 运行结果:
177 | ```
178 | 小明抢: toyA
179 | 小明拿到: toyA
180 | 小红抢: toyA
181 | 小明抢 toyB
182 | 小明拿到: toyB
183 | 小红拿到: toyA
184 | 小红抢 toyB
185 | 小红拿到: toyB
186 | ```
187 | 该方法简单易理解,虽然对性能有不小的性能,但为了保证程序的正确性也是没有办法的事。如果遇到多把锁问题,应优先考虑该方法。
188 |
189 | 2. 试锁定-回退
190 | 这种方法需要标准库的锁的API支持trylock方法,调用trylock会立即返回,如果获取锁成功就返回true, 否则返回false。如java中的Lock.trylock(), linux pthread_mutex_trylock, C++11 std::timed_mutex::try_lock等等。很可惜Go语言现在的标准库并不支持该方法,但不代表不能自己去实现,如果你感兴趣可以[参考这篇文章](http://colobu.com/2017/03/09/implement-TryLock-in-Go/)。
191 | **试锁定-回退** 方法的核心思想是,如果在执行一个代码块的时候,需要先后(顺序不定)锁定两把锁,那么在成功锁定其中一把锁之后应该使用**试锁定的方法来锁定另一把锁**。如果试锁定第二把锁失败,就把已锁定的第一把锁解开,并重新对这两把锁进行锁定和试锁定。
192 | 该方法也不算难理解,但实践起来比较复杂,尤其当锁的数量多于两把时,而且这种方法可以会出现另一个问题 - **活锁**。还拿以上小红与小明的例子来说,如果小红每次都是先抢到玩具A,同时小明抢到了玩具B,然后两人在争抢对方的玩具失败之后,都回退重试..., 就这样在失败,回退和重试之前重复N次,当然每次都这么巧也是不太可能的,因此活锁是有机会自己恢复正常的,但死锁是不可能的。
193 |
194 | #### 锁的公平性
195 | 当多个线程同时抢占同一把互斥锁时,只有一个线程可以成功,剩下的线程都要进入锁的同步队列中排队,**公平性锁**会保证同步队列中的线程按照FIFO的方式去获取到锁,且**新加入争抢的线程**发现锁的同步队列中有等待线程,就只能将自己放入队尾按先来后到来排队。而**非公平性锁**,新加入争抢的线程 将与同步队列队首等待获取锁的线程站在同一起跑线上争夺锁。因此非公平锁容易造成锁的同步队列中的争抢线程“饥饿”(如新加入争抢的线程每次都比同步队列队首线程先抢到锁)的情况,那为什么还需要这种锁呢?
196 | 答案是**非公平锁**可以减少线程做上下文切换的机会,从而提高性能。But how? 如果我们把每次不同线程获取到锁定义为一次切换,公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。**非公平性锁**允许新加入争抢的线程与同步队列队首等待获取锁的线程站在同一起跑线上争夺锁,如果新加入争抢的线程是刚刚释放锁的线程,其获取锁的机会将很大,如果其再次获取锁成功,就不需要进行上下切换了。因此非公平性锁虽然可能造成线程“饥饿”,但减少了线程切换,保证了其更大的吞吐量。
197 | 而Go语言的sync.Mutex实现上更偏向非公平性:如果某一个Goroutine在获取锁失败之后,通常会先自旋一小段时间,如果还是无法获取成功才进入锁的同步队列排队等待(具体实现源码可以参考[这篇文章](http://legendtkl.com/2016/10/23/golang-mutex/))。这样某一个goroutine在释放锁之后马上又去获取锁成功的机会很大。以下是一段[试验代码](https://github.com/k2huang/goapps/blob/master/unfairlock/unfairlock.go)以及某一次运行结果:
198 | ```go
199 | var m sync.Mutex
200 | log.SetFlags(log.Lmicroseconds)
201 |
202 | go func() {
203 | log.Println("1, start to lock...")
204 | m.Lock()
205 | log.Println("1, locked")
206 | m.Unlock()
207 | log.Println("1, unlocked")
208 | }()
209 |
210 | go func() {
211 | log.Println("2, start to lock...")
212 | m.Lock()
213 | log.Println("2, locked")
214 | m.Unlock()
215 | log.Println("2, unlocked")
216 | }()
217 |
218 | log.Println("main, start to lock...")
219 | m.Lock()
220 | log.Println("main, locked")
221 | time.Sleep(3 * time.Millisecond)
222 | m.Unlock()
223 | log.Println("main, unlocked")
224 |
225 | log.Println("main, start to lock again...")
226 | m.Lock()
227 | log.Println("main, locked again")
228 | m.Unlock()
229 | log.Println("main, unlocked again")
230 |
231 | time.Sleep(time.Second)
232 | log.Println("main, end")
233 | ```
234 | ```
235 | 23:35:43.547841 main, start to lock...
236 | 23:35:43.560877 main, locked
237 | 23:35:43.547841 1, start to lock...
238 | 23:35:43.547841 2, start to lock...
239 | 23:35:43.563913 main, unlocked
240 | 23:35:43.563913 main, start to lock again...
241 | 23:35:43.563913 main, locked again
242 | 23:35:43.563913 main, unlocked again
243 | 23:35:43.563913 1, locked
244 | 23:35:43.563913 1, unlocked
245 | 23:35:43.563913 2, locked
246 | 23:35:43.563913 2, unlocked
247 | 23:35:44.564335 main, end
248 | ```
249 | 从结果上看,main goroutine在先获取到锁之后,goroutine 1,2获取锁时被阻塞,然后main goroutine在释放锁之后马上又去抢占锁,又比goroutine 1,2先获取到了锁。
250 |
251 | #### 使用锁的一些建议
252 | 1.多个线程会并发访问同一份共享资源,并且存在写操作时,一定要记住用锁来保护共享资源。通常不同的共享资源用不同的锁来保护,多把锁要注意死锁问题。
253 | 2.由于使用锁会降低并发的性能,所以应尽量减少锁的作用域(lock与unlock之间的区域),即锁只是用来保护共享资源的,不要将不相关的部分放在锁的作用域中。
254 | 3.多个线程对共享资源的访问,在读多写少情况下考虑用读写锁(如Go标准库中的sync.RWMutex)来提高性能。
255 | 4.尽量不要在持有锁(已抢占到某一共享资源)的同时,去访问其他共享资源(也即拿着一把锁的时候又去试图抢另一把锁,容易造成死锁)。当然有时会不太好避免,这时可以考虑对资源排序,即多个线程都按照统一的顺序访问共享资源。
256 | 5.由于Go语言不支持**可重入锁**(也称递归锁),要避免同一个goroutine在unlock之前多次lock同一把锁,不然该goroutine将无法继续运行与也无法退出(goroutine泄漏)。
257 | 6.获取锁的API除了lock, 还有trylock和timedlock(如linux pthread_mutex_timedlock,java Lock.trylock的重载版本,调用方可以指定为了获取锁而等待的时间,如果超时还未获取到锁就返回false,而不像lock方法死等到底),在不同的场景可以考虑使用不同的API。很可惜Go语言标准库又不支持,可能是想让我们多考虑用channel(后续会单独写一篇介绍Go channel的文章)!
258 |
259 | #### 经典问题:哲学家进餐问题
260 | 
261 | 1. 问题描述
262 |
263 | 场景:5个哲学家,5把叉子,5盘意大利面(意大利面很滑,需要同时用两把叉子才能拿起)大家围绕桌子,进行思考与进餐的活动。
264 |
265 | 哲学家的活动方式为:要么放下左右手刀叉进行思考,要么拿起刀叉开始进餐(刀叉拿起时,必须拿两把,而且只能左右手依次拿,先左手拿左边,后右手拿右边,或者先右手拿右边,左边拿左边)。且只有这两种交替状态。
266 |
267 | 2. 问题分析
268 |
269 | 如果5个哲学家某一时刻恰好赶上同时想进餐,于是都同时拿起了右(或左)手边的叉子,那将导致5个人都无法拿到左(或右)手边的叉子并进餐,也就是发生了“死锁”问题。在这篇文章中,我先用上面讲到的资源排序的方法解决这个问题。后续文章还会介绍其他解法。
270 |
271 | 3. 代码实现
272 |
273 | [有问题版本代码](https://github.com/k2huang/goapps/blob/master/philosopher_problem_version/philosopher.go) - 模拟问题,关键代码如下:
274 | ```go
275 | for i := 0; i < 5; i++ {
276 | //所有哲学家要拿起的第一把叉子正好是左手边的,
277 | //要拿起的第二把叉子正好是右手边的
278 | philosophers[i] = New(fmt.Sprintf("P%d", i), forks[i], forks[(i+1)%5])
279 |
280 | wg.Add(1)
281 | go func(i int) {
282 | defer wg.Done()
283 |
284 | philosophers[i].ThinkOrEat()
285 | }(i)
286 | }
287 | ```
288 | 结合代码与上图标记的序号可以看出,若所有哲学家同一时刻拿起第一把叉子(都是左手边的),将出现死锁问题。
289 | [资源排序版本代码](https://github.com/k2huang/goapps/blob/master/philosopher_resource_sort/philosopher.go) - 我们还是将5把叉子按上图所示排一个全局ID号,并要求哲学家拿起的第一把叉子的id号必须比第二把小,即抢占资源的顺序是按一个全局的资源排列来进行的。关键代码如下:
290 | ```go
291 | func New(name string, first, second *Fork) *Philosopher {
292 | p := &Philosopher{name: name}
293 |
294 | //资源排序关键代码:
295 | //保证第一把要拿起的叉子必须是id较小的
296 | if first.ID < second.ID {
297 | p.first = first
298 | p.second = second
299 | } else {
300 | p.first = second
301 | p.second = first
302 | }
303 |
304 | return p
305 | }
306 | ```
307 | 此时当P0-P3 4位哲学家在都拿起左手边的叉子(0-3)的同时, 哲学家P4只能等待而不是去先拿叉子4,然后哲学家P3就有机会拿到第二把叉子(4)开始进餐了。
308 |
309 |
310 | #### 参考
311 | Go并发编程实战 第2版
312 | 七周七并发模型
313 | Java并发编程的艺术
314 |
--------------------------------------------------------------------------------
/golang/并发编程/并发机制/Go并发机制.md:
--------------------------------------------------------------------------------
1 | ### 1. C/C++ 与 Go语言的“价值观”对照
2 | 之前看过 白明老师 在GopherChina2017的一篇演讲文章[《Go coding in go way》](http://tonybai.com/2017/04/20/go-coding-in-go-way/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io),里面提到C/C++/Go三门语言价值观,感觉很有意思,分享给大家感受一下:
3 |
4 | C的价值观摘录
5 | - 相信程序员:提供指针和指针运算,让C程序员天马行空的发挥
6 | - 自己动手,丰衣足食:提供一个很小的标准库,其余的让程序员自造
7 | - 保持语言的短小和简单
8 | - 性能优先
9 |
10 | C++价值观摘录
11 | - 支持多范式,不强迫程序员使用某个特定的范式
12 | - 不求完美,但求实用(并且立即可用)
13 |
14 | Go价值观
15 | - Overall Simplicity 全面的简单
16 | - Orthogonal Composition 正交组合
17 | - Preference in Concurrency 偏好并发
18 |
19 | 用一句话概括Go的价值观:
20 | Go is about orthogonal composition of simple concepts with preference in concurrency(Go是在偏好并发的环境下的简单概念/事物的正交组合).
21 |
22 | 从Go的价值观介绍可以看出 Go很适合并发编程,可以说其是为并发而生的一门语言,那它的并发机制如何?这正是这篇文章想要介绍的。
23 |
24 | ### 2. 从线程实现模型说起
25 | 线程的实现模型主要有3种:[内核级线程模型、用户级线程模型和混合型线程模型](https://en.wikipedia.org/wiki/Thread_(computing)#Models)。它们之间最大的区别在于线程与内核调度实体KSE(Kernel Scheduling Entity)之间的对应关系上。所谓的内核调度实体KSE 就是指可以被操作系统内核调度器调度的对象实体,有些地方也称其为**内核级线程**,是操作系统内核的最小调度单元。
26 |
27 | #### 2.1 内核级线程模型
28 | 用户线程与KSE是1对1关系(1:1)。大部分编程语言的线程库(如linux的pthread,Java的java.lang.Thread,C++11的std::thread等等)都是对操作系统的线程(内核级线程)的一层封装,创建出来的每个线程与一个不同的KSE静态关联,因此其调度完全由OS调度器来做。这种方式实现简单,直接借助OS提供的线程能力,并且不同用户线程之间一般也不会相互影响。但其创建,销毁以及多个线程之间的上下文切换等操作都是直接由OS层面亲自来做,在需要使用大量线程的场景下对OS的性能影响会很大。
29 |
30 | #### 2.2 用户级线程模型
31 | 用户线程与KSE是多对1关系(M:1),这种线程的创建,销毁以及多个线程之间的协调等操作都是由用户自己实现的线程库来负责,对OS内核透明,一个进程中所有创建的线程都与同一个KSE在运行时动态关联。现在有许多语言实现的 **协程** 基本上都属于这种方式。这种实现方式相比内核级线程可以做的很轻量级,对系统资源的消耗会小很多,因此可以创建的数量与上下文切换所花费的代价也会小得多。但该模型有个致命的缺点,如果我们在某个用户线程上调用阻塞式系统调用(如用阻塞方式read网络IO),那么一旦KSE因阻塞被内核调度出CPU的话,剩下的所有对应的用户线程全都会变为阻塞状态(整个进程挂起)。
32 | 所以这些语言的**协程库**会把自己一些阻塞的操作重新封装为完全的非阻塞形式,然后在以前要阻塞的点上,主动让出自己,并通过某种方式通知或唤醒其他待执行的用户线程在该KSE上运行,从而避免了内核调度器由于KSE阻塞而做上下文切换,这样整个进程也不会被阻塞了。
33 |
34 | #### 2.3 混合型线程模型
35 | 用户线程与KSE是多对多关系(M:N), 这种实现综合了前两种模型的优点,为一个进程中创建多个KSE,并且线程可以与不同的KSE在运行时进行动态关联,当某个KSE由于其上工作的线程的阻塞操作被内核调度出CPU时,当前与其关联的其余用户线程可以重新与其他KSE建立关联关系。当然这种动态关联机制的实现很复杂,也需要用户自己去实现,这算是它的一个缺点吧。Go语言中的并发就是使用的这种实现方式,Go为了实现该模型自己实现了一个运行时调度器来负责Go中的"线程"与KSE的动态关联。此模型有时也被称为 **两级线程模型**,**即用户调度器实现用户线程到KSE的“调度”,内核调度器实现KSE到CPU上的调度**。
36 |
37 | 三种模型的示意图如下:
38 | 
39 | ### 3. Go并发调度: G-P-M模型
40 | #### 3.1 G-P-M模型
41 | 有了上面的认识,我们可以开始真正的介绍Go的并发机制了,先用一段代码展示一下在Go语言中新建一个“线程”(Go语言中称为Goroutine)的样子:
42 | ```go
43 | // 用go关键字加上一个函数(这里用了匿名函数)
44 | // 调用就做到了在一个新的“线程”并发执行任务
45 | go func() {
46 | // do something in one new goroutine
47 | }()
48 | ```
49 | 功能上等价于Java8的代码:
50 | ```java
51 | new java.lang.Thread(() -> {
52 | // do something in one new thread
53 | }).start();
54 | ```
55 | 可以看到Go的并发用起来非常简单,用了一个语法糖将内部复杂的实现结结实实的包装了起来。其内部可以用下面这张图来概述:
56 | 
57 | 其图中的G, P和M都是Go语言运行时系统(其中包括内存分配器,并发调度器,垃圾收集器等组件,可以想象为Java中的JVM)抽象出来概念和数据结构对象:
58 | G:Goroutine的简称,上面用go关键字加函数调用的代码就是创建了一个G对象,是对一个要并发执行的任务的封装,也可以称作用户态线程。属于用户级资源,对OS透明,具备轻量级,可以大量创建,上下文切换成本低等特点。
59 | M:Machine的简称,在linux平台上是用clone系统调用创建的,其与用linux pthread库创建出来的线程本质上是一样的,都是利用系统调用创建出来的OS线程实体。M的作用就是执行G中包装的并发任务。**Go运行时系统中的调度器的主要职责就是将G公平合理的安排到多个M上去执行**。其属于OS资源,可创建的数量上也受限了OS,通常情况下G的数量都多于活跃的M的。
60 | P:Processor的简称,逻辑处理器,主要作用是管理G对象(每个P都有一个G队列),并为G在M上的运行提供本地化资源。
61 | 从2.3节介绍的两级线程模型来看,似乎并不需要P的参与,有G和M就可以了,那为什么要加入P这个东东呢?
62 | 其实Go语言运行时系统早期(Go1.0)的实现中并没有P的概念,Go中的调度器直接将G分配到合适的M上运行。但这样带来了很多问题,例如,不同的G在不同的M上并发运行时可能都需向系统申请资源(如堆内存),由于资源是全局的,将会由于资源竞争造成很多系统性能损耗,为了解决类似的问题,后面的Go(Go1.1)运行时系统加入了P,让P去管理G对象,M要想运行G必须先与一个P绑定,然后才能运行该P管理的G。这样带来的好处是,我们可以在P对象中预先申请一些系统资源(本地资源),G需要的时候先向自己的本地P申请(无需锁保护),如果不够用或没有再向全局申请,而且从全局拿的时候会多拿一部分,以供后面高效的使用。就像现在我们去政府办事情一样,先去本地政府看能搞定不,如果搞不定再去中央,从而提供办事效率。
63 | 而且由于P解耦了G和M对象,这样即使M由于被其上正在运行的G阻塞住,其余与该M关联的G也可以随着P一起迁移到别的活跃的M上继续运行,从而让G总能及时找到M并运行自己,从而提高系统的并发能力。
64 | Go运行时系统通过构造G-P-M对象模型实现了一套用户态的并发调度系统,可以自己管理和调度自己的并发任务,所以可以说Go语言**原生支持并发**。**自己实现的调度器负责将并发任务分配到不同的内核线程上运行,然后内核调度器接管内核线程在CPU上的执行与调度。**
65 |
66 | #### 3.2 调度过程
67 | Go运行时完整的调度系统是很复杂,很难用一篇文章描述的清楚,这里只能从宏观上介绍一下,让大家有个整体的认识。
68 | ```go
69 | // Goroutine1
70 | func task1() {
71 | go task2()
72 | go task3()
73 | }
74 | ```
75 | 假如我们有一个G(Goroutine1)已经通过P被安排到了一个M上正在执行,在Goroutine1执行的过程中我们又创建两个G,这两个G会被马上放入与Goroutine1相同的P的本地G任务队列中,排队等待与该P绑定的M的执行,这是最基本的结构,很好理解。 关键问题是:
76 | **a.如何在一个多核心系统上尽量合理分配G到多个M上运行,充分利用多核,提高并发能力呢?**
如果我们在一个Goroutine中通过**go**关键字创建了大量G,这些G虽然暂时会被放在同一个队列, 但如果这时还有空闲P(系统内P的数量默认等于系统cpu核心数),Go运行时系统始终能保证至少有一个(通常也只有一个)活跃的M与空闲P绑定去各种G队列去寻找可运行的G任务,该种M称为**自旋的M**。一般寻找顺序为:自己绑定的P的队列,全局队列,然后其他P队列。如果自己P队列找到就拿出来开始运行,否则去全局队列看看,由于全局队列需要锁保护,如果里面有很多任务,会转移一批到本地P队列中,避免每次都去竞争锁。如果全局队列还是没有,就要开始玩狠的了,直接从其他P队列偷任务了(偷一半任务回来)。这样就保证了在还有可运行的G任务的情况下,总有与CPU核心数相等的M+P组合 在执行G任务或在执行G的路上(寻找G任务)。
77 | **b. 如果某个M在执行G的过程中被G中的系统调用阻塞了,怎么办?**
78 | 在这种情况下,这个M将会被内核调度器调度出CPU并处于阻塞状态,与该M关联的其他G就没有办法继续执行了,但Go运行时系统的一个监控线程(sysmon线程)能探测到这样的M,并把与该M绑定的P剥离,寻找其他空闲或新建M接管该P,然后继续运行其中的G,大致过程如下图所示。然后等到该M从阻塞状态恢复,需要重新找一个空闲P来继续执行原来的G,如果这时系统正好没有空闲的P,就把原来的G放到全局队列当中,等待其他M+P组合发掘并执行。
79 | 
80 | **c. 如果某一个G在M运行时间过长,有没有办法做抢占式调度,让该M上的其他G获得一定的运行时间,以保证调度系统的公平性?**
81 | 我们知道linux的内核调度器主要是基于时间片和优先级做调度的。对于相同优先级的线程,内核调度器会尽量保证每个线程都能获得一定的执行时间。为了防止有些线程"饿死"的情况,内核调度器会发起抢占式调度将长期运行的线程中断并让出CPU资源,让其他线程获得执行机会。当然在Go的运行时调度器中也有类似的抢占机制,但并不能保证抢占能成功,因为Go运行时系统并没有内核调度器的中断能力,它只能通过向运行时间过长的G中设置抢占flag的方法温柔的让运行的G自己主动让出M的执行权。
82 | 说到这里就不得不提一下Goroutine在运行过程中可以动态扩展自己线程栈的能力,可以从初始的2KB大小扩展到最大1G(64bit系统上),因此在每次调用函数之前需要先计算该函数调用需要的栈空间大小,然后按需扩展(超过最大值将导致运行时异常)。Go抢占式调度的机制就是利用在判断要不要扩栈的时候顺便查看以下自己的抢占flag,决定是否继续执行,还是让出自己。
83 | 运行时系统的监控线程会计时并设置抢占flag到运行时间过长的G,然后G在有函数调用的时候会检查该抢占flag,如果已设置就将自己放入全局队列,这样该M上关联的其他G就有机会执行了。但如果正在执行的G是个很耗时的操作且没有任何函数调用(如只是for循环中的计算操作),即使抢占flag已经被设置,该G还是将一直霸占着当前M直到执行完自己的任务。
84 |
85 | ### 4. Goroutine与Channel: 锁之外的另一种同步机制
86 | 在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等。Go语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言也差不多。
87 | 除了这些基本的同步手段,Go语言还提供了一种新的同步机制: Channel,它在Go语言中是一个像int, float32等的基本类型,一个channel可以认为是一个能够在多个Goroutine之间传递某一类型的数据的管道。Go中的channel无论是实现机制还是使用场景都和Java中的BlockingQueue很接近。
88 | **使用方式**
89 | ```go
90 | // 声明channel变量
91 | var syncChan = make(chan int) // 无缓冲channel,主要用于两个Goroutine之间建立同步点
92 | var cacheChan = make(chan int, 10) // 缓冲channel
93 | // 向channel中写入数据
94 | syncChan <- 1
95 | cacheChan <- 1
96 | // 从channel读取数据
97 | var i = <-syncChan
98 | var j = <-cacheChan
99 | ```
100 | 几乎等价于的Java中的操作:
101 | ```java
102 | TransferQueue syncQueue = new LinkedTransferQueue();
103 | BlockingQueue cacheQueue = new ArrayBlockingQueue(10);
104 |
105 | syncQueue.transfer(1);
106 | cacheQueue.put(1);
107 |
108 | int i = syncQueue.take();
109 | int j = cacheQueu.take();
110 | ```
111 | **使用场景**
112 | a. 与Java的BlockingQueue一样用在需要生产者消费者模型的并发环境中。
113 | b. 锁同步场景下一种替代方案。在Go的并发编程中有一句很经典的话:[不要以共享内存的方式去通信,而要以通信的方式去共享内存](https://golang.org/doc/effective_go.html#concurrency)。在Go语言中并不鼓励用锁保护共享状态的方式在不同的Goroutine中分享信息(以共享内存的方式去通信)。而是鼓励通过channel将共享状态或共享状态的变化在各个Goroutine之间传递(以通信的方式去共享内存),这样同样能像用锁一样保证在同一的时间只有一个Goroutine访问共享状态。但这的确需要转换以前用锁做并发同步的思维方式,大家觉得那种适合自己和自己的使用场景就用哪种好了,并不能很简单、绝对地说哪种方式更好,更高效。
114 |
115 | ### 5. Go语言对网络IO的优化
116 | 在谈论高性能网络IO编程时,我们几乎都离不开epoll/kqueue/iocp等技术术语了,如Java最新的的NIO,Node等等的高性能网络IO模型都是基于这些技术实现的。诞生于21世纪,有**互联网时代的C语言**之称的Go语言,这么重视高并发,当然不会放过对网络的优化。且Go语言中对网络IO的优化很巧妙,让你可以用和以前一样的(同步的)思维方式去编程的同时(而不是反人类的异步方式),还能享受到与异步方式几乎同等高效的运行性能。那Go语言中是如何做的呢?主要是从两方面下手的:
117 | a. 将标准库中的网络库全部封装为非阻塞形式,防止其阻塞底层的M并导致内核调度器切换上下文带来的系统开销。
118 | b. 运行时系统加入epoll机制(针对Linux系统),当某一个Goroutine在进行网络IO操作时,如果网络IO未就绪,就将其该Goroutine封装一下,放入epoll的等待队列中,当前G挂起,与其关联的M可以继续运行其他G。当相应的网络IO就绪后,Go运行时系统会将等待网络IO就绪的G从epoll就绪队列中取出(主要在两个地方从epoll中获取已网络IO就绪的G列表,一是sysmon监控线程中,二是自旋的M中),再由调度器将它们像普通的G一样分配给各个M去执行。
119 | Go语言将高性能网络IO的实现方式直接集成到了Go本身的运行时系统中,与Go的并发调度系统协同高效的工作,让开发人员可以简单,高效地进行网络编程。
120 |
121 | ### 6. 总结
122 | Go语言并不完美,它是[以软件工程为目的的语言设计](http://blog.jobbole.com/36480/)。其实现的并发机制也并不是什么革新的技术,只是将这些经典的理论和技术以一种简洁高效的方式组合了起来,并用简单抽象的API或语法糖开放给开发人员,着实减轻了开发人员编程的心智负担。而且其通过引入channel机制,将另一种并发编程模型(CSP: 通信顺序进程)带给了我们,给我们提供了使用其他并发编程思维方式的机会(有关CSP模型建议大家看看《七周七并发模型》这本书的第六章),Goroutine与Channel的组合是一对很有powerful的并发工具,相信其可以给你带了更好的并发编程体验。
123 |
124 | ### 7. 参考
125 | 《Go并发编程实战》 第2版
126 | 《Go语言学习笔记》
127 | [也谈goroutine调度器](http://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/)
128 | [Go coding in go way](http://tonybai.com/2017/04/20/go-coding-in-go-way/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io)
129 |
--------------------------------------------------------------------------------
/golang/并发编程/并发机制/imgs/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k2huang/blogpost/5225b76a86f855088cd7c40c44fde9949159c8f8/golang/并发编程/并发机制/imgs/1.png
--------------------------------------------------------------------------------
/golang/并发编程/并发机制/imgs/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k2huang/blogpost/5225b76a86f855088cd7c40c44fde9949159c8f8/golang/并发编程/并发机制/imgs/2.png
--------------------------------------------------------------------------------
/golang/并发编程/并发机制/imgs/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k2huang/blogpost/5225b76a86f855088cd7c40c44fde9949159c8f8/golang/并发编程/并发机制/imgs/3.jpg
--------------------------------------------------------------------------------
/golang/应用程序/用Go和FUSE自己的文件系统/README.md:
--------------------------------------------------------------------------------
1 |
2 | ### 1. 前言
3 | 当我们在linux系统上访问磁盘上的一个文件时,要经历 open -> read/write -> close 流程,其中涉及的操作都是通过linux内核提供的**系统调用**来触发,并最终访问到磁盘上的文件。从linux文件系统的角度,我们的访问过程要经历 系统调用 --> 虚拟文件系统(VFS) --> 真实文件系统(如ext4,NFS...), 这也是linux文件系统主要的3大块。3大块中需要特别拿出来说说的就是VFS了,linux内核为了支持多种文件系统,如NFS,ext4..., 抽象出来了一层VFS,然后通过VFS来管理真实的文件系统,这样我们就可以用同样的方式访问不同的文件系统。如下图:
4 | 如果现在我们想实现一套自己的文件系统,按传统的做法,我们要与内核(VFS)打交道,然而对于应用程序开发的我们来说,那也忒可怕了,谁想去编译与调试内核呀!幸运的是现在我们可以用FUSE(filesytem in userspace)来实现, 编写FUSE文件系统时,只需要内核加载了fuse内核模块即可(自己单独安装),不需要重新编译内核。
5 |
6 | ### 2. FUSE概述
7 | 用户空间文件系统(Filesystem in Userspace,简称FUSE)是操作系统中的概念,指完全在用户态实现的文件系统。目前Linux通过内核模块对此进行支持。一些文件系统如ZFS,glusterfs和luster使用FUSE实现。Linux用于支持用户空间文件系统的内核模块名叫FUSE,FUSE一词有时特指Linux下的用户空间文件系统。Linux从2.6.14版本开始通过FUSE模块支持在用户空间实现文件系统。
8 | 在用户空间实现文件系统能够大幅提高生产率,简化了为操作系统提供新的文件系统的工作量,特别适用于各种虚拟文件系统和网络文件系统。上述ZFS和 glusterfs都属于网络文件系统。但是,在用户态实现文件系统必然会引入额外的内核态/用户态切换带来的开销,对性能会产生一定影响。
9 | 目前Linux,FreeBSD,NetBSD,OpenSolaris和Mac OSX支持用户空间态文件系统。
10 |
11 | ### 3. FUSE模块组成与工作原理
12 | 
13 | 如上图所示,FUSE由三大模块组成:1. FUSE内核模块,主要实现VFS接口。2. libfuse用户库,主要提供负责与FUSE内核模块交互的功能。3.用户实现的文件系统,这个就是我们要自己开发实现的部分,当然是基于libfuse库来实现。
14 | 假如现在用户访问的是磁盘文件,整个过程走的就是左边的绿色路线:**User -> [glibc ->] VFS -> EXT4驱动 -> 磁盘文件数据**, 然后依次返回。如果用户访问的是我门利用FUSE实现的用户态文件系统, 那整个过程走的就是右边的蓝色路线: **User -> [glibc ->] VFS -> FUSE内核模块(或驱动) -> [glibc ->] libfuse -> Userfileystem**, 然后依次返回。可以看到,通过FUSE内核模块,把用户访问文件系统的请求 **“转发”** 到与FUSE内核模块建立了**连接**的用户态程序,**因此我们通过FUSE实现用户态文件系统的过程:就是借助libfuse库,与FUSE内核模块建立连接并处理来自内核模块的请求的过程。用户的open/read/wirte/close等操作,经过VFS以及FUSE内核模块之后,会以request/response的形式与我们实现的用户态文件系统程序交互。**
15 |
16 | ### 4. 实例:用Go语言以及FUSE实现一个mirrorfs
17 | 要想使用FUSE开发自己的文件系统,通过第3部分的介绍我们知道,我们首先需要给我们的linux系统安装FUSE内核模块(自己解决吧),然后借助 [libfuse用户库](https://github.com/libfuse/libfuse)(这个是C库)来实现。由于我想使用Go语言来开发,我选择了 [**bazil.org/fuse**](https://bazil.org/fuse/), 它其实算是一个纯用Go语言实现的框架,将上面提到的FUSE三大模块中的2&3中的功能糅合到了一起,我们只需要实现一些指定的接口,并将实现注入到框架中,就可以轻松实现自己的文件系统。
18 | 这里需要特别强调一点,bazil.org/fuse是完全用Go实现的,而不是用cgo封装了libfuse的功能,那前面也说了,用户态文件系统与FUSE内核模块交互一般是借助libfuse,而这个框架为什么可以不使用libfuse? 其实FUSE内核模块与我们实现的用户态文件系统以request/reaponse形式的交互是通过**FUSE协议**来进行的,只要bazil.org/fuse实现了该协议,我们就可以直接用FUSE内核模块交互,而不需要再通过libfuse。
19 |
20 | #### 4.1 bazil.org/fuse的使用说明
21 | 使用该库(或框架)的时候,主要参考看库作者自己写的一篇[文章](https://blog.gopheracademy.com/advent-2014/fuse-zipfs/)(看懂它之后基本上就学会了如何使用了)和[API doc](https://godoc.org/bazil.org/fuse)即可。
22 | bazil.org/fuse库主要包括两个部分,底层一点的实现在bazil.org/fuse包中,上层的实现bazil.org/fuse/fs包中(我们要实现的接口就在这里).
23 | 我这里大概说一下使用流程:
24 | 1). 使用fuse.Mount函数与FUSE内核模块建立接连,这个连接很重要,后面所有交互的request/response都是通过它。fuse.Mount需要我们提供一个入参:挂载点目录,其实就是为了将我们实现的文件系统挂在到该目录下,然后用户访问该目录(其实就是我们文件系统的根目录)就是在访问我们实现的文件系统。
25 | 2). 新建一个fs.Server,并将1)中的连接 和 我们实现的根文件系统(稍后再讲) 注入, 然后运行。
26 | 然后我们对挂载点目录下任何文件的create/open/read/write/close等操作的执行,都会以相应的request请求的到达我们的fs.Server,fs.Server根据request的类型及携带的参数,调用我们注入的文件系统所实现的接口的方法,并将结果以response的形式返回给内核,进而返回给用户,这样以后一个用户访问我们文件系统的操作才算执行完成。
27 |
28 | #### 4.2 bazil.org/fuse的工作原理
29 |
30 | 在4.1.2)中所说的fs.Server运行的主流程就在bazil.org/fuse/fs/serve.go中的**func (s *Server) Serve(fs FS) error**的:
31 | ```go
32 | for {
33 | //从FUSE内核模块读取用户访问文件系统操作所对应的请求
34 | //如open对应fuse.OpenRequest, write对应fuse.WriteRequest...
35 | req, err := s.conn.ReadRequest()
36 | ...
37 |
38 |
39 | go func() {
40 | ...
41 | // 新起一个goroutine来处理请求
42 | s.serve(req)
43 | }()
44 | }
45 | ```
46 | 而s.serve(req)中先是根据请求信息找到对应的node,然后执行handleRequest函数,这里只截取handleRequest的其中一小段代码看看:
47 | ```go
48 | ...
49 | case *fuse.OpenRequest: // 打开文件的请求
50 | s := &fuse.OpenResponse{}
51 | var h2 Handle
52 | if n, ok := node.(NodeOpener); ok {
53 | // 如果该node实现了NodeOpener接口,就调用其上的Open方法
54 | hh, err := n.Open(ctx, r, s)
55 | if err != nil {
56 | return err
57 | }
58 | h2 = hh
59 | } else {
60 | h2 = node
61 | }
62 | s.Handle = c.saveHandle(h2, r.Hdr().Node)
63 | done(s)
64 | r.Respond(s)
65 | return nil
66 | ...
67 | ```
68 | 其实就是根据request的类型,在相应的node结点上执行相应的操作。而该node也是我们实现的文件系统注入的,所以调用该node的Open方法,就是在调用我们自己实现的Open方法。
69 |
70 | #### 4.3 利用bazil.org/fuse实现mirrorfs的过程说明
71 | 这里我想实现的是一个 镜像文件系统(mirrorfs): 当我们以 **./progname -mount /a -mirror /b** 的方式启动我们的的用户态文件系统程序之后,我们在访问 /a目录(我们文件系统的根目录)时,我们实际上是访问的 /b目录(镜像目录)。
72 |
73 | 在开始写我们的第一个文件系统之前,先来了解一些bazil.org/fuse中基础的且重要的数据结构或接口:
74 | **fs.FS** : 用户实现的文件系统必须实现的接口,这个接口中唯一的Root方法就是为了能让fs.Server能获取注入的文件系统的根结点。
75 | **fs.Node** : 我们的文件系统中的所有节点(如文件/目录/根 结点)都必须实现的接口。在fs.Server中以node id唯一标识。
76 | **fs.Handle** : 用来表示我们文件系统中的文件结点打开之后的句柄。在fs.Server中以handle id唯一标识
77 |
78 | 在我的[实现代码](https://github.com/k2huang/mirrorfs)中,所有与文件系统相关的实现都放在了 src/mfs 目录下,[mfs.go](https://github.com/k2huang/mirrorfs/blob/master/src/mfs/mfs.go)中的 **MirrorFS结构体** 是为了维护与管理我们的根文件系统,其必须实现上面所说的fs.FS接口;而[dir.go](https://github.com/k2huang/mirrorfs/blob/master/src/mfs/dir.go)中的 **Dir结构体** 与[file.go](https://github.com/k2huang/mirrorfs/blob/master/src/mfs/file.go)中 **File结构体** 分别表示我们文件系统中的目录结点与文件结点,它们必须实现上面所说的fs.Node接口,有了这三个结构我们的文件系统已初步实现。
79 | 接下来就是要定义我们的文件系统中的每个结点可以执行的操作了,如对于目录结点,我们可以执行创建新文件(Create),新子目录(Mkdir)等操作,所以我们的 Dir结构体 可以实现fs.NodeCreater和fs.NodeMkdirer等接口。再如对于文件结点,我们可以执行打开关闭/读写等操作,所以我们可以让 File结构体 实现fs.NodeOpener/fs.HandleReleaser/fs.HandleReader/fs.HandleWriter等接口。**也就是我们实现fs.Node接口的文件系统结点需要执行什么操作,我们就实现对应接口就可以了,可以实现的接口列表在[这里](https://godoc.org/bazil.org/fuse/fs)。至于这些操作到底会做什么完全取决于我们。在mirrorfs中,我们希望对 /a目录 下的任何操作都镜像到 /b,如对/a/x/y.txt的读写操作,那我们实现的相应的接口的真实行为就是读写/b/x/y.txt文件。**
80 | 有了文件系统所需要的数据结构之后,我们是如何将这些数据结构组织起来(像Linux目录树结构一样)的呢?其实我们不用关心太多,因为fs.Server已经帮我们做了许多事情。下面以一个用户的具体操作过程来说明一下我们的mirrorfs的工作流程,以及fs.Server如何将这些数据结构组织起来的:
81 | 用户操作(伪代码)如下:
82 | ```go
83 | f = open /a/x/y.txt
84 | f.write "Hello, world"
85 | f.close
86 | ```
87 | 一开始我们注册给fs.Server的文件系统只有一个根结点(Dir结构体实例, node id == 1), 也就是fs.Server从我们注册的文件系统的Root()方法那里得到的结点。
88 | ```go
89 | // src/main.go
90 | srv := fs.New(c, cfg)
91 | filesys := mfs.NewMirrorFS(*mirror)
92 |
93 | if err := srv.Serve(filesys); err != nil {
94 | log.Fatal(err)
95 | }
96 | ```
97 | 现在用户要操作的是/a/x/y.txt,我们必须从文件系统中找到对应的fs.Node,然后得到open之后的fs.Handle,之后才能写数据。
98 | 讲到这里就不得不提一下linux/unix文件系统中的inode(index node),简单来说inode就是内核用来管理文件或目录等结点的元数据(如访问权限)和实际数据内容的数据结构, 在内核中用inode id来唯一标识一个文件或目录结点。当我们在访问一个文件结点(目录,文件等) 时,我们给系统调用函数的是一个字符串的文件名,当操作执行到内核时,内核需要通过文件名找到inode id,通过id找到对应的inode结点,然后在对该结点执行相应的操作。
99 | 对应到这里,我们也是需要先找到 /a/x/y.txt 对应的node id(由fs.Server维护), 而且是从/a, /a/x, /a/x/y.txt按目录树结构一级一级向下查找的,FUSE内核模块在查找的过程是通过向fs.Server发送fuse.LookupRequest的方式查找的,这就是我们的 **Dir结构体** 要实现fs.NodeStringLookuper接口的原因。/a结点的node id是1,然后内核开始查找/a/x结点,当fs.Server收到fuse.LookupRequest,执行其父结点(/a结点)的Lookup方法,我们的Lookup方法实现中判断到/b/x存在,就新生成一个 Dir结构 来表示 /a/x结点,就这样在层级向下查找的过程中,表示/a/x, /a/x/y.txt数据结构结点在fs.Server逐渐建立起来,最终得到/a/x/y.txt对应的 File结构体 的node id,然后执行open操作(发送fuse.OpenRequest请求)。在我们的Open操作的具体实现中,其实是打开了/b/x/y.txt的文件并将句柄存在了File实例对象中。这时一个open操作才做完,可以看到在我们的实现中,Open返回了一个 新的File实例 作为fs.Handle, 这个fs.Handle实例会被fs.Server保存起来,然后在回复内核的fuse.OpenResponse时,给了内核一个handle id(由fs.Server维护,我们不用关心),之后的write/close操作对应的fuse.WriteRequest/fuse.ReleaseRequest中都带有handle id,然后fs.Server根据handle id找到fs.Handle实例(也即File实例),并执行其上的对应方法。
100 |
101 | 总而言之,在用户操作文件的过程中,当操作执行过程到达内核后,内核先根据目录层级结构,一级一级的向fs.Server发送fs.LookupRequest直到找到最终结点fs.Node实例node id,然后在发送具体要执行的操作的fuse.*Request, fs.Server在收到Request之后,根据其携带的node id或handle id,找到具体的fs.Node或fs.Handle实例,然后执行其上的方法,之后将结果返回给内核,内核再返回给用户。
102 |
103 | ### 5. 实例之后的总结
104 | 在上面我并没有很详细的讲[mirrorfs代码](https://github.com/k2huang/mirrorfs)实现的细节,只是把bazil.org/fuse在处理用户访问我们的文件系统的过程中的交互流程说了一遍,我相信流程弄明白了,实现功能就很简单了。像mirrorfs只是实现了一个 镜像操作(将一个目录的操作映射到另一个的目录),其实我们可以更进一步,如将对一个目录的所有的写文件操作的数据(如日志数据)完全截取到我们的文件系统进程,然后发送到一个统一的地方(如日志中心),这样我们就不用先将数据写到本地磁盘然后再用一个Agent去采集了到统一的地方,是不是有点像NFS的功能。总之一句话,用户访问我们FUSE实现的文件系统的接口我们无法定义,但接口对应的具体操作完全由我们来决定,我们就是文件系统。
105 |
106 | ### 6. 参考链接
107 | [Wiki: Filesystem in Userspace](https://en.wikipedia.org/wiki/Filesystem_in_Userspace)
108 | [Writing file systems in Go with FUSE](https://blog.gopheracademy.com/advent-2014/fuse-zipfs/)
109 | [FUSE API documentation](https://lastlog.de/misc/fuse-doc/doc/html/)
--------------------------------------------------------------------------------
/golang/应用程序/用Go和FUSE自己的文件系统/imgs/FUSE.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k2huang/blogpost/5225b76a86f855088cd7c40c44fde9949159c8f8/golang/应用程序/用Go和FUSE自己的文件系统/imgs/FUSE.png
--------------------------------------------------------------------------------
/golang/应用程序/用Go和FUSE自己的文件系统/imgs/VFS.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/k2huang/blogpost/5225b76a86f855088cd7c40c44fde9949159c8f8/golang/应用程序/用Go和FUSE自己的文件系统/imgs/VFS.jpg
--------------------------------------------------------------------------------
/golang/标准库/http包基础/README.md:
--------------------------------------------------------------------------------
1 | # 标准库net/http包使用及工作原理
2 |
3 | ## 一. 1个 HTTP Server的基本构成
4 | 一个Web应用从 `浏览器向服务器发送请求(Request)开始,到服务器根据请求做出相应响应(Response)结束`,整个流程中服务器的`控制核心`无疑是:服务器从HTTP Request中提取请求路径(URL)并找到对应的处理程序(Handler)处理请求,最后返回结果。
5 |
6 | 我们知道HTTP协议是基于TCP协议的,我们当然可以自己通过标准库net中提供TCP API开始写一个HTTP Server,但你不得不自己额外做的事有:
7 | 1. 实现TCP Server监听,为每一新来的TCP link建立一个goroutine, 并在goroutine与客户端交互(不用担心`单机C10K问题`,因为goroutine是用户态线程,很轻量级,可以很随意就创建成千上万个)。
8 | 2. 在每个goroutine中将TCP link中的请求数据按HTTP协议格式解析出来(可以将数据解析出成Request对象,以后的访问提供方便),并根据其URL找到相应的处理程序Handler, 因此你还需要提前建立好 `URL:Handler映射表`。
9 | 3. 当处理程序处理结束后,你还需要将处理结果数据按HTTP协议格式返回给客户端。代码大致如下:
10 | ```go
11 | //建立 URL:Handler映射表
12 | //注意:由于table会在不同goroutine中使用,因此真正环境中需要锁保护
13 | var table = map[string]Handler {
14 | "/": rootHandler,
15 | "/login": loginHandler,
16 | ...
17 | }
18 |
19 | //Server监听
20 | ln, _ := net.Listen("tcp", ":8080")
21 | for {
22 | conn, _ := ln.Accept()
23 | go handleConn(conn)
24 | }
25 |
26 | //请求处理
27 | func handleConn(conn net.Conn) {
28 | //1. 将请求成封装Request对象
29 | //2. 从table中查找相应处理程序
30 | //3. 将处理结果封装HTTP格式数据返回给客户端
31 | }
32 |
33 | ```
34 | 然而这些需求在net/http包中都满足了,为什么不直接用?(Don't Repeat Yourself)
35 | net/http包中几个重要的类型:
36 | `http.ServeMux`: 建立URL:Handler映射表
37 | `http.Server`: 运行HTTP Server
38 | `http.Request`: 封装客户端HTTP请求数据
39 | `http.ResponseWriter`: 用来构造服务器端HTTP响应数据
40 | `http.Handler`: URL处理程序必须实现的接口
41 |
42 | ## 二. 使用net/http搭建一个最简单的HTTP Server
43 | ```go
44 | //step1. 建立 URL:Handler映射表
45 | mux := http.NewServeMux()
46 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
47 | fmt.Fprintln(w, "Hello, world")
48 | })
49 |
50 | //step2. 创建并运行HTTP server
51 | server := http.Server{Addr: ":8080", Handler: mux}
52 | log.Fatal(server.ListenAndServe())
53 |
54 | ```
55 | 然后我们打开浏览器在地址栏输入:http://localhost:8080
56 | 服务器将返回:Hello, world
57 | 是不是so easy!!!
58 |
59 | ## 三. 第二部分代码内部工作原理
60 | 第二部分代码和第一部分工作原理基本一致。首先看看step1中的 mux(http.ServeMux)的定义:
61 | ```go
62 | type ServeMux struct {
63 | mu sync.RWMutex //保护m
64 | m map[string]muxEntry //URL:Handler映射表
65 | hosts bool
66 | }
67 | type muxEntry struct {
68 | explicit bool
69 | h Handler
70 | pattern string
71 | }
72 | ```
73 | 很显然当我们调用mux.HandleFunc(...)的时候就是添加`URL:Handler`键值对到mux的map中。
74 | 那么`问题`来了,func(http.ResponseWriter,*http.Request)是个函数类型,而http.Handler是一个接口类型,二者是如何转换的?不妨看看 mux.HandleFunc:
75 | ```go
76 | func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
77 | mux.Handle(pattern, HandlerFunc(handler)) //又调用了mux.Handle
78 | }
79 | ```
80 | 而 mux.Handle 就比较简单了,就是将 func(http.ResponseWriter,*http.Request)转换为 http.Handler 然后放入mux的map中。
81 |
82 | `HandlerFunc`(注意不要与 `HandleFunc` 函数混淆了)是个什么鬼?又有什么用?
83 | ```go
84 | type HandlerFunc func(ResponseWriter, *Request)
85 | func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
86 | f(w, r)
87 | }
88 | ```
89 | 其实就是一个实现了 `http.Handler 接口`的类型,该类型底层基础类型就是 `func(ResponseWriter, *Request)`,我们知道在go语言中除了 `指针与接口` 其他基础类型也是可以定义方法的,标准库定义这个一个类型,为的就是将 普通`func(ResponseWriter, *Request)` 适配到 `http.Handler接口`. 所以 `HandlerFunc` 这样的类又称为`适配器类`,这一做法很有用,一定要掌握住该技巧。
90 |
91 | 好了,现在看看step2的 `http.Server`,将 `监听地址` 和 step1中的 `http.ServeMux` 对象传递给了它,然后调用`server.ListenAndServe()`开始监听, 处理流程大致如下:
92 | 1. server监听到有新链接进来,创建一个goroutine来处理新链接
93 | 2. 在goroutine中,将请求和响应分别封装为 http.Request和http.ResponseWriter对象。然后用这两个对象作为函数参数调用 server.Handler.serveHTTP(...), 而server.Handler 即为我们传入的 `http.ServeMux` 对象,而http.ServeMux对象的serveHTTP方法,我们都没有碰过,里面到底做了什么?
94 | 3. http.ServeMux对象的serveHTTP方法做的事,其实就是根据 http.Request对象中的URL 在自己的map中查找对应的Handler(这个又是我们在step1中添加的),然后执行。
95 |
96 | 绕了一大圈,简单来说就是 每当有新请求进来,server都会为我们新建一个goroutine,并在其中根据请求URL调用 我们在创建server之前添加的 URL:Handler映射表(通过server中的http.Handler字段混入)中的相应URL的Handler.
97 |
98 | `问题:为什么不在server中放置一个 URL:Handler 映射表?`
99 | 这样上面步骤2中就不用先绕到 server.Handler.serveHTTP(...)中,才能查找映射表了。这样的话`http.ServeMux` 对象也不需要了。 这样做从程序逻辑上讲没有问题,但将`http.Server`的逻辑弄得更复杂了,通过一个 `http.Handler中间层` 将 `URL路由功能从http.Server 解耦出来`,虽然理解起来有点绕,但各自的职责将更加清楚(`http.Server`就是只管HTTP Server中通用功能的部分,业务逻辑的不同处理都通过 `http.ServeMux` 来构建)
100 |
101 |
102 | ## 四. 更简单的代码写法
103 | ```go
104 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
105 | fmt.Fprintln(w, "Hello, world")
106 | })
107 | log.Fatal(http.ListenAndServe(":8080", nil))
108 | ```
109 | 为什么连 `http.ServeMux` 和 `http.Server`都没有用到?其实肯定是需要的,只是被封装了起来。大家都说golang很容易上手,这其实得益于 实现golang的团队 强大的封装抽象能力,将复杂留给了自己,简单易用性给了开发者。
110 |
111 | 其实这种写法是使用了 net/http包中定义的一个 全局`http.ServeMux`对象变量 `DefaultServeMux`, 然后在 http.ListenAndServe(":8080", nil) 函数中新构建了一个 `http.Server`对象,然后让server进入监听。比较巧妙地是 函数http.ListenAndServe的第二个参数,如果是nil,该server才会使用`DefaultServeMux`,否则使用新传入的`http.ServeMux`对象。
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------