##线程与进程

线程就是操作系统能够进行运算调度的最小单位。
计算机中有各种各样的应用程序,这些程序执行任何操作都需要CPU来计算,CPU计算需要操作系统发送指令给CPU,线程就相当于这一系列的指令 。操作系统去调度CPU最小的单位就是线程。

线程被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

计算机同时运行多种程序时,这些程序都存储在内存中,不同的程序之间不能直接访问和控制。假如能够通过内存中实现程序之间互访和控制的话,那么就非常不安全了,比如程序1如果能访问程序2,那么程序1可以关闭程序2,可以利用程序2做各种操作,这是不希望看到的情况。所以说每个程序之间都是独立的。

进程:假如qq这个进程需要将数据发送到网络,那么就需要利用网卡,但是qq不能直接访问网卡,所以此时就需要一个相当于中间介质的东西来管理多个进程之间的访问,这个中间介质就是操作系统。 那么qq就需要提供一个接口暴露给操作系统,以便被操作系统来管理。 这个接口里面包含对各种资源的调用,内存的管理,网络接口的调用等(这个接口就称之为进程)。 程序各种资源管理的集合 就称为进程。 
qq本身就是一个独立的进程,这个进程里面包含qq要运行的所占内存的资源,操作系统对qq的管理就是通过这个进程实现的。

线程:是操作系统最小的调度单位,是一串指令的集合,这些指令会发送给CPU实现CPU的计算。

进程与线程关系举例: 假如有一个工厂(这个工厂相当于整个内存空间),这个工厂有十个车间(每个车间相当于一个进程,每个进程占用了部分内存空间,这个进程相当于这个车间内所有资源的集合),每个车间里有至少需要一个到多个工人来工作(每个工人都相当于线程,所有线程共享同一块内存空间和资源),这些工人(线程)做实际的工作操作(发送数据和指令给cpu运算,比如发送1和2两个数据给CPU,然后告诉CPU是进行加法还是减法的指令运算)。 每个车间(进程)的正常执行至少要有一个工人(线程)。 当启动一个进程会自动启动一个线程,这第一个启动的线程就是主线程。 主线程会创建子线程,子线程还可以在创建其他的子线程,主线程与子线程和子线程与子线程之间是独立的,不会受对方而影响,假如A子线程创建了B子线程,当A子线程关闭时,不会影响B子线程正常运作。
CPU有多核,双核CPU就表示同一时间可以做两件事。但是一核CPU的电脑也同样可以在同一时间执行多个应用程序,其实这是错觉,这是因为一核CPU在1秒内可以做上亿次计算,会在不同程序之间快速的切换,所以因为运算速度非常快导致我们以为可以同时计算多个程序的错觉。
多线程表示可以同一时间针对同一个资源同时操作,就好比两个工人在做同一件事。 但在计算机中却不是这样,在计算机中多线程并不是真正的多线程,比如一个Word属于一个进程,那么这个进程有两个线程在使用这个Word资源,但却不能同时使用这个资源,只是因为CPU计算过快造成了两个线程在同时使用同一个资源的错觉而已。

进程与线程的区别:
1、进程与线程哪个速度快:其实这个问题是错误的,因为进程和线程没有可比性,进程是资源的集合,进程也是需要线程来发送指令给CPU计算的。
2、启动一个进程快还是启动一个线程快:启进线程比进程快,因为启动进程需要到内存需申请空间和计算,启动线程只是相当于发送指令。
3、线程共享内存空间,进程的内存是独立的。
4、线程之间数据可共享,进程之间数据独立。
进程可以分为父进程和子进程,子进程就是完整的copy了一份父进程的内容,子进程可以有多个,不过因为是完整的copy了内容,所以子进程是独立的,不可以相互访问。 
进程可以生成线程,不过线程之间是共享数据的,将其中一个数据修改后,其他线程看到的也是修改后的数据,而子进程之间因为是完全独立的,所以各自进程的数据被修改,是不会影响其他子进程的。
一个线程可以控制和同一进程的其他线程,但是父进程只能操作子进程,子进程之间不能相互操作。
对其中一个线程修改,可能会影响其他线程就,线程之间没有隶属关系; 对一个父进程的修改,不会影响其他子进程;删除父进程会影响子进程。

并发:
一核CPU同时只能处理一个任务,假如现在需要处理十个文档,那么就需要一个一个的来处理,需要处理十次。
但是我们用十个任务分别处理这十个文档就,对于CPU虽然不是同时的,但因为CPU处理切换很快,给人感觉上就像是同时在处理。

