十八、并发编程之多线程

十八、并发编程之多线程

一、什么是线程

  在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程

  线程顾名思义,就是一条流水线工作的过程,一条流水线必须属于一个车间,一个车间的工作过程就是一个进程

  车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一个流水线

  流水线的工作需要电源,电源就相当于CPU

  所以进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是CPU上的执行单位

  

  多线程(即多个控制线程)的概念,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间内的资源

二、为何要用多线程

  多线程指的是在一个进程中开启多个线程,简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程,详细的讲分为4点:

  1.多线程共享一个进程的地址空间

  2.线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量需要动态和快速修改时,这一特性很有用

  3.若多个线程都是CPU密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度

  4.在多CPU系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python)

三、线程与进程的区别

  1.创建进程的开销要远大于线程

  2.进程之间是竞争的关系,线程之间是协作关系

  3.进程之间的内存空间物理隔离,线程之间的内存空间共享

  4.进程只能对子进程进行控制,线程可以对同一进程的线程进行相当大的控制

  5.对父进程的更改不会影响子进程,对主线程的更改(取消,优先级更改等)可能会影响进程中其他的线程

四、开启线程的两种方式

 

 1 #方式一
 2 from threading import Thread
 3 
 4 def task():
 5     for i in range(1,5):
 6         print('子线程干了第%s次活'%i)
 7 
 8 
 9 for i in range(10):
10     t=Thread(target=task,name='线程%s'%i)
11     print(t.name)
12     t.start()

 

 1 #方式二
 2 from threading import Thread
 3 
 4 class MYthread(Thread):
 5     def __init__(self,name):
 6         super().__init__()
 7         self.name=name
 8 
 9     def run(self):
10         for i in range(1, 5):
11             print('子线程干了第%s次活' % i)
12 
13 for i in range(10):
14     t=MYthread('线程%s'%i)
15     print(t.name)
16     t.start()

 

五、死锁问题 

1、所谓死锁:是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去,此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程或线程称之为死锁进程或线程

死锁发生的条件:有多个线程多个锁,如果只有一个锁,无论是Lock,还是Rlock都不会死锁(前提是程序逻辑正确)

2、递归锁:Rlock就算你的代码逻辑不对,同一个线程多次对一个锁执行acquire也不会死锁

 1 #例如抢盘子,叉子吃饭
 2 
 3 from threading import Thread,Lock,current_thread,RLock
 4 import time
 5 # 叉子
 6 locka = RLock()
 7 # 盘子
 8 lockb = RLock()
 9 
10 
11 
12 def task1():
13     print(current_thread())
14     locka.acquire()
15     print("抢到叉子 需要盘子")
16     time.sleep(0.1)
17     lockb.acquire()
18     print("吃饭")
19 
20     lockb.release()
21     locka.release()
22 
23 def task2():
24     print(current_thread())
25     lockb.acquire()
26     print("抢到盘子 需要叉子")
27     time.sleep(0.1)
28     locka.acquire()
29 
30     print("吃饭")
31     locka.release()
32     lockb.release()
33 
34 
35 t1 = Thread(target=task1)
36 t1.start()
37 t2 = Thread(target=task2)
38 t2.start()

 

六、信号量

1、semaphore管理一个内置的计数器,每当调用acquire时内置计数器+1,调用release时内置计数器-1

计数器不能小于0,当计数器为0时,acquire将阻塞线程直到其他线程调用release

 1 from threading import Thread,Semaphore,current_thread,active_count
 2 
 3 import time
 4 # 用于控制 同时执行被锁定代码的线程数量   也就是线程的并发数量
 5 # 也是一种锁,控制同时进入锁内代码的线程数量
 6 sm = Semaphore(1)
 7 
 8 def task():
 9     sm.acquire()
10     for i in range(10):
11         print(current_thread())
12         time.sleep(0.5)
13     sm.release()
14 
15 def task2():
16      for i in range(10):
17         print(current_thread())
18         time.sleep(0.5)
19 
20 
21 for i in range(5):
22     Thread(target=task).start()
23     Thread(target=task2).start()
24 print(active_count())

 

