Python并发编程之多线程
一 什么是线程
线程,有时被称为轻量级进程(Lightweight Process,LWP),它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。线程没有自己的系统资源,它与同属一个进程的其他线程共享该进程所有的资源。
一个线程可以创建和撤销另外一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。
线程有就绪、阻塞和运行三种基本状态:
就绪:指线程具备运行的所有条件,逻辑上是可以运行的,在等待处理机;
阻塞:指线程在等待一个事件(如某个信号量),逻辑上不可执行;
运行:指线程占有处理机正在运行。
线程是程序中一个单一的顺序控制流程。在单个程序中同时运行多个线程完成不同的被划分为一块一块的工作,称为多线程。
二 为什么有了进程为什么还要线程
进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:
- 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了
- 进程在执行的过程中如果阻塞,例如:等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行
例如:我们在使用qq聊天, qq做为一个独立进程如果同一时间只能干一件事,那它如何实现在同一时刻既能监听键盘输入、又能监听收到的消息、同时还能把收到的消息显示在屏幕上?可能我们会想到操作系统不是有分时吗?但是分时是指在不同进程间的分时, 即操作系统处理一会你的qq任务,又切换到qq音乐任务上了,每个cpu时间片分给你的qq程序时,你的qq还是只能同时干一件事
线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一件事的缺陷,使进程内并发成为可能。
三 进、线程之间的区别
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。或者说进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程则是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

它们之间的区别
1. Threads share the address space of the process that created it; processes have their own address space. 2. Threads have direct access to the data segment of its process; processes have their own copy of the data segment of the parent process. 3. Threads can directly communicate with other threads of its process; processes must use interprocess communication to communicate with sibling processes. 4. New threads are easily created; new processes require duplication of the parent process. 5. Threads can exercise considerable control over threads of the same process; processes can only exercise control over child processes. 6. Changes to the main thread (cancellation, priority change, etc.) may affect the behavior of the other threads of the process; changes to the parent process does not affect child processes.
四 为何要用多线程
多线程指的是,在一个进程中开启多个线程,简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。详细的讲分为4点:
- 多线程共享一个进程的地址空间
- 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用
- 若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度
- 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python)
更多关于线程的理论知识可参考:http://www.cnblogs.com/linhaifeng/articles/7430082.html
五 threading模块
相对于thread,threading通过对thread模块进行二次封装,提供了更方便的API来操作线程
5.1 使用threading.Thread创建线程
Thread 是threading模块中最重要的类之一,可以使用它来创建线程。有两种方式来创建线程:
1. 创建一个threading.Thread对象,在它的初始化函数(__init__)中将可调用对象作为参数传入
2. 通过继承Thread类,重写它的run方法
实例代码如下:
import threading import time def num(n): # 定义每个线程要运行的函数 print("running on number:%s" % n) time.sleep(3) if __name__ == '__main__': t1 = threading.Thread(target=num, args=(10,)) # 生成一个线程实例 t2 = threading.Thread(target=num, args=(20,)) # 生成另一个线程实例 t1.start() # 启动线程 t2.start() # 启动另一个线程 print(t1.getName()) # 获取线程名 print(t2.getName()) print('主线程')
import threading import time class MyThread(threading.Thread): def __init__(self, num): threading.Thread.__init__(self) # super().__init__() self.num = num def run(self): # 定义每个线程要运行的函数 print("running on number:%s"%self.num) time.sleep(1) if __name__ == '__main__': t1 = MyThread(10) t2 = MyThread(20) t1.start() t2.start() print('主线程')