import threading

def run(n):
    print ("task",n)

t1 = threading.Thread(target=run,args=("t1",))
t2 = threading.Thread(target=run,args=("t2",))
## target是目标,启动一个线程的目标是谁; 通过args传递参数。
## 这里参数只有"t1"这么一个元素,不过即使一个元素后面也要加逗号,否则就会将("t1")包括 括号也当成参数。

t1.start()
t2.start()

执行结果:
task t1
task t2
## 可以看到瞬间就执行完成了
import threading
import time
def run(n):
    print ("task",n)
    time.sleep(2)

# t1 = threading.Thread(target=run,args=("t1",))
# t2 = threading.Thread(target=run,args=("t2",))
# ## target是目标,启动一个线程的目标是谁; 通过args传递参数。
# ## 这里参数只有"t1"怎么一个元素,不过即使一个元素后面也要加逗号,否则就会将("t1")包括 括号也当成参数。
#
# t1.start()
# t2.start()

run("t1")
run("t2")

执行结果:
task t1
task t2
## 我们不通过线程来执行,通过调用函数的方式来执行,可以看到task t1结果出来以后,需要等待2秒才会得到结果task t2
run("t1")和run("t2")一共需要等待4秒才能将真个程序执行完成。

import threading
import time
def run(n):
    print ("task",n)
    time.sleep(2)   #这里延迟2秒

t1 = threading.Thread(target=run,args=("t1",))
t2 = threading.Thread(target=run,args=("t2",))
## target是目标,启动一个线程的目标是谁; 通过args传递参数。
## 这里参数只有"t1"怎么一个元素,不过即使一个元素后面也要加逗号,否则就会将("t1")包括 括号也当成参数。

t1.start()
t2.start()

执行结果:
task t1
task t2
## 这里我们可以看到虽然所依然有time.sleep(2),但是瞬间执行完成;
## 这是因为我们用函数执行时,必须在执行完成run("t1")后等待2秒后才去执行run("t2");  而我们通过多线程并发去执行但却瞬间完成了,原因是当执行t1.start()时瞬间就完成了,不需要CPU再去计算,直接切换执行t2.start()时并不需要等待t1.start()的2秒,所以因为计算过快t1.start()和t2.start()相当于并发完成,总共也就等待了2秒钟,而不是4秒。

这里要注意:假如计算机是4核CPU,我们一个进程有4个线程,这4个线程只会在其中一个核来处理,而不会分到4核分别处理。
通过类执行线程

import threading
import time

class MyThread(threading.Thread):
    def __init__(self,n):
        super(MyThread,self).__init__()
        self.n = n

    def run(self):  #通过类写多线程,方法名称只能写run,这是写死的。 不在类中的话,这个名称可以自定义。
        print ("running task",self.n)
        time.sleep(2)

t1 = MyThread("t1") #线程1
t2 = MyThread("t2") #线程2

t1.start()
t2.start()

执行结果:
running task t1
running task t2
## 两个线程很快执行完成
循环线程

import threading
import time
def run(n):
    print ("task",n)
    time.sleep(2)   #这里延迟2秒

for i in range(50):
    t = threading.Thread(target=run,args=("t-%s"%i,))
    t.start()

执行结果:
task t-0
task t-1
task t-2
task t-3
task t-4
task t-5
task t-6
.........
task t-47
task t-48
task t-49

## 也很快就执行完成了


import threading
import time
def run(n):
    print ("task",n)
    time.sleep(2)   #这里延迟2秒

start_time = time.time()

for i in range(50):
    t = threading.Thread(target=run,args=("t-%s"%i,))
    t.start()

print ("cost:",time.time() - start_time)    #用当前时间减去start_time来得出执行完成花费了多久

执行结果:
task t-0
task t-1
task t-2
........
task t-47
task t-48
task t-49
cost: 0.00800466537475586
## 可以看到执行完成采用了0.008秒????  这显然不正常!
import threading
import time
def run(n):
    print ("task",n)
    time.sleep(2)   #这里延迟2秒
    print ('task done ',n )
start_time = time.time()

for i in range(50):
    t = threading.Thread(target=run,args=("t-%s"%i,))
    t.start()

print ("cost:",time.time() - start_time)

