<audio id="audio" title="第24讲  | 不可忽视的多线程及并发问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f0/db/f0ed6005d2e4957c32774f5f3d393edb.mp3"></audio>

既然我们说到了服务器端的开发，我们就不得不提起多线程和并发的问题，因为如果没有多线程和并发，是不可能做网络服务器端的，除非你的项目是base在Nginx或者Apache之上的。

## 多线程和并发究竟有什么区别和联系？

提到并发，不得不提到并行，所以我就讲这三个概念：并发、并行，以及多线程。作为初学者，你或许不太明白，多线程和并发究竟有什么区别和联系？下面我们就分别来看看。

**并发**出现在电脑只有一个CPU的情况下，那如果有多个线程需要操作，该怎么办呢？CPU不可能一次只运行一个程序，运行完一个再运行第二个，这个效率任谁都忍受不了啊！所以，就想了个办法。

CPU将运行的线程分成若干个CPU时间片来运行。不运行的那个线程就挂起，运行的时候，那个线程就活过来，切换地特别快，就好像是在同时运行一样。

你可以想象这个场景，有一个象棋大师，一个人对十个对手下棋，那十个人轮流和他下。大师从1号棋手这里开始下，下完1号走到2号的棋手面前，下2号棋手的棋，一直轮流走下去，直到再走回1号棋手这里再下一步。只要象棋大师下象棋下得足够快，然后他移动到下一位棋手这里又移动得足够快，大家都会觉得好像有十位象棋大师在和十个对手下棋。事实上只有一位象棋大师在下棋，只是他移动得很快而已。

**并行**和并发不同，并行是出现在多个物理CPU的情况下。在这种情况下，并行是真正的并发状态，是在物理状态下的并发运行。所以，并行是真的有几位象棋大师在应对几个对手。当然在并行的同时，CPU也会进行并发运算。

而**多线程**是单个进程的切片，单个进程中的线程中的内存和资源都是共享的，所以线程之间进行沟通是很方便的。

多线程的意义，就好比一个厨师，他掌管了三个锅，一个锅在煮排骨，一个锅在烧鱼，另一个锅在煮面，这三个锅内容不同，火候不同，但是所有的调料和资源，包括菜、油、水、盐、味精、糖、酱油等等，都来自同一个地方（也就是资源共享），而厨师自己是一个进程，他分配了三个线程（也就是三个锅），这三个锅烧着不同的东西，三个食物或许不是同时出锅的，但是厨师心里有数，什么时候这个菜可以出锅，什么时候这个菜还需要煮。这就是多线程的一个比喻。

我们在编写网络服务器的时候，多线程和并发的问题是一定会考虑的。我们说的网络并发和CPU的并发可以说是异曲同工，也就是说，**网络并发的意义是，这个网络服务器可以同时支撑多少个用户同时登陆，或者同时在线操作**。

## 为什么Python用多个CPU的时候会出现问题？

那么我们又回头来看，为什么Python、Ruby或者Node.js，在利用多个CPU的时候会出现问题呢？这是因为，它们是使用C/C++语言编写的。是的，问题就在这里。

我们后续的内容还是会用Python来写，所以我们先来看看Python的多线程问题。Python有个**GIL**（Global Interpreter Lock，全局解释锁），问题就出在GIL上。

使用C语言编写的Python版本（后面简写为C-Python）的线程是操作系统的原生线程。在Linux上为pthread，在Windows上为Win thread，完全由操作系统调度线程的执行。

一个Python解释器进程内有一条主线程，以及多条用户程序的执行线程。即使在多核CPU平台上。由于GIL的存在，所以会禁止多线程的并行执行。这是为什么呢?

因为Python解释器进程内的多线程是合作多任务方式执行的。当一个线程遇到I/O（输入输出）任务时，将释放GIL锁。计算密集型（以计算为主的逻辑代码）的线程在执行大约100次解释器的计步时，将释放GIL锁。你可以将计步看作是Python虚拟机的指令。计步实际上与CPU的时间片长度无关。我们可以通过Python的库sys.setcheckinterval()设置计步长度来控制GIL的释放事件。

在单核的CPU上，数百次间隔检查才会导致一次线程切换。在多核CPU上，就做不到这些了。从Python 3.2开始就使用新的GIL锁了。在新的GIL实现中，用一个固定的超时时间来指示当前的线程放弃全局锁。在当前线程保持这个锁，且其他线程请求这个锁的时候，当前线程就会在五毫秒后被强制释放掉这个锁。

我们如果要实现并行，利用Python的多线程效果不好，所以我们可以创建独立的进程来实现并行化。Python 2.6（含）以上版本引进了multiprocessing这个多进程包。

我们也可以把多线程的关键部分用C/C++写成Python扩展，通过ctypes使Python程序直接调用C语言编译的动态库的导出函数来使用。

