多线程
多任务的概念
什么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边看电影,一边聊QQ,一边在用Word赶作业,这就是多任务,这时至少同时有3个任务正在运行。
单核CPU如何执行多任务? 多核CPU如何执行多任务?
真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
注意:
-
并发:同时发起,单个执行 指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
-
并行:同时发起,同时执行(4核,4个任务) 指的是任务数小于等于cpu核数,即任务真的是一起执行的
线程:是cpu执行的一个基本单元,暂用的资源非常少,并且线程和线程之间的资源
是共享的,线程是依赖于进程而存在的,多线程一般适用于I/O密集型操作,线程的
执行是无序的
在python语言中,并不能够正真意义上实现多线程,因为CPython解释器
有一个全局的GIL解释器锁,来保证同一时刻只有一个线程在执行
线程
python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用
- 使用threading模块
单线程执行
import time
def saySorry():
for i in range(5):
print("亲爱的,我错了,我能吃饭了吗?")
time.sleep(1)
def saydo():
for i in range(5):
print("亲爱的,我错了,我给你按摩")
time.sleep(1)
if __name__ == "__main__":
saySorry()
saydo()
多线程执行
import threading
import time
def saySorry():
for i in range(5):
print("亲爱的,我错了,我能吃饭了吗?")
time.sleep(1)
def do():
for i in range(5):
print("亲爱的,我错了,我给你按摩")
time.sleep(1)
if __name__ == "__main__":
td1 = threading.Thread(target=saySorry)
td1.start() #启动线程,即让线程开始执行
td2 = threading.Thread(target=do)
td2.start() #启动线程,即让线程开始执行
运行结果
"""
亲爱的,我错了,我能吃饭了吗?
亲爱的,我错了,我给你按摩
亲爱的,我错了,我给你按摩
亲爱的,我错了,我能吃饭了吗?
亲爱的,我错了,我给你按摩
亲爱的,我错了,我能吃饭了吗?
亲爱的,我错了,我能吃饭了吗?
亲爱的,我错了,我给你按摩
亲爱的,我错了,我给你按摩
亲爱的,我错了,我能吃饭了吗?
"""
threading.Thread参数介绍
-
target:线程执行的函数
-
name:线程名称
-
args:为目标函数,传递参数,元组类型 另外:注意daemon参数
-
如果某个子线程的daemon属性为False,主线程结束时会检测该子线程是否结束,如果该子线程还在运行,则主线程会等待它完成后再退出;
-
如果某个子线程的daemon属性为True,主线程运行结束时不对这个子线程进行检查而直接退出,同时所有daemon值为True的子线程将随主线程一起结束,而不论是否运行完成。
-
属性daemon的值默认为False,如果需要修改,必须在调用start()方法启动线程之前进行设置
创建线程
thread_sub1 = Thread(target=download_image,name="下载线程")
是否守护进程(在开启线程之前设置)
daemon:False,在主线程结束的时候,会检测子线程任务是否结束,
如果子线程中任务没有结束,则会让子线程正常结束任务
daemon:True 在主线程结束的时候,会检测子线程任务是否结束,
如果子线程中任务没有结束,则会让子线程跟随主线程一起结束
thread_sub1.daemon = False
开启线程
thread_sub2.start()
join():阻塞,等待子线程中的任务执行完毕后,再回到主线程中继续执行
thread_sub1.join()
线程-注意点
线程执行代码的封装 通过上一小节,能够看出,通过使用threading模块能完成多任务的程序开发,为了让每个线程的封装性更完美,所以使用threading模块时,往往会定义一个新的子类class,只要继承threading.Thread就可以了,然后重写run方法
import threading
import time
class MyThread(threading.Thread):
def run(self):
for i in range(3):
time.sleep(1)
msg = "I'm "+self.name+' @ '+str(i) #name属性中保存的是当前线程的名字
print(msg)
if __name__ == '__main__':
t = MyThread()
t.start()
多线程-共享全局变量
元组当做实参传递到线程中
from threading import Thread
import time
def work1(nums):
nums.append(44)
print("----in work1---",nums)
def work2(nums):
#延时一会,保证t1线程中的事情做完
time.sleep(1)
print("----in work2---",nums)
g_nums = [11,22,33]
t1 = Thread(target=work1, args=(g_nums,))
t1.start()
t2 = Thread(target=work2, args=(g_nums,))
t2.start()
运行结果:
"""
----in work1--- [11, 22, 33, 44]
----in work2--- [11, 22, 33, 44]
"""
缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)
互斥锁(重点)
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁为资源引入一个状态:锁定/非锁定
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,可以方便的处理锁定
# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire()
# 释放
mutex.release()
- 如果这个锁之前是没有上锁的,那么acquire不会堵塞
- 如果在调用acquire对这个锁上锁之前 它已经被 其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止
使用互斥锁完成2个线程对同一个全局变量各加100万次的操作
import threading
import time
g_num = 0
def test1(num):
global g_num
for i in range(num):
mutex.acquire() # 上锁
g_num += 1
mutex.release() # 解锁
print("---test1---g_num=%d"%g_num)
def test2(num):
global g_num
for i in range(num):
mutex.acquire() # 上锁
g_num += 1
mutex.release() # 解锁
print("---test2---g_num=%d"%g_num)
# 创建一个互斥锁
# 默认是未上锁的状态
mutex = threading.Lock()
# 创建2个线程,让他们各自对g_num加1000000次
p1 = threading.Thread(target=test1, args=(1000000,))
p1.start()
p2 = threading.Thread(target=test2, args=(1000000,))
p2.start()
p1.join()
p2.join()
print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)
"""
---test1---g_num=1935969
---test2---g_num=2000000
2个线程对同一个全局变量操作之后的最终结果是:2000000
"""
不加锁(每次结果都不一样)
"""
---test2---g_num=1222294
---test1---g_num=1616727
2个线程对同一个全局变量操作之后的最终结果是:1616727
"""
可以看到最后的结果,加入互斥锁后,其结果与预期相符。
上锁解锁过程 当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态
锁的好处:
- 确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
-
阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
-
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
死锁问题
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
尽管死锁很少发生,但一旦发生就会造成应用的停止响应。下面看一个死锁的例子
import threading
import time
class MyThread1(threading.Thread):
def run(self):
# 对mutexA上锁
mutexA.acquire()
# mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
print(self.name+'----do1---up----')
time.sleep(1)
# 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
mutexB.acquire()
print(self.name+'----do1---down----')
mutexB.release()
# 对mutexA解锁
mutexA.release()
class MyThread2(threading.Thread):
def run(self):
# 对mutexB上锁
mutexB.acquire()
# mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
print(self.name+'----do2---up----')
time.sleep(1)
# 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
mutexA.acquire()
print(self.name+'----do2---down----')
mutexA.release()
# 对mutexB解锁
mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
网友评论