执行结果:
task t-0
task t-1
task t-2
task t-3
......
task t-47
task t-48
task t-49
cost: 0.009006023406982422
task done t-0
task done t-1
task done t-4
task done t-5
task done t-3
##可以看到没有执行完主线程,就已经去执行其他子线程了,当执行到t.start()时,就会生成其他子线程,子线程是独立的,所以执行这个子线程时不需要等待主线程执行完成,而主线程和子线程,子线程和子线程之间执行也是不是同步的,所以最后我们看到task done是无序的。
循环50次,相当于有50个子线程,这50个子线程是并发的,总共执行完该程序也就两秒左右。
线程执行等待

import threading
import time

class MyThread(threading.Thread):
    def __init__(self,n):
        super(MyThread,self).__init__()
        self.n = n

    def run(self):  #通过类写多线程,方法名称只能写run,这是写死的。
        print ("running task",self.n)
        time.sleep(2)

t1 = MyThread("t1")
t2 = MyThread("t2")

t1.start()
t1.join()   ## t1.join()用来等待t1这个线程执行完成后,才会继续向下执行

t2.start()

执行结果:
running task t1
running task t2
## 等待t1线程执行完成,也就是2秒的时间后,才看到t2被执行。

import threading
import time

class MyThread(threading.Thread):
    def __init__(self,n):
        super(MyThread,self).__init__()
        self.n = n

    def run(self):  #通过类写多线程,方法名称只能写run,这是写死的。
        print ("running task",self.n)
        time.sleep(2)

t1 = MyThread("t1")
t2 = MyThread("t2")

t1.start()
t2.start()

t1.join()   ##我们将t1.join()放到所有线程下面这样就不影响其他子线程的执行了,因为所有线程是并行的,都是sleep2秒,所以当t1这个线程执行完成了,就表示其他线程也差不多完成了。 这样就能计算出所有线程基本的执行时间。
import threading
import time

class MyThread(threading.Thread):
    def __init__(self,n,sleep_time):    #这里加一个参数
        super(MyThread,self).__init__()
        self.n = n
        self.sleep_time = sleep_time    #定义不同线程使用不同的时间

    def run(self):
        print ("running task",self.n)
        time.sleep(self.sleep_time)     #每个线程sleep时间不同
        print ("task done",self.n)

t1 = MyThread("t1",2)
t2 = MyThread("t2",4)

t1.start()
t2.start()

t1.join()
print("main thread....")

执行结果:
running task t1
running task t2     ## 这里说明t2已经和t1并行执行了。
task done t1    
main thread....     ## 这里是t1.join()执行完成后(等了2秒)打印的
task done t2        ## t2和t1是并行的,但是等待了4秒,比t1长,所以会在最后打印出来。

## 当前有两个线程,每个线程sleep的时间都不一样,不能只靠t1.join()执行完成来计算时间。
## 
import threading
import time

class MyThread(threading.Thread):
    def __init__(self,n,sleep_time):    #这里加一个参数
        super(MyThread,self).__init__()
        self.n = n
        self.sleep_time = sleep_time    #定义不同线程使用不同的时间

    def run(self):
        print ("running task",self.n)
        time.sleep(self.sleep_time)     #每个线程sleep时间不同
        print ("task done",self.n)

t1 = MyThread("t1",2)
t2 = MyThread("t2",4)

t1.start()
t2.start()

t1.join()
t2.join()
## 我们需要将所有线程都进行.join(),然后等待所有线程执行完成后,在去计算时间,这样就能计算出总共花费多久了。

print("main thread....")
import threading
import time
def run(n):
    print ("task",n)
    time.sleep(2)   #这里延迟2秒
    print ("task done",n)   #这里我们加一行代码

start_time = time.time()

t_objs = []
for i in range(50):
    t = threading.Thread(target=run,args=("t-%s"%i,))
    t.start()   ## 这里已经并行了所有子线程(非主线程);
    t_objs.append(t)    ## 写一个空列表,将所有线程假如到列表中

for t in t_objs:
    t.join()    #循环将所有线程.join(),这样就可以等待所有线程结束后,来计算时间了。

print ("cost:",time.time() - start_time)    ##上面的for循环执行完成表示所有线程已经执行完成了,然后此时在计算时间即可。

执行结果:
import threading
import time
def run(n):
    print ("task",n)
    time.sleep(2)
    print ("task done",n,threading.current_thread())    ##threading.current_thread()来判断是子线程还是主线程

start_time = time.time()

t_objs = []
for i in range(50):
    t = threading.Thread(target=run,args=("t-%s"%i,))
    t.start()   ## 注意这里第一个线程不是主线程,因为主线程就是程序本身。
    t_objs.append(t)

