线程
线程和进程之间的比较
定义
-
进程是系统进行资源分配和调度的一个独立单位.
-
线程是进程的一个实体,是CPU调度和分派的基本单位
区别
-
一个程序至少有一个进程,一个进程至少有一个线程.
-
线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
-
进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
-
线线程不能够独立执行,必须依存在进程中
优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。
多线程
Thread模块的简单使用
rom threading import Thread
import time
def sayhello():
print('hello world')
time.sleep(1)
for i in range(10):
t = Thread(target=sayhello)
t.start()
- threading.enumerate()返回当前的线程对象列表
- threading.current_thread().name 获取当前线程的名字
将代码封装为类
为了让每个线程的封装性更完美,所以使用threading模块时,往往会定义一个新的子类class,只要继承threading.Thread就可以了,然后重写run方法
mport threading import time class MyThread(threading.Thread): def run(self): for i in range(3): print('{}线程执行第{}次'.format(self.name,i)) time.sleep(1) if __name__ == '__main__': # 创建三个线程 for i in range(3): my_t = MyThread() my_t.start()
说明
- python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过Thread类的start方法,可以启动该线程,交给python虚拟机进行调度,当该线程获得执行的机会时,就会调用run方法执行线程
线程的执行顺序
import threading import time class MyThread(threading.Thread): def run(self): for i in range(5): time.sleep(1) print('{}线程执行第{}次'.format(self.name,i)) if __name__ == '__main__': # 创建三个线程 for i in range(20): my_t = MyThread() my_t.start()
执行顺序可能变化
结果:
... Thread-3线程执行第4次 Thread-2线程执行第4次 Thread-4线程执行第4次 Thread-5线程执行第4次 Thread-6线程执行第4次 Thread-7线程执行第4次 Thread-1线程执行第4次 Thread-8线程执行第4次 ...
说明:
从代码和执行结果我们可以看出,多线程程序的执行顺序是不确定的。当执行到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进入就绪(Runnable)状态,等待调度。而线程调度将自行选择一个线程执行。上面的代码中只能保证每个线程都运行完整个run函数,但是线程的启动顺序、run函数中每次循环的执行顺序都不能确定。

