python的thread模块是⽐较底层的模块,python的threading
模块是对thread做了⼀些包装的,可以更加⽅便的被使⽤
# –*– coding: utf-8 –*–
# @Time : 2019/1/7 22:21
# @Author : Damon_duanlei
# @FileName : thread_test01.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
import threading
import time
def hello_bye(name):
print("hello! {}".format(name))
time.sleep(1)
print("bye! {}".format(name))
if __name__ == '__main__':
t1 = threading.Thread(target=hello_bye, args=("臭臭",))
t2 = threading.Thread(target=hello_bye, args=("小迪",))
t1.start()
t2.start()
运行结果:
>>>
hello! 臭臭
hello! 小迪
bye! 臭臭
bye! 小迪
# –*– coding: utf-8 –*–
# @Time : 2019/1/7 22:29
# @Author : Damon_duanlei
# @FileName : thread_test02.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
import threading
import time
def hello_bye(name):
print("hello! {}".format(name))
time.sleep(1)
print("bye! {}".format(name))
class MyThread(threading.Thread):
def __init__(self, func, name):
threading.Thread.__init__(self)
self.func = func
self.name = name
def run(self):
self.func(self.name)
if __name__ == '__main__':
t1 = MyThread(hello_bye, "臭臭")
t2 = MyThread(hello_bye, "小迪")
t1.start()
t2.start()
运行结果:
>>>
hello! 臭臭
hello! 小迪
bye! 臭臭
bye! 小迪
使用继承的方式重写run()方法,start()时不走父类的run()方法,而是走子类重写的run()方法.使用threading.Thread直接创建,start()的时候走Thread类下的run()方法,该run()方法会主动调用 target 函数. 所以使用继承方式调用多线程需要将业务逻辑写入重写后的run()方法.
# –*– coding: utf-8 –*–
# @Time : 2019/1/13 11:07
# @Author : Damon_duanlei
# @FileName : thread_runing_order.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
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)
print(msg)
def test():
for i in range(5):
t = MyThread()
t.start()
if __name__ == '__main__':
test()
运行结果:( 运行的结果可能不一样, 但是大体一致)
>>>
I'm Thread-1 @ 0
I'm Thread-5 @ 0
I'm Thread-3 @ 0
I'm Thread-2 @ 0
I'm Thread-4 @ 0
I'm Thread-1 @ 1
I'm Thread-5 @ 1
I'm Thread-4 @ 1
I'm Thread-3 @ 1
I'm Thread-2 @ 1
I'm Thread-1 @ 2
I'm Thread-4 @ 2
I'm Thread-5 @ 2
I'm Thread-3 @ 2
I'm Thread-2 @ 2
结论:
多线程程序的执行顺序是不确定的. 当执行到 sleep 语句时, 线程将被阻塞( Blocked ), 到 sleep 结束后, 线程进入就绪状态( Runable ), 等待调度. 而线程调度间自行选择一个线程执行. 上面代码中能保证每个线程都运行完整个 run 函数, 但是线程的启动顺序及 run 函数中每次循环的执行顺序都不能确定.
python 中主线程会等待所有的子线程都执行结束后才结束, 有时实际开发需要子线程开启后, 主线程等待子线程执行结束后再继续向下执行,可以使用 线程.join()
方法进行阻塞.(以下示例代码均使用直接调用线程的方法)
# –*– coding: utf-8 –*–
# @Time : 2019/1/7 22:21
# @Author : Damon_duanlei
# @FileName : thread_test01.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
import threading
import time
def hello_bye(name, thread_name):
print("线程{}启动...".format(thread_name))
print("hello! {}".format(name))
time.sleep(3)
print("bye! {}".format(name))
time.sleep(3)
print("线程{}结束...".format(thread_name))
if __name__ == '__main__':
t1 = threading.Thread(target=hello_bye, args=("臭臭", "t1"))
t2 = threading.Thread(target=hello_bye, args=("小迪", "t2"))
print("程序开始执行")
t1.start()
# t1.join()
print("主线程继续向下执行")
# t1.join()
t2.start()
t1.join()
t2.join()
print("主线程执行结束")
小伙伴可以尝试 t1.join()
三处位置不同执行的结果有什么差异.
结论:
线程名.join()添加在何处, 主线程就阻塞在何处等待该子线程执行结束后方解阻塞. 根据业务需求可以将线程的开启和添加阻塞写为类似以下代码结构:
thread_list = []
t1 = threading.Thread(target=hello_bye, args=("臭臭", "t1"))
thread_list.append(t1)
t2 = threading.Thread(target=hello_bye, args=("小迪", "t2"))
thread_list.append(t2)
t3 = threading.Thread(target=hello_bye, args=("笨笨", "t3"))
thread_list.append(t3)
t4 = threading.Thread(target=hello_bye, args=("白克", "t4"))
thread_list.append(t4)
for t in thread_list:
t.start()
for t in thread_list:
t.join()
# –*– coding: utf-8 –*–
# @Time : 2019/1/13 11:28
# @Author : Damon_duanlei
# @FileName : thread_globle_var.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
from threading import Thread
import time
g_num = 100
def work1():
global g_num
for i in range(3):
g_num += 1
print("----in work1, g_num is %d---" % g_num)
def work2():
global g_num
print("----in work2, g_num is %d---" % g_num)
if __name__ == '__main__':
print("---线程创建之前g_num is %d---" % g_num)
t1 = Thread(target=work1)
t1.start()
# 延时,保证t1线程中的事情做完
time.sleep(1)
t2 = Thread(target=work2)
t2.start()
运行结果:
>>>
---线程创建之前g_num is 100---
----in work1, g_num is 103---
----in work2, g_num is 103---
结论:在一个进程内所有线程共享全局变量, 很方便多个线程间共享数据, 缺点就是,线程对全局变量随意修改可能造成多线程之间全局变量的混乱( 即线程非安全)
假设两个线程 t1 和 t2 都要对全局变量进行加1运算(g_num = 0), t1 和 t2 都各自对同一个全局变量加10次, g_num 的结果应该为20. 但是由于多线程同时操作, 很有可能出现下面的情况:
在g_num = 0 时, t1 取得 g_num = 0. 此时系统把 t1 调度为 sleeping 状态, 把t2转换为 “running” 状态, t2 也获得 g_num = 0. 然后 t2 对得到的值进行加 1 并赋值给 g_num, 使得 g_num = 1. 然后系统又把 t2 调度为 sleeping 状态, 把 t1 转换为 running . 线程 t1 有吧他之前得到的0加1后赋值给 g_num. 这样导致虽然 t1和 t2 都对 g_num 加1 , 但结果仍然是 g_num = 1
测试
# –*– coding: utf-8 –*–
# @Time : 2019/1/13 14:13
# @Author : Damon_duanlei
# @FileName : test_01.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
import threading
g_num = 0
def work1(num):
global g_num
for i in range(num):
g_num += 1
print("----in work1, g_num is %d---" % g_num)
def work2(num):
global g_num
for i in range(num):
g_num += 1
print("----in work2, g_num is {}---".format(g_num))
print("---线程创建之前g_num is {}---".format(g_num))
t1 = threading.Thread(target=work1, args=(1000000,))
t1.start()
t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()
t1.join()
t2.join()
print("2个线程对同⼀个全局变量操作之后的最终结果是:{}".format(g_num))
运行结果:
---线程创建之前g_num is 0---
----in work1, g_num is 1333706---
----in work2, g_num is 1450558---
2个线程对同⼀个全局变量操作之后的最终结果是:1450558
结论:
同步就是协同步调, 按预定的先后次序进行运行. "同"字从字面上容易理解为一起动作,其实不是, "同"字应是指协同, 协助, 相互配合. 如: 进程,线程同步, 可理解为进程或线程A 和 B 一块配合, A执行到一定程度时要依靠 B 的某个结果, 于是停下来, 示意B 运行; B执行,得到结果后, 再将结果给A; A 再继续操作.
思路如下:
当多个线程几乎同时修改某个共享数据的时候, 需要进行同步控制. 线程同步能够保证多个线程安全访问竞争资源, 最简单的同步机制是引入互斥锁
互斥锁为资源引入一个状态: 锁定/非锁定
某个线程要更改共享数据时, 先将其锁定, 此时资源的状态为"锁定",其他线程不能更改; 直到该线程释放资源, 将资源的状态变成 “非锁定”, 其他的线程才能再次锁定该资源. 互斥锁保证了每次只有一个线程进行写入操作, 从而保证了多线程情况下数据的准确性.
threading 模块中定义了 Lock 类, 可以方便的处理锁定:
# 创建锁
lock = threading.Lock()
# 锁定
lock.acquire()
# 释放锁
lock.release()
注意:
对上文测试代码引入互斥锁后代码及运行结果:
# –*– coding: utf-8 –*–
# @Time : 2019/1/13 14:47
# @Author : Damon_duanlei
# @FileName : thread_lock.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
import threading
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={}".format(g_num))
def test2(num):
global g_num
for i in range(num):
mutex.acquire() # 上锁
g_num += 1
mutex.release() # 解锁
print("---test2---g_num={}".format(g_num))
# 创建⼀个互斥锁
# 默认是未上锁的状态
mutex = threading.Lock()
# 创建2个线程,让他们各⾃对g_num加1000000次
t1 = threading.Thread(target=test1, args=(1000000,))
t1.start()
t2 = threading.Thread(target=test2, args=(1000000,))
t2.start()
# 等待计算完成
t1.join()
t2.join()
print("2个线程对同⼀个全局变量操作之后的最终结果是:{}".format(g_num))
运行结果:
>>>
---test2---g_num=1997389
---test1---g_num=2000000
2个线程对同⼀个全局变量操作之后的最终结果是:2000000
可以看到最后的结果, 加入互斥锁后, 其结果与预期相符.
锁的好处:
锁的坏处:
避免死锁:
GIL是什么?为什么会有GIL? 网上有茫茫多的答案, 感兴趣的小伙伴请自行了解, 总之,因为 GIL 的存在Cpython的解释器同时只有一个线程运行. 因此python的多线程在面对计算密集型( CPU密集型 ) 相比单线程没有效率优势,甚至在python2.7之前面对计算密集型多线程效率远远低于单线程效率. 但是Cpython的多线程并非鸡肋, 在IO密集型程序中,因多次进行IO操作反复对线程进行阻塞,在等待阻塞的时间系统可以将线程调用在非阻塞的地方.所以在IO密集型程序中python多线程效率远远大于单线程效率.
待续 …