七、多进程与多线程之间的开启时间对比

 1 from threading import Thread
 2 from multiprocessing import Process
 3 import time
 4 import os
 5 
 6 def work():
 7     time.sleep(2)
 8     print('====>')
 9 
10 if __name__ == '__main__':
11     lis=[]
12     print(os.cpu_count())#此笔记本电脑为8核
13     start_time=time.time()
14     for i in range(500):
15         # p=Process(target=work) #耗时57秒
16         p=Thread(target=work)#耗时2秒
17         lis.append(p)
18         p.start()
19     for p in lis:
20         p.join()
21     print('run time is %s'%(time.time()-start_time))

 

八、GIL

1、什么是GIL:

  GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据的安全

  可以肯定的一点是:保护不同的数据安全,就因该加不同的锁

  要想了解GIL,首先确定一点:每次执行python程序,都会产生一个独立的进程,例如python test.py  ,  python aaa.py  ,  python  bbb.py会产生3个不同的python进程

 1 '''
 2 #验证python test.py只会产生一个进程
 3 #test.py内容
 4 import os,time
 5 print(os.getpid())
 6 time.sleep(1000)
 7 '''
 8 python3 test.py 
 9 #在windows下
10 tasklist |findstr python
11 
12 验证python test.py只会产生一个进程

在一个python的进程内,不仅有test.py的主线程或者由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一个进程内

1 #1 所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码)
2 例如:test.py定义一个函数work(代码内容如下图),在进程内所有线程都能访问到work的代码,于是我们可以开启三个线程然后target都指向该代码,能访问到意味着就是可以执行。
3 
4 #2 所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码。

 

九、同步、异步和阻塞、非阻塞

进程或线程的三种状态:

  1.就绪

  2.运行

  3.阻塞

阻塞:遇到了IO操作,代码卡主无法执行下一行,CPU会切换到其他任务

非阻塞:与阻塞相反,代码正在执行(运行状态)或处于就绪状态

阻塞和非阻塞描述的是运行的状态

同步:提交任务必须等任务完成,才能执行下一行

异步:提交任务不需要等待任务完成,立即执行下一行

指的是一种提交任务的方式

1 #所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回。按照这个定义,其实绝大多数函数都是同步调用。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。
2 #举例:
3 #1. multiprocessing.Pool下的apply #发起同步调用后,就在原地等着任务结束,根本不考虑任务是在计算还是在io阻塞,总之就是一股脑地等任务结束
4 #2. concurrent.futures.ProcessPoolExecutor().submit(func,).result()
5 #3. concurrent.futures.ThreadPoolExecutor().submit(func,).result()
1 #异步的概念和同步相对。当一个异步功能调用发出后,调用者不能立刻得到结果。当该异步功能完成后,通过状态、通知或回调来通知调用者。如果异步功能用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一 种很严重的错误)。如果是使用通知的方式,效率则很高,因为异步功能几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。
2 #举例:
3 #1. multiprocessing.Pool().apply_async() #发起异步调用后,并不会等待任务结束才返回,相反,会立即获取一个临时结果(并不是最终的结果,可能是封装好的一个对象)。
4 #2. concurrent.futures.ProcessPoolExecutor(3).submit(func,)
5 #3. concurrent.futures.ThreadPoolExecutor(3).submit(func,)
1 #阻塞调用是指调用结果返回之前,当前线程会被挂起(如遇到io操作)。函数只有在得到结果之后才会将阻塞的线程激活。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
2 #举例:
3 #1. 同步调用:apply一个累计1亿次的任务,该调用会一直等待,直到任务返回结果为止,但并未阻塞住(即便是被抢走cpu的执行权限,那也是处于就绪态);
4 #2. 阻塞调用:当socket工作在阻塞模式的时候,如果没有数据的情况下调用recv函数,则当前线程就会被挂起,直到有数据为止。
1 #非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前也会立刻返回,同时该函数不会阻塞当前线程。