C-Python的GIL的问题存在于C-Python的编写语言，原生语言C语言中，由于GIL为了保证Python解释器的顺利运行，所以事实上，多线程只是模拟了切换线程而已。这么做的话，如果你使用的是IO密集型任务的时候，就会提高速度。为什么这么说？

因为写文件读文件的时间完全可以将GIL锁给释放出来，而如果是计算密集型的任务，或许将会得到比单线程更慢的速度。为什么呢？事实上GIL是一个全局的排他锁，它并不能很好地利用CPU的多核，相反地，它会将多线程模拟成单线程进行上下文切换的形式进行运行。

我们来看一下，在计算密集型的代码中，单线程和多线程的比较。

单线程版本：

```
from threading import Thread
  import time
  def my_counter():
      i = 0
      for x in range(10000):
          i = i + 1
      return True
  def run():
      thread_array = {}
      start_time = time.time()
      for tt in range(2):
          t = Thread(target=my_counter)
          t.start()
          t.join()
      end_time = time.time()
      print(&quot;count time: {}&quot;.format(end_time - start_time))
  if __name__ == '__main__':
      run()

```

多线程版本：

```
from threading import Thread
  import time
  def my_counter():
      i = 0
      for x in range(10000):
          i = i + 1
      return True
  def run():
      thread_array = {}
      start_time = time.time()
      for tt in range(2):
          t = Thread(target=my_counter)
          t.start()
          thread_array[tid] = t
      for i in range(2):
          thread_array[i].join()
      end_time = time.time()
      print(&quot;count time: {}&quot;.format(end_time - start_time))
  if __name__ == '__main__':
      run()

```

当然，我们还可以把这个ranger的数字改得更大，看到更大的差异。

当计步完成后，将会达到一个释放锁的阀值，释放完后立刻又取得锁，然而这在单CPU环境下毫无问题，但是多CPU的时候，第二块CPU正要被唤醒线程的时候，第一块CPU的主线程又直接取得了主线程锁，这时候，就出现了第二块CPU不停地被唤醒，第一块CPU拿到了主线程锁继续执行内容，第二块继续等待锁，唤醒、等待，唤醒、等待。这样，事实上只有一块CPU在执行指令，浪费了其他CPU的时间。这就是问题所在。

这也就是C语言开发的Python语言的问题。当然如果是使用Java写成的Python（Jython）和.NET下的Python（Iron Python），并没有GIL的问题。事实上，它们其实连GIL锁都不存在。我们也可以使用新的Python实作项目PyPy。所以，这些问题事实上是由于实现语言的差异造成的。

## 如何尽可能利用多线程和并发的优势？

我们来尝试另一种解决思路，我们仍然用的是C-Python，但是我们要尽可能使之能利用多线程和并发的优势，这该怎么做呢？

multiprocess是在Python 2.6（含）以上版本的提供是为了弥补GIL的效率问题而出现的，不同的是它使用了多进程而不是多线程。每个进程有自己的独立的GIL锁，因此也不会出现进程之间，CPU进行GIL锁的争抢问题，因为都是独立的进程。

当然multiprocessing也有不少问题。首先它会增加程序实现时线程间数据通信和同步的困难。

就拿计数器来举例子。如果我们要多个线程累加同一个变量，对于thread来说，申明一个global变量，用thread.Lock的context就可以了。而multiprocessing由于进程之间无法看到对方的数据，只能通过在主线程申明一个Queue，put再get或者用共享内存、共享文件、管道等等方法。

我们可以来看一下multiprocess的共享内容数据的方案。

```
from multiprocessing import Process, Queue
  def f(q):
      q.put([4031, 1024, 'my data'])
  if __name__ == '__main__':
      q = Queue()
      p = Process(target=f, args=(q,))
      p.start()
      print q.get()
      p.join()

```

这样的方案虽说可行，但是编码效率变得比较低下，但是也是一种权宜之计吧。

## 小结

我们来总结一下今天的内容。

<li>
我首先介绍了几个概念。并发是单个CPU之间切换多线程任务的操作。并行是多个CPU同时分配和运行多线程任务的操作。线程是进程内的独立任务单元，但是共享这个进程的所有资源。网络的并发指的是服务器同时可以承载多少数量的人数和任务。
</li>
<li>
而C语言编写的Python有GIL锁的问题，会让其多线程计算密集型的任务效率更低，解决方案有，利用多进程解决问题 或者 更换Python语言的实现版本，比如PyPy或者JPython等等。
</li>

给你留一个小问题，如果Python以多进程方式进行操作，那么如果我们网络服务器是用Python编写的，其中一个Python进程崩溃或者报错了，有什么办法可以让其复活？

欢迎留言说出你的看法。我在下一节的挑战中等你！