总结
- 每个线程一定会有一个名字,尽管上面的例子中没有指定线程对象的name,但是python会自动为线程指定一个名字。
- 当线程的run()方法结束时该线程完成。
- 无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。
- 线程的几种状态,新建,就绪,运行,阻塞,死亡
线程之间共享全局变量的问题
import threading import time num = 100 def work1(): global num for i in range(3): num += 1 print('work---->num---->{}'.format(num)) time.sleep(1) def work2(): global num print("work2---->num is {}".format(num)) if __name__ == '__main__': # work1当中增加值 work2中打印 t1 = threading.Thread(target=work1) t1.start() t1.join() t2 = threading.Thread(target=work2) print('t2开始执行') t2.start()
结果
work---->num---->101 work---->num---->102 work---->num---->103 t2开始执行 work2---->num is 103
列表为全局变量时
import threading import time num = [11, 22, 33] def work1(): num.append(44) print('work1 in num of {}'.format(num)) def work2(): print('work2 in num of {}'.format(num)) t = threading.Thread(target=work1) t.start() time.sleep(1) t2 = threading.Thread(target=work2) t2.start()
结果:
work1 in num of [11, 22, 33, 44] work2 in num of [11, 22, 33, 44]
总结:
- 在一个进程内的所有线程共享全局变量,能够在不使用其他方式的前提下完成多线程之间的数据共享(这点要比多进程要好)
- 缺点就是,线程是对全局变量随意更改可能造成多线程之间对全局变量的混乱(即线程非安全)
同步
1. 多线程开发可能遇到的问题
假设两个线程t1和t2都要对num=0进行增1运算,t1和t2都各对num修改10次,num的最终的结果应该为20。
但是由于是多线程访问,有可能出现下面情况:
在num=0时,t1取得num=0。此时系统把t1调度为”sleeping”状态,把t2转换为”running”状态,t2也获得num=0。然后t2对得到的值进行加1并赋给num,使得num=1。然后系统又把t2调度为”sleeping”,把t1转为”running”。线程t1又把它之前得到的0加1后赋值给num。这样,明明t1和t2都完成了1次加1工作,但结果仍然是num=1。
import threading num = 0 def add_num1(): global num for i in range(1000000): num += 1 print('add_num1的num结果为:{}'.format(num)) def add_num2(): global num for i in range(1000000): num += 1 print('add_num2的num结果为:{}'.format(num)) t1 = threading.Thread(target=add_num1) t2 = threading.Thread(target=add_num2) t1.start() t2.start() t1.join() t2.join() print(num)
结果:
add_num1的num结果为:1321462 add_num2的num结果为:1386698 1386698
问题产生的原因就是没有控制多个线程对同一资源的访问,对数据造成破坏,使得线程运行的结果不可预期。这种现象称为“线程不安全”。
同步的概念
同步就是协同步调,按预定的先后次序进行运行
这里的同步跟现实生活当中为相反的,现实生活当中的同步为一起,你我一起执行,程序当中的同步为,你执行完我执行,协同执行
互斥锁
解决线程不安全思路:让线程同步
- 系统调用t1,然后获取到num的值为0,此时上一把锁,即不允许其他现在操作num
- 对num的值进行+1
- 解锁,此时num的值为1,其他的线程就可以使用num了,而且是num的值不是0而是1
- 同理其他线程在对num进行修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证了数据的正确性
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁为资源引入一个状态:锁定/非锁定。
互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,可以方便的处理锁定:
#创建锁 mutex = threading.Lock() #锁定 mutex.acquire([blocking]) #释放 mutex.release()
其中,锁定方法acquire可以有一个blocking参数。
- 如果设定blocking为True,则当前线程会堵塞,直到获取到这个锁为止(如果没有指定,那么默认为True)
- 如果设定blocking为False,则当前线程不会堵塞
使用互斥锁实现以上的案例:
import threading num = 0 def add_num1(): global num for i in range(1000000): mutex.acquire() num += 1 mutex.release() print('add_num1的num结果为:{}'.format(num)) def add_num2(): global num for i in range(1000000): # 获取锁 mutex.acquire() num += 1 # 释放锁 mutex.release() print('add_num2的num结果为:{}'.format(num)) # 创建锁 mutex = threading.Lock() t1 = threading.Thread(target=add_num1) t2 = threading.Thread(target=add_num2) t1.start() t2.start() t1.join() t2.join() print(num)
上锁解锁过程
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
总结
锁的好处:
- 确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
- 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
- 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
死锁
死锁
产生原因:在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
多线程互斥访问共享资源时,如果锁使用不当,就可能出现死锁,造成应用的停止响应。下面看一个死锁的例子
from threading import Lock, Thread import time def demo1(): print('demo01---_<_>_---start') res_a = mutex_A.acquire() if res_a: print('\tdemo1---获取了A锁') time.sleep(1) print('demo1正在获得B锁') res_b = mutex_B.acquire() if res_b: print('\tdemo1---获取了B锁') mutex_B.release() mutex_A.release() print('demo1执行结束') def demo2(): print('demo02---_<_>_---start') res_a = mutex_B.acquire() if res_a: print('\tdemo2---获取了B锁') print('demo2正在获得A锁') res_b = mutex_A.acquire(timeout=2) # 添加超时时间可避免死锁的产生 if res_b: print('\tdemo2---获取了A锁') mutex_A.release() mutex_B.release() print('demo2执行结束') mutex_A = Lock() mutex_B = Lock() t1 = Thread(target=demo1) t2 = Thread(target=demo2) t1.start() t2.start() t1.join() t2.join()
实现多个线程同步执行
1 from threading import Lock, Thread 2 3 import time 4 5 # 利用三把锁 在没有启动线程时现将b,c锁获取到,,,然后在demo_A当中获取a,释放b,在demo_B当中获取b,释放c即可达到同步 6 def demo_A(): 7 while 1: 8 mutex_a.acquire() 9 print('A') 10 mutex_b.release() 11 time.sleep(1) 12 13 14 def demo_B(): 15 while 1: 16 mutex_b.acquire() 17 print('B') 18 mutex_c.release() 19 time.sleep(1) 20 21 def demo_C(): 22 while 1: 23 mutex_c.acquire() 24 print('C') 25 mutex_a.release() 26 time.sleep(1) 27 28 29 mutex_a = Lock() 30 mutex_b = Lock() 31 mutex_c = Lock() 32 33 34 if __name__ == '__main__': 35 ta = Thread(target=demo_A) 36 tb = Thread(target=demo_B) 37 tc = Thread(target=demo_C) 38 39 mutex_b.acquire() 40 mutex_c.acquire() 41 42 ta.start() 43 tb.start() 44 tc.start()
总结
- 可以使用互斥锁完成多个任务,有序的进程工作,这就是线程的同步
生产者与消费者模式
Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么就做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。
1 import random 2 from queue import Queue 3 from threading import Thread 4 5 # 创建生产着类 6 import time 7 8 9 class Producer(Thread): 10 def __init__(self, name): 11 Thread.__init__(self) 12 self.name = name 13 14 def run(self): 15 """""" 16 i = 1 17 while 1: 18 if not my_queue.full(): 19 my_queue.put(i) 20 print('{}已生产数据{}'.format(self.name, '编号:{}'.format(i))) 21 print('\t当前库存{}'.format(my_queue.qsize())) 22 i += 1 23 time.sleep(random.random()*2) 24 25 26 # 创建消费者类 27 class Consumer(Thread): 28 def __init__(self, name): 29 Thread.__init__(self) 30 self.name = name 31 32 def run(self): 33 while 1: 34 if not my_queue.empty(): 35 msg = my_queue.get() 36 print('{}消费数据{}'.format(self.name, msg)) 37 print('\t当前库存为{}'.format(my_queue.qsize())) 38 time.sleep(random.random()*2) 39 40 41 my_queue = Queue(10) 42 43 # 创建线程 44 # 三个生产者 45 for i in range(3): 46 p = Producer('Producer-'+str(i+1)) 47 p.start() 48 49 50 # 两个消费者 51 for i in range(2): 52 c = Consumer('Consumer-'+str(i+1)) 53 c.start()
3. Queue的说明
- 对于Queue,在多线程通信之间扮演重要的角色
- 添加数据到队列中,使用put()方法
- 从队列中取数据,使用get()方法
- 判断队列中是否还有数据,使用qsize()方法
4. 生产者消费者模式的说明
- 为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
- 什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
这个阻塞队列就是用来给生产者和消费者解耦的。
http://www.jb51.net/article/87385.htm
多线程之间的局部变量
在多线程开发中,全局变量是多个线程都共享的数据,而局部变量等是各自线程的,是非共享的
Threading.local
from threading import Thread import threading # 利用threading的local类可以 解决 线程内调用其他函数时不断传参数的问题 # 原理就是利用字典 用线程名字作为key值作为value def demo(name): my_local.name = name demo_mid() def demo_mid(): demo_in() def demo_in(): name = my_local.name print(threading.current_thread().name, '---->', name) my_local = threading.local() t1 = Thread(target=demo, args=('小明',)) t2 = Thread(target=demo, args=('小红',)) t1.start() t2.start()
说明
全局变量my_local就是一个ThreadLocal对象,每个Thread对它都可以读写name属性,但互不影响。你可以把my_local看成全局变量,但每个属性如my_local.name都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。
可以理解为全局变量my_local是一个dict,不但可以用my_local.name,还可以绑定其他变量,如my_local.age等等。
ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
小结
一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题
GIL问题
GIL是CPython解释器设计遗留下的历史问题。
Python官方实现解释器CPython,存在一个GIL(Global Interpreter Lock)全局解释器锁机制:
任何线程在运行之前必须获取这个全局锁才能执行,每当执行完100条字节码,全局解释器锁就会释放,切换到其他线程执行。
这样保证了多线程访问公共资源的安全性。
不过GIL使得Python中的多线程不能充分利用多核计算机的优势,无论有多少个核,同一时间只有一个线程能得到全局解释器锁,即只有一个线程能够运行。
如果要真正利用多核处理器优势,需重写一个不带GIL的解释器。像JPython和IronPython这样的解释器虽然没有GIL问题,但由于使用Java、C#语言实现,不能很好使用众多C语言实现扩展模块,所以使用不如CPython广泛。
利用处理器多核优势方式:
- 使用多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
- 如果一定要通过多线程利用多核,那只能通过C扩展来实现,不够简单易用。
计算密集型和IO密集型。
我们可以把任务分为计算密集型和IO密集型。
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。

浙公网安备 33010602011771号