总结:

1 #1. 同步与异步针对的是函数/任务的调用方式:同步就是当一个进程发起一个函数(任务)调用的时候,一直等到函数(任务)完成,而进程继续处于激活状态。而异步情况下是当一个进程发起一个函数(任务)调用的时候,不会等函数返回,而是继续往下执行当,函数返回的时候通过状态、通知、事件等方式通知进程任务完成。
2 
3 #2. 阻塞与非阻塞针对的是进程或线程:阻塞是当请求不能满足的时候就将进程挂起,而非阻塞则不会阻塞当前进程

 

十、协程

1、什么是协程:

  协程:是单线程下的并发,又称微线程,纤程。

  协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的

2、为什么要用协程:

  python的线程属于内核级别的,即由操作系统控制调度,如单线程遇到IO或执行时间过长就会被迫交出CPU的执行权限,切换其他线程运行

  单线程内开启协程,一旦遇到IO阻塞,就会从应用程序级别控制切换,以此来提升效率

3、对比操作系统控制线程的切换,用户在单线程内控制协程的切换:

  优点:

  1.协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级

  2.单线程内就可以实现并发的效果,最大限度地利用CPU

  缺点;

  1.协程的本质是单线程下的,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程

  2.协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

4、总结特点;

  1.必须在只有一个单线程里实现并发

  2.修改共享数据不需加锁

  3.用户程序里自己保存多个控制流的上下文栈

  4.附加:一个协程遇到IO操作自动切换到其他协程,需要用到gevent模块

 1 from gevent import monkey
 2 #用来检测整个代码中的IO操作,如有就切换
 3 monkey.patch_all()
 4 
 5 import gevent
 6 import time
 7 def eat():
 8     print('eat food 1')
 9     time.sleep(2)
10     print('eat food 2')
11 
12 def play():
13     print('play 1')
14     time.sleep(1)
15     print('play 2')
16 
17 g1=gevent.spawn(eat)
18 g2=gevent.spawn(play)
19 gevent.joinall([g1,g2])
20 print('')

 

十一、socketserver

基于tcp的套接字,关键就是两个循环,一个链接循环,一个通信循环

socketserver模块中分两大类:server类(解决链接问题)和request类(解决通信问题)

基于tcp的socketserver我们自己定义的类中

  1.self.server即套接字对象

  2.self.request即一个链接

  3.self.client_address即客户端地址

基于udp的socketserver我们自己定义的类中

  1.self.request是一个元组(第一个元素是客户端发来的数据,第二个元素是服务端的udp套接字对象)

  2.self.client_address即客户端的地址

 1 #tcp服务端
 2 from threading import current_thread
 3 import socketserver
 4 
 5 class MyServer(socketserver.BaseRequestHandler):
 6     def handle(self):
 7         print(self.request)
 8         print(self.client_address)
 9         print(self.server)
10 
11         while True:
12             try:
13                 data=self.request.recv(1024)
14                 print(data.decode('utf-8'))
15                 self.request.sendall(data.upper())
16                 print(current_thread())
17             except ConnectionResetError:
18                 print('客户端异常关闭')
19                 self.request.close()
20                 break
21 
22 if __name__ == '__main__':
23     server=socketserver.ThreadingTCPServer(('127.0.0.1',10000),MyServer)
24     server.serve_forever()
 1 #udp服务端
 2 import socketserver
 3 from threading import current_thread
 4 
 5 class MyserverUDP(socketserver.BaseRequestHandler):
 6     def handle(self):
 7         print(self.request)
 8         print(self.server)
 9         print(self.client_address)
10         data,server=self.request
11         print(data.decode('utf-8'))
12         server.sendto(data.upper(),self.client_address)
13         print(current_thread())
14 
15 if __name__ == '__main__':
16     server=socketserver.ThreadingUDPServer(('127.0.0.1',10001),MyserverUDP)
17     server.serve_forever()

 

posted @ 2018-11-09 15:06  Maple_feng  阅读(328)  评论(0编辑  收藏  举报