for t in t_objs:
    t.join()

print ("finished!",threading.current_thread())  ##threading.current_thread()来判断是子线程还是主线程
print ("cost:",time.time() - start_time)

执行结果:
.......
task done t-45 <Thread(Thread-46, started 10096)>   
task done t-47 <Thread(Thread-48, started 6380)>
task done t-42 <Thread(Thread-43, started 4128)>    #Thread表示子线程
finished! <_MainThread(MainThread, started 6100)>   #MianThread表示主线程
cost: 2.0156285762786865

## 最终表明,for循环时执行的都是子线程;程序本身有一个主线程。
import threading
import time
def run(n):
    print ("task",n)
    time.sleep(2)
    print ("task done",n)

start_time = time.time()

t_objs = []
for i in range(50):
    t = threading.Thread(target=run,args=("t-%s"%i,))
    t.start()
    t_objs.append(t)

print ("first:",threading.active_count())
## 这里可以看到一共有51个线程,1个主线程,50个子线程。
## 注意改行代码要在t.join完成之前print才能看到有多少个正在运行的线程,否则t.join完整执行线程之后,就看不正在执行的线程了。

for t in t_objs:
    t.join()
    ## 这个t.join()不可以放在上面那个for循环中,不然就得等待每次循环的线程完成才会进行下一次循环;
    ## 这里我们是将所有线程加入到了列表中,然后等待所有线程完成(但不影响之前for循环的执行)
    ## 如果这里不使用for循环来执行t.join(),只是在全局执行的话,那么这里只会匹配最后启动的线程,等待最后一个子线程执行完成。

print ("second:",threading.active_count())  ##这里属于主线程的一部分
## 这里已经完成了t.join线程的执行,所以这里只能看到一个主线程
## 该程序除了子线程外,其他整个部分都属于一个主线程。

print ("finished!")  ##threading.current_thread()来判断是子线程还是主线程
print ("cost:",time.time() - start_time)

守护线程

正常情况下主线程和子线程是并发的,如果整个程序主线程或子线程没有执行完成的话,那么就不会退出该程序,必须得全部执行完成才会退出(在不使用.join的情况下)。

现在有一个守护线程的概念,将子线程变成守护线程,当非守护线程(主线程)执行完成时,不会管守护线程有没有执行完成,都会退出改程序; 守护线程只是帮助主线程做事的,守护线程在这里并不重要,所以只要主线程执行完成,就会退出程序。


import threading
import time
def run(n):
    print ("task",n)
    time.sleep(2)
    print ("task done",n)

start_time = time.time()

t_objs = []
for i in range(50):
    t = threading.Thread(target=run,args=("t-%s"%i,))
    t.setDaemon(True)
    ## 把当前线程设置为守护线程;  设置守护之前一定要在t.start()之前,在t.start()之后就不允许设置了。
    t.start()
    t_objs.append(t)

# for t in t_objs:      #这里需要把.join的操作注释掉
#     t.join()

print ("finished!")
print ("cost:",time.time() - start_time)

执行结果:
........
ask t-47
task t-48
task t-49
finished!
cost: 0.007991552352905273

## 这里可以看到并没有看到守护线程的执行结果内容task done。主线程执行完成后,直接退出程序,没有给机会再去执守护线程(子线程)。

## 比如当要关闭某个服务时,直接关闭主线程就直接停止服务(主线程停止服务后就不能提供完整的服务了,守护线程没存在的用处了),不需要再等子线程执行完成了,这样能快速的关闭服务。

GIL锁(全局锁)

面试时会经常问道有关全局锁的问题。

python有通过C语言开发的,也有通过JAVA开发的。C语言开发的就叫做cpython,Java开发的就叫做jpython。

其中cpython存在一个问题需要使用到全局锁。

此时假如有4个任务,如果只有一核CPU,那么执行任务时,只能是串行的方式执行,不是并行的方式,执行任务1时,如果任务1没有计算完成,可能会切换到任务2去计算,那么此时任务1会被记住已经执行到了哪一页,哪一行,哪一个具体字符的位置(称作:上下文),等到下次切换到任务1时,就继续之前的位置计算执行。

如果有4核CPU,就可以同时执行4个任务,实现真正的在同一时刻并发,其中Java和c都可以实现真正的并发;而python是假并发,这是因为当初python在设计时还没有多核CPU,所以就没考虑使用多核CPU来实现并发,而导致了python设计缺陷; 当前python只是因为CPU计算切换的较快,所以看到的是假并发的错觉。
python不管计算机有多少核CPU,在同一时刻,能计算的线程只有一个。