threading模块提供的一些其他方法:
threading.currentThread() # 返回当前的线程变量 threading.enumerate() # 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程 threading.activeCount() # 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
5.2 Thread类的实例方法
run() # 通常需要重写,编写代码实现需要的功能。定义线程功能的函数 getName() # 获得线程对象名称 setName() # 设置线程对象名称 start() # 启动线程,等待CPU调度 jion([timeout]) # 在子线程完成运行之前,这个子线程的父线程将一直被阻塞,timeout设置等待时间 setDaemon(bool) # 将线程声明为守护线程,设置子线程是否随主线程一起结束,必须在start()之前调用。默认为False。当其设置为True时,如果主线程退出,那么子线程也将跟着退出,反之,子线程将继续运行,直到正常退出 isDaemon() # 判断线程是否随主线程一起结束 isAlive() # 检查线程是否在运行中
import threading from time import ctime,sleep def func1(): print ("Func1 begin! %s" %ctime()) sleep(3) print("Func1 end! %s" %ctime()) def func2(): print("Func2 begin! %s" % ctime()) sleep(5) print("Func2 end! %s" % ctime()) threads = [] t1 = threading.Thread(target=func1,args=()) t2 = threading.Thread(target=func2,args=()) threads.append(t1) threads.append(t2) if __name__ == '__main__': # t2.setDaemon(True) # 开启后t1结束则结束 for t in threads: # t.setDaemon(True) # 注意:一定在start之前设置,开启后主进程结束则结束,等价于t.daemon = True t.start() # t.join() # 开启后需要等到func1执行完成后,才能执行func2 # t1.join() # 单独:func1开始-->func2开始-->func1结束-->主进程-->func2结束 # t2.join() # 单独(同时):func1开始-->func2开始-->func1结束-->func2结束-->主进程 print ("all over %s" %ctime())
更多threading模块介绍:https://docs.python.org/3/library/threading.html?highlight=threading#
5.3 在一个进程下开启多个线程与在一个进程下开启多个子进程的区别
from threading import Thread from multiprocessing import Process def work(): print('hello') if __name__ == '__main__': # 在主进程下开启线程 t = Thread(target=work) t.start() print('主进程下开启线程') ''' 打印结果: hello 主进程下开启线程 ''' # 由执行结构可以看出:几乎是t.start()的同时就将线程开启了,然后先打印出了hello,证明线程的创建开销小 # 在主进程下开启子进程 p = Process(target=work) p.start() print('主进程下开启子进程') ''' 打印结果: 主进程下开启子进程 hello ''' # 由执行结构可以看出:p.start()将开启进程的信号发给操作系统,操作系统要申请内存空间,拷贝父进程地址到子进程,开销远大于线程
from threading import Thread from multiprocessing import Process import os def work(): print('hello',os.getpid()) if __name__ == '__main__': # 在主进程下开启多个线程,每个线程都跟主进程的pid一样 t1 = Thread(target=work) t2 = Thread(target=work) t1.start() t2.start() print('主进程pid',os.getpid()) ''' 打印结果: hello 9708 hello 9708 主进程pid 9708 ''' # 开多个进程,每个进程都有不同的pid p1 = Process(target=work) p2 = Process(target=work) p1.start() p2.start() print('主进程pid',os.getpid()) ''' 打印结果: 主进程pid 9708 hello 14160 hello 7312 '''
from threading import Thread from multiprocessing import Process def work(): global n n = 0 if __name__ == '__main__': # 进程 # n = 1 # p = Process(target=work) # p.start() # p.join() # print('主',n) # 毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为1 # 线程 n = 1 t = Thread(target=work) t.start() t.join() print('主',n) # 查看结果为0,因为同一进程内的线程之间共享进程内的数据
5.4 实例练习
练习一:实现多线程并发的socket服务端
import socket import threading ip_port = ('127.0.0.1',8081) buf_size = 1024 tcp_server = socket.socket() tcp_server.bind(ip_port) tcp_server.listen(5) def run(conn,addr): while True: try: recv_data = conn.recv(buf_size) if not recv_data:break print('收到来自%s的消息:%s' %(addr, recv_data.decode('utf-8'))) conn.send(recv_data.upper()) except Exception: break if __name__ == '__main__': print('The server is ready...') while True: conn,addr = tcp_server.accept() t = threading.Thread(target=run,args=(conn,addr,)) t.start()
import socket ip_port = ('127.0.0.1',8081) buf_size = 1024 tcp_client = socket.socket() tcp_client.connect(ip_port) while True: send_msg = input('>>: ').strip() if not send_msg:continue tcp_client.send(send_msg.encode('utf-8')) recv_msg = tcp_client.recv(buf_size) print(recv_msg.decode('utf-8'))
练习二:三个任务,一个接收用户输入,一个将用户输入的内容格式化成大写,一个将格式化后的结果存入文件
from threading import Thread msg_list = [] format_list = [] def recv_msg(): while True: recv_data = input('>>:').strip() if not recv_data: continue msg_list.append(recv_data) def format_msg(): while True: if msg_list: ret = msg_list.pop() format_list.append(ret.upper()) def save_msg(): while True: if format_list: ret = format_list.pop() with open('db.txt', 'a', encoding='utf-8') as f: f.write('%s\n' % ret) if __name__ == '__main__': t_list = [] func_list = [recv_msg, format_msg, save_msg] for func in func_list: t = Thread(target=func) t_list.append(t) for t in t_list: t.start()
5.5 守护线程
其实在jion&setDaemon实例时,已经了解了守护进程,这里单独再说明一下:无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁
需要强调的是:运行完毕并非终止运行
1.对主进程来说,运行完毕指的是主进程代码运行完毕 2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
详细解释:
1.主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束 2.主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
from threading import Thread import time def func(): time.sleep(2) print('func执行') # 不会被执行 if __name__ == '__main__': t = Thread(target=func) t.setDaemon(True) # 必须在t.start()之前设置 t.start() print('主线程') print(t.is_alive()) ''' 执行结果: 主线程 True '''
from threading import Thread import time def func1(): print("Func1 begin!") time.sleep(1) print("Func1 end!") # 实际会打印,因为func2为非守护线程,主线程会等待func2的执行,在这个过程中func1也执行了 def func2(): print("Func2 begin!" ) time.sleep(3) print("Func2 end!") t1 = Thread(target=func1) t2 = Thread(target=func2) t1.daemon = True t1.start() t2.start() print("主线程")
5.6 线程锁
CPU执行任务时,在线程之间是进行随机调度的,并且每个线程可能只执行n条代码后就转而执行另外一条线程。由于在一个进程中的多个线程之间是共享资源和数据的,这就容易造成资源抢夺或脏数据,于是就有了锁的概念,限制某一时刻只有一个线程能访问某个指定的数据
import time import threading def sub_num(): global num # 在每个线程中都获取这个全局变量 time.sleep(0.2) num -= 1 # 对公共变量进行-1操作 num = 10 # 设定一个共享变量 thread_list = [] for i in range(10): t = threading.Thread(target=sub_num) t.start() thread_list.append(t) for t in thread_list: # 等待所有线程执行完毕 t.join() print('Result: ', num)
正常来讲,这个num结果应该是0, 但在python 2.7上多运行几次,会发现,最后打印出来的num结果不总是0,为什么每次运行的结果不一样呢? 很简单,假设你有A,B两个线程,此时都要对num 进行减1操作, 由于2个线程是并发同时运行的,所以2个线程很有可能同时拿走了num=10这个初始变量交给cpu去运算,当A线程去处完的结果是9,但此时B线程运算完的结果也是9,两个线程同时CPU运算的结果再赋值给num变量后,结果就都是9,导致最终结果错误
*注:不要在3.x上运行,不知为什么,3.x上的结果总是正确的,可能是自动加了锁
由上可见,由于线程同时访问一个数据,产生了错误的结果。为了解决这个问题,python在threading模块中定义了几种线程锁类,分别是:
- Lock 互斥锁(不可嵌套)
- RLock递归锁(可嵌套)常用
- Semaphore 信号量
- event 事件
- condition 条件
5.6.1 互斥锁 Lock(同步锁)
import threading,time def sub_num(): global num # 在每个线程中都获取这个全局变量 time.sleep(0.1) lock.acquire() # 修改数据前加锁 num -= 1 # 对此公共变量进行-1操作 lock.release() # 锁释放 num = 10 # 设定一个共享变量 thread_list = [] lock = threading.Lock() # 生成一个锁对象 for i in range(10): t = threading.Thread(target = sub_num) t.start() thread_list.append(t) for t in thread_list: # 等待所有线程执行完毕 t.join() print('Result: ', num)
GIL VS Lock
详细内容请参考另一篇文章:PYTHON GIL(全局解释器锁)
锁通常被用来实现对共享资源的同步访问。为每一个共享资源创建一个Lock对象,当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁:
import threading R=threading.Lock() R.acquire() ''' 对公共数据的操作 ''' R.release()
# Lock,执行时间:6.070462465286255 import time from threading import Thread,Lock def sub_num(): time.sleep(1) global num # 在每个线程中都获取这个全局变量 lock.acquire() # 加锁的代码串行运行 time.sleep(0.5) num -= 1 # 对公共变量进行-1操作 lock.release() num = 10 # 设定一个共享变量 lock = Lock() thread_list = [] start = time.time() for i in range(10): t = Thread(target=sub_num) t.start() thread_list.append(t) for t in thread_list: # 等待所有线程执行完毕 t.join() end = time.time() print(end-start) # 6.070462465286255 print('Result: ', num) # 数据安全 # join,执行时间:15.224017143249512 import time from threading import Thread,Lock def sub_num(): time.sleep(1) global num # 在每个线程中都获取这个全局变量 time.sleep(0.5) num -= 1 # 对公共变量进行-1操作 num = 10 # 设定一个共享变量 thread_list = [] start = time.time() for i in range(10): t = Thread(target=sub_num) t.start() t.join() # 所有线程变串行执行 thread_list.append(t) end = time.time() print(end-start) # 15.224017143249512 print('Result: ', num) # 数据安全
5.6.2 死锁与递归锁 RLock
所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
import threading import time lock1 = threading.Lock() lock2 = threading.Lock() class MyThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): self.func1() self.func2() def func1(self): lock1.acquire() # 如果锁被占用,则阻塞在这里,等待锁释放 print ("%s , get %s---%s" % (self.name, "lock1", time.time())) lock2.acquire() print ("%s , get %s---%s" % (self.name, "lock2", time.time())) lock2.release() lock1.release() def func2(self): lock2.acquire() print ("%s , get %s---%s" % (self.name, "lock1", time.time())) time.sleep(0.2) lock1.acquire() print ("%s , get %s---%s" % (self.name, "lock2", time.time())) lock1.release() lock2.release() if __name__ == "__main__": print("Program start----------%s" % time.time()) for i in range(10): my_thread = MyThread() my_thread.start()
在Python中为了支持在同一线程中多次请求同一资源,python提供了递归锁RLock。这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁
import threading import time rlock = threading.RLock() class MyThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): self.func1() self.func2() def func1(self): rlock.acquire() # 如果锁被占用,则阻塞在这里,等待锁释放 print ("%s , get %s---%s" % (self.name, "lock1", time.time())) rlock.acquire() print ("%s , get %s---%s" % (self.name, "lock2", time.time())) rlock.release() rlock.release() def func2(self): rlock.acquire() print ("%s , get %s---%s" % (self.name, "lock1", time.time())) time.sleep(0.2) rlock.acquire() print ("%s , get %s---%s" % (self.name, "lock2", time.time())) rlock.release() rlock.release() if __name__ == "__main__": print("Program start----------%s" % time.time()) for i in range(10): my_thread = MyThread() my_thread.start()
5.6.3 信号量 Semaphore
这种锁允许一定数量的线程同时更改数据,它不是互斥锁。
它管理一个内置的计数器,每当调用acquire()时内置计数-1;调用release()时内置计数+1;计数器不能小于0,当计数器为0时,acquire()将阻塞线程直到其他线程调用release()
实例:同时只有5个线程可以获得semaphore,即可以限制最大连接数为5,代码如下:
import threading import time semaphore = threading.Semaphore(5) def func(): if semaphore.acquire(): print (threading.currentThread().getName() + ' get semaphore') time.sleep(2) semaphore.release() for i in range(20): t1 = threading.Thread(target=func) t1.start()
注意:信号量与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程
互斥锁与信号量推荐博客:http://url.cn/5DMsS9r
5.6.4 事件 Event
线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。
事件机制:全局定义了一个“Flag”,如果“Flag”的值为False,那么当程序执行wait方法时就会阻塞,如果“Flag”值为True,那么wait方法时便不再阻塞

