在引入多线程之前首先看一下多任务:
所谓多任务,简单的说就是操作系统同时执行多个任务
真正的多任务是只能在多核CPU上才可以实现的,单核CPU只是切换速度比较快,根本不是真正的多任务
并行和并发概念
并行:当任务数小于CPU核数,每个任务占用一个CPU核,所有任务真正的一起执行
并发:当任务数大于CPU核数,操作系统通过调度算法,在各个任务之间进行切换,实现多任务"一起"执行,实际上只是切换的速度比较快,看起来一起执行而已
多线程
python底层已经封装好自己的模块threading,可以更加方便的使用,并且主线程会等待所有的子线程结束以后再结束
首先看一下多线程的使用
import threading
import time
def func1():
for i in range(3):
print("开发")
# 由于操作系统速度非常快,如果没有sleep是看不出效果的
time.sleep(1)
def func2():
for i in range(3):
print("测试")
time.sleep(1)
t1 = threading.Thread(target=func1)
t2 = threading.Thread(target=func2)
# 只有在调用start()方法之后才会真正的创建线程,并启动线程
t1.start()
t2.start()
输出:
开发
测试
开发
测试
开发
测试
一般来说多线程是比单线程快
单线程:
import threading
import time
def func1():
print("开发")
time.sleep(1)
start_time = time.time()
for i in range(5):
func1()
end_time = time.time()
print(end_time-start_time)
输出:
开发
开发
开发
开发
开发
5.0102128982543945
多线程:
import threading
import time
def func1():
print("开发")
time.sleep(1)
start_time = time.time()
for i in range(5):
t = threading.Thread(target=func1)
t.start()
t.join() # 主线程等待所有的子线程结束以后才会接着向下执行代码,即使调用join()方法,主线程也会等待子线程结束,只是end_time的时间就不是所有线程执行完以后的时间
end_time = time.time()
print(end_time-start_time)
输出:
开发
开发
开发
开发
开发
1.005455493927002
以上比较可以得出:单线程用了5秒而开了5个线程用了1秒,所以多线程比单线程快。。。
在cpython解释器中时存在GIL的,当一个进程中同时存在多个线程时,并不能真正实现多任务,尤其是在线程中存在I/O操作时GIL锁会解开而继续执行下一个线程,即使如此,在程序中存在I/O操作时多线程还是比单线程快。。。
注意:多线程的执行顺序是不确定的,
import threading
import time
def func1(num):
for i in range(num):
print("开发")
time.sleep(1)
def func2(num):
for i in range(num):
print("测试")
time.sleep(1)
t1 = threading.Thread(target=func1, args=(3,))
t2 = threading.Thread(target=func2, args=(3,))
t1.start()
t2.start()
输出:
开发
测试
测试
开发
测试
开发
从代码和执⾏结果我们可以看出,多线程程序的执⾏顺序是不确定的。当执 ⾏到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进⼊就 绪(Runnable)状态,等待调度。⽽线程调度将⾃⾏选择⼀个线程执⾏。
多线程之间共享全局变量
import threading
import time
def func1(num):
num.append(4)
print("func1 num {}".format(num))
def func2(num):
time.sleep(1)
print("func2 num {}".format(num))
num = [1,2,3]
t1 = threading.Thread(target=func1, args=(num, ))
t1.start()
t2 = threading.Thread(target=func2, args=(num, ))
t2.start()
输出:
func1 num [1, 2, 3, 4]
func2 num [1, 2, 3, 4]
总结:
1、在一个进程内所有的线程共享全局变量,有利于数据共享
2、线程是对全局变量随意遂改可能造成多线程之间对全局变量 的混乱(即线程⾮安全)
多线程共享全局变量可能出现的问题(经典的数字相加问题)
import threading
sum_num = 0
def func1(num):
global sum_num # 使用全局变量
for i in range(num):
sum_num += 1
print(sum_num)
def func2(num):
global sum_num
for i in range(num):
sum_num += 1
print(sum_num)
t1 = threading.Thread(target=func1, args=(1000000,))
t2 = threading.Thread(target=func2, args=(1000000,))
t1.start()
t2.start()
输出:
1031410
1183429
正常的结果应该是第一次加完以后是1000000,第二次加完以后是2000000,但是两次结果都没有符合预期的结果值,由于是多线程操作,可能的原因如下:
1、在sum_num=0时,t1取得sum_num=0。此时系统把t1调度为”sleeping”状态, 把t2转换为”running”状态,t2也获得g_num=0
2、然后t2对得到的值进⾏加1并赋给sum_num,使得sum_num=1
3、然后系统⼜把t2调度为”sleeping”,把t1转为”running”。线程t1⼜把它之 前得到的0加1后赋值给sum_num。
4、这样导致虽然t1和t2都对sum_num加1,但结果仍然是sum_num=1
结论:多个线程同时对一个全局变量进行操作会出现资源竞争问题,导致结果偏差
解决多线程资源竞争可以使用线程同步的方法,即互斥锁
互斥锁的概念:某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他 线程不能更改;直到该线程释放资源,将资源的状态变成“⾮锁定”,其他的 线程才能再次锁定该资源。互斥锁保证了每次只有⼀个线程进⾏写⼊操作, 从⽽保证了多线程情况下数据的正确性。
import threading
sum_num = 0
mutex = threading.Lock() # 创建一把锁
def func1(num):
global sum_num # 使用全局变量
for i in range(num):
mutex.acquire() # 上锁
sum_num += 1
mutex.release() # 解锁
print(sum_num)
def func2(num):
global sum_num
for i in range(num):
mutex.acquire()
sum_num += 1
mutex.release()
print(sum_num)
t1 = threading.Thread(target=func1, args=(1000000,))
t2 = threading.Thread(target=func2, args=(1000000,))
t1.start()
t2.start()
输出:
1937509
2000000
注意:
1、如果这个锁之前是没有上锁的,那么acquire不会堵塞 。
2、如果在调⽤acquire对这个锁上锁之前 它已经被 其他线程上了锁,那么 此时acquire会堵塞,直到这个锁被解锁为⽌。
3、加锁的位置不同,t1的输出结果可能有变化,但t2的结果是一定的,因为总共就加了2000000次,所以最终的结果肯定是2000000,如果把锁加在for循环的上边,即整个的相加都被锁定的话,那么t1是1000000,t2是2000000。
4、加锁以后,锁定的部分是单线程执行的,这样数据保证是安全的,但是效率会下降,这也是加锁的一个缺点。
死锁问题
在线程间共享多个资源的时候,如果两个线程分别占有⼀部分资源并且同时 等待对⽅的资源,就会造成死锁
import threading
import time
class T:
def func1(self):
mutex1.acquire() # mutex1 上锁
print("func1 mutex1 abc acquire")
mutex2.acquire() # mutex2 上锁
print("func1 mutex2 acquire")
mutex2.release() # mutex2 解锁
mutex1.release() # mutex1 解锁
def func2(self):
mutex2.acquire()
print("func2 mutex2 acquire")
time.sleep(1)
mutex1.acquire()
print("func2 mutex1 acquire")
mutex1.release()
mutex2.release()
def run(self):
self.func1()
self.func2()
mutex1 = threading.Lock()
mutex2 = threading.Lock()
for i in range(3):
t = threading.Thread(target=T().run)
t.start() # 创建3个线程
输出:
func1 mutex1 abc acquire
func2 mutex2 acquire
func2 mutex2 acquire
func1 mutex1 abc acquire
从以上结果可以看出,假如现在需要创建3个线程A、B、C,第一次假如A上锁mutex1,那么B和C线程只有等待A释放,此时A处于睡眠状态,可以轻松获取mutex2,当线程A执行完func1时,会把mutex1释放,在A执行func2时获取mutex2,假如是B抢到锁mutex1,这时A占用mutex2,需要获取mutex1才能向下执行,而B占用mutex1, 需要获取mutex2才能向下执行,此时就会形成一个死锁。。。
解决死锁的办法就是使用递归锁(RLock),只需要把mutex1 = threading.Lock()和mutex2= threading.Lock()
改为mutex1=mutex2=threading.RLock()即可解决问题