假如现在有个数据 number = 1,此时我们起4个线程,希望每个线程+1后,根据加减结果另外线程再去+1,我们同时交给4核CPU分别取处理去+1(要求最终结果等于4),线程1将数据交给了CPU1,线程2将数据交给了CPU2......,每核CPU在同一时间获取了number = 1这个数据,然后分别各自去做 +1 这个操作等于2,而我们每核CPU会将2这个结果返还给number = 2,所以最后number还是 =2,并不是我们期望的4核CPU分别+1最终=4。

像加减运算这类的数据,我们还是期望使用串行的方式,一个一个的去计算,而不是并发导致最终计算错误。

此时我们就可以使用全局锁来解决cpython中出现的这个问题。
虽然4个线程将数据同时交给了4个CPU,但线程到python解释器去申请全局锁后才能执行(python解释器负责全局锁分配),得到gil锁后就正常执行没得到gil锁的就不能执行,同一时间只能有一个线程获取gil锁,这样可以避免所有线程同时去计算。 
但gil锁也有个问题是,其中一个线程拿到gil锁后会对线程有执行的时间限制。假如线程1拿到了gil锁,一共需要执行5秒才能完成线程1的代码,但是gil锁只给了线程1秒的执行时间就必须释放gil锁给其他线程轮询执行(此时CPU会记住线程1的位置,也就是上下文),此时线程1还没执行完成,线程2就拿到gil锁去执行可能就会导致最终共享数据计算错误。

我们从www.pyrhon.org下载的python,就是cpython

image_1c7g31td410gd17621kcg1nbdsnb9.png-109.6kB
多线程,在python2中有gil锁依然出错,下面我们用0+1来计算模拟问题:
步骤1:线程1拿到共享数据
步骤2:线程1到python解释器去申请gil锁。
步骤3:解释器调用系统原生线程(操作系统的线程)
步骤4:将线程1的任务分配到CPU1。
步骤5:此时线程1还没有执行完成,因为执行时间到了,被要求释放gil锁。
步骤6:线程2拿到共享数据。
步骤7:线程2到解释器去申请gil锁。
步骤8:调用原生线程。
步骤9:线程2被分配到了CPU3.
步骤10:此时由于线程2执行较快,在执行时间内完成了计算,返回给解释器。
步骤11:count默认=0,此时线程2进行0+1=1,将1这个值赋值给了count,count此时=1。
步骤12:线程1重复第一次执行的所有动作。
步骤13:此时线程1也计算完成,将0+1 ,count=1的结果赋值给count,此时会覆盖count的数据,所以最终count还是=1,。

有全局锁,依然会存在修改共享数据的问题,那么我们可以通过在加一把锁来解决这个问题。在加的锁就不是全局锁了,和全局锁也没有关系,而是针对数据加一把锁,新加锁后只能其中一个线程来修改这个数据。 也就是说计算的时候依然使用全局锁,但是改变数据时只能等待拿到新锁的线程修改完后,其他线程才能去修改这个数据。
假如线程1拿到了修改数据的新锁,此时线程2计算的比线程1要快,但线程2不能改变数据,只能等线程1修改完后,线程2才能去获取锁在修改。
最终多线程还是可以实现的,只是计算的过程实现了并行,而修改结果变成了串行。


import threading
import time
def run(n):
    lock.acquire()  ##获取修改数据的锁
    global num  ##修改全局变量,在子线程中去修改num;  此时所有线程都共享num这个资源了
    num += 1
    #time.sleep(2) ##sleep时是不占用CPU的。
    ## 注意如果这里使用了sleep,效果就变成了串行,因为在释放锁之前,这个线程因为sleep还没有执行完,每个线程都需要等待2秒,所以其他线程都要等,如果不使用sleep的话,虽然计算结果时也是串行,但计算是非常快的。
    lock.release()  ## 修改完结果后,拿到锁的线程,需要释放锁

lock = threading.Lock()

num = 0
start_time = time.time()

t_objs = []
for i in range(10):  ##定义1000个子线程
    t = threading.Thread(target=run,args=("t-%s"%i,))
    t.start()
    t_objs.append(t)

for t in t_objs:      #这里需要把.join的操作注释掉
    t.join()

print ("finished!")

print ("num:",num)

到了python3中,可能是进行了优化,不进行数据锁,也不会出现共享数据计算出错的问题。
不过python3虽然不会错先这种问题,但依然需要加数据锁,因为python官网并没有声明在python3中是否解决了这个问题。