Event对象方法如下:
event.isSet() # 返回event的状态值 event.wait() # 如果 event.isSet()==False将阻塞线程 event.set() # 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度 event.clear() # 恢复event的状态值为False
import threading def func(e, i): print(i) e.wait() # 检测当前event是什么状态,如果是为False,则阻塞,如果为True则继续往下执行。 print(i + 10) event = threading.Event() for i in range(5): t = threading.Thread(target=func, args=(event, i)) t.start() # event.clear() # 默认为False,将状态设置为False while True: inp = input(">>>") if inp == "y": event.set() # 将状态设置为True break else: continue
threading.Event的wait方法还接受一个超时参数,默认情况下如果事件一直没有发生,wait方法会一直阻塞下去,而加入这个超时参数之后,如果阻塞时间超过这个参数设定的值之后,wait方法会返回。
5.6.5 条件 condition
类名:Condition,该机制会使得线程等待,只有满足某条件时,才释放n个线程。
import threading,time def con(): res = False r = input(">>:") if r == "y": res = True return res def func(lock, i): print(i) lock.acquire() time.sleep(0.1) lock.wait_for(con) # 这个方法接受一个函数的返回值 print(i + 10) lock.release() c = threading.Condition() for i in range(5): t = threading.Thread(target=func, args=(c, i,)) t.start()
import threading,time def func(num): con.acquire() con.wait() print("run the thread: %s" % num) con.release() if __name__ == '__main__': con = threading.Condition() for i in range(10): t = threading.Thread(target=func, args=(i,)) t.start() while True: inp = input('>>:') if inp == "q": break # 下面这三行是固定语法 con.acquire() con.notify(int(inp)) # 这个方法接收一个整数,表示让多少个线程通过 con.release() time.sleep(0.5)
5.7 定时器(Timer)
定时器,指定n秒后执行某操作
from threading import Timer def func(): print("hello, world") t = Timer(1, func) t.start() # 1秒后执行
from threading import Timer import random,time class Code: def __init__(self): self.make_cache() def make_cache(self,interval=5): """ 获取验证码并设置定时器 :param interval: 验证码失效时间 """ self.cache = self.make_code() print(self.cache) self.t = Timer(interval,self.make_cache) self.t.start() def make_code(self,n=4): """ 用于产生4位包含数字与字母的验证码 :param n: 验证码位数 :return: 验证码 """ res='' for i in range(n): s1 = str(random.randint(0,9)) s2 = chr(random.randint(65,90)) res += random.choice([s1,s2]) return res def check(self): while True: input_code = input('>>: ').strip() if input_code.upper() == self.cache: print('验证成功',end='\n') self.t.cancel() break if __name__ == '__main__': obj = Code() obj.check()
六 队列(queue)
使用import queue,用法与进程Queue一样
queue.Queue(maxsize=0) # 先进先出
import queue q = queue.Queue() q.put('first') q.put('second') q.put('third') print(q.get()) print(q.get()) print(q.get()) ''' 结果(先进先出): first second third '''
queue.LifoQueue(maxsize=0) # 先进后出
import queue q = queue.LifoQueue() q.put('first') q.put('second') q.put('third') print(q.get()) print(q.get()) print(q.get()) ''' 结果(后进先出): third second first '''
queue.PriorityQueue(maxsize=0) # 存储数据时可设置优先级的队列
import queue q = queue.PriorityQueue() # put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高 q.put((20,'a')) q.put((10,'b')) q.put((30,'c')) print(q.get()) print(q.get()) print(q.get()) ''' 结果(数字越小优先级越高,优先级高的优先出队): (10, 'b') (20, 'a') (30, 'c') '''

浙公网安备 33010602011771号