递归锁

递归锁就是一把大锁还有自锁

import threading, time

def run1():
    print("grab the first part data")
    lock.acquire()  ## 在run3已经执行了一把锁,所以在run1里这是第二小把锁,同时有两把锁
    global num
    num += 1
    lock.release()  
    ## 解锁,不过这里解的不是run1里的锁,而是run3。用run1的要是去解run3的锁,是解不开的,所以就会卡死,陷入循环来打印当前活动线程
    return num

def run2():
    print("grab the second part data")
    lock.acquire()
    global num2
    num2 += 1
    lock.release()
    return num2

def run3():
    lock.acquire()  ## run3是第一把锁
    res = run1()    ## 执行run1();  res等于run1返回的num结果
    print('--------between run1 and run2-----')
    res2 = run2()   ## 执行run2()
    lock.release()
    print(res, res2)

if __name__ == '__main__':

    num, num2 = 0, 0
    lock = threading.Lock()
    for i in range(1):     #1个子线程+1个主线程一共2个线程
        t = threading.Thread(target=run3)   #通过run3来启动子线程
        t.start()

while threading.active_count() != 1:    ##来判断活动的线程不等于1的时候,就循环。
    ##线程等于1的话,表示子线程都结束了,只剩下1个子线程。
    print(threading.active_count())     ## 子线程还没执行完成,就不断的显示还有几个活动的子线程
else:
    print('----all threads done---')
    print(num, num2)    ##只剩下1个主线程,就打印num, num2

执行结果:
grab the first part data
2
2
2
......
import threading, time

def run1():
    print("grab the first part data")
    lock.acquire()  
    global num
    num += 1
    lock.release()

    return num

def run2():
    print("grab the second part data")
    lock.acquire()
    global num2
    num2 += 1
    lock.release()
    return num2

def run3():
    lock.acquire()  
    res = run1()    
    print('--------between run1 and run2-----')
    res2 = run2() 
    lock.release()
    print(res, res2)

if __name__ == '__main__':

    num, num2 = 0, 0
    lock = threading.RLock()    ##这里使用RLock递归所,就解决了卡死的问题
    for i in range(1):    
        t = threading.Thread(target=run3)  
        t.start()

while threading.active_count() != 1:    
    print(threading.active_count())     
else:
    print('----all threads done---')
    print(num, num2)    ##只剩下1个主线程,就打印num, num2

执行结果:
grab the first part data
--------between run1 and run2-----
grab the second part data
1 1
----all threads done---
1 1

##用递归所解决了问题;  不过这种情况很少会用到。

Semaphore(信号量)

import threading, time

def run(n):
    semaphore.acquire()     ##信号量也需要获取一把锁(信号量与全局锁功能差不多,只是展现的形式不同而已)
    ##全局锁同时只能有一个锁,信号量同时可以有多个锁。
    time.sleep(1)
    print("run the thread: %s\n" % n)
    semaphore.release()     ##信号量释放锁

if __name__ == '__main__':

    # num = 0
    semaphore = threading.BoundedSemaphore(5)  ##用信号量生成锁
    ## 此时信号量允许最多5个线程同时修改数据
    # 全局锁允许同时1个线程修改数据,使用信号量同时最多允许多个线程同时修改数据
    for i in range(20):
        t = threading.Thread(target=run, args=(i,))
        t.start()

while threading.active_count() != 1:
    pass  # print threading.active_count()
else:
    print('----all threads done---')
    # print(num)

执行结果:
run the thread: 3

run the thread: 1

run the thread: 4

run the thread: 2

run the thread: 0

run the thread: 7
run the thread: 6
run the thread: 9

run the thread: 8

run the thread: 5

run the thread: 11

run the thread: 10

run the thread: 12

run the thread: 14

run the thread: 13

run the thread: 15

run the thread: 19
run the thread: 17

run the thread: 16
run the thread: 18

----all threads done---
0

## 同一时间可以执行5个线程,假如其中3线程个已经执行完成了,不需要等待其他2个,会直接在放进去3个线程来执行,保证同一时间一直有5个线程执行。
## 注意使用信号量,同时5个线程执行的话,也会导致加减计算错误; 不过信号量不是用来做数字计算的,一般涉及与连接池、线程池等。
## 系统默认不会限制线程数量,如果线程越多就会对系统影响越大,导致系统运行缓慢。  这里就可以通过信号量限制多少个连接进来。