目录
1. 多线程简单介绍
2. threading模块介绍
2.1 threading模块常用方法
2.2 Thread类使用
2.2.1 使用构造函数传递可调用对象的方法创建线程
2.2.2 继承threading.Thread类
3. 多线程程序中使用(共享)全局变量
4. 多线程共享全局变量遇到的问题
5. 线程同步
5.1 线程同步的解决机制:互斥锁
5.2 互斥锁可能造成的问题--死锁
活动地址:CSDN21天学习挑战赛
**
**
多线程,或者说多任务,指的是操作系统同时运行多个任务。例如,听歌、洗衣服、看视频可以同时进行,这种就是多任务。
单核CPU执行多任务:操作系统轮流让各个任务交替执行,任务1执行t1时间,切换到任务2,任务2执行t2时间,再切换到任务3,执行t3时间...如此反复执行,表面上看,每个任务交替执行,但是由于CPU的执行速度很快,在人看来就好像所有任务同时执行。
这里需要注意并发和并行的概念:
python中提供了thread和threading模块对线程进行操作,其中thread模块是比较底层的模块,threading模块对thread做了一些包装,使用更方便。
注:线程标识是一个非负整数,并无特殊含义,只是用来标识线程,该整数可能会被循环利用,python3.3及以后版本支持该方法。
threading模块提供了Thread、Lock、RLock、Condition、Event、Timer和Semaphore等类支持多线程。
Thread是threading提供的最重要也是最基本的类,可以通过该类创建线程并控制线程的运行。
使用Thread创建线程,有两种方式:
threading.Thread类常用的方法和属性如下
方法名 | 说明 |
start() | 启动线程 |
run() | 线程代码,用来实现线程的功能和业务逻辑,可以在子类中重写该方法自定义线程的行为 |
init(self,group=None,target=None,name=None,args=(),kwargs=None,daemon=None) | 构造函数 |
is_alive() | 判断线程是否存货 |
getName() | 返回线程名 |
setName() | 设置线程名 |
isDaemon() | 判断线程是否为守护线程 |
setDaemon() | 设置线程是否为守护线程 |
常用属性
属性名 | 说明 |
name | 读取或设置线程的名字 |
ident | 线程标识,用非0数字或None(线程未被启动) |
daemon | 标识线程是否为守护线程,默认为False |
join(timeout=None) | 当timeout=None时,会等待至线程结束; 否则,会等待至timeout时间结束,单位为秒 |
threading.Thread类的构造函数声明如下:
threading.Thread(group=None,
target=None,
name=None,
args=(),
kwargs={},
*,
daemon=None)
参数 group 通常默认,作为扩展ThreadGroup类保留。
参数 target 用于run()方法调用的可调用对象,默认为None,标识run()不调用任何对象。
参数 name 线程名称,默认是Thread-N格式狗策划给你的唯一名称,N是十进制数。
参数 args 用于调用目标函数的参数元组,默认是()。
参数 kwargs 用于调用目标函数的关键字参数字典,默认为{}。
参数 daemon 设置线程是否为守护模式,默认为None。
示例1 :使用threading模块创建单线程
import threading
import time
def sing():
print("正在唱歌...")
time.sleep(1)
if __name__ == "__main__":
print("======================调用函数======================")
print("开始时间:%s"%time.ctime())
for i in range(10):
sing()
print("结束时间:%s"%time.ctime())
print("==========threading.Thread.start()开启单线程==========")
print("开始时间:%s"%time.ctime())
for i in range(10):
t = threading.Thread(target=sing)
t.start()
print("结束时间:%s"%time.ctime())
显示结果,可以看出来使用threading.Thread启动线程时,使用的时间要短很多。
当调用start()时,才开始真正的创建线程,并开始执行。
示例2:使用threading.Thread创建多线程,并查看当前处于active状态的线程的数量。
import threading
from time import ctime,sleep
def sing():
for i in range(3):
print("singing...%d"%i)
sleep(1)
def dance():
for i in range(3):
print("dancing...%d"%i)
sleep(1)
if __name__ == "__main__":
print("---start---:%s"%ctime())
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
while True:
length = len(threading.enumerate())
print("current thread has %d"%length)
if length <= 1:
break
sleep(0.5)
print("---end---:%s"%ctime())
显示结果:
在使用threading模块时,也可以定义一个新的类class,继承threading.Thread,然后重写run()。
threading.Thread类中的run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。创建自己的线程实例后,通过Thread类的start方法,可以启动该线程。
示例1:创建一个继承自threading.Thread类的子类
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()
显示结果:
示例2:观察线程执行的顺序
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)
def test():
for i in range(5):
t = MyThread()
t.start()
if __name__ == "__main__":
test()
显示结果:
每次运行时显示的执行顺序是不确定的。当执行到sleep()的时候,当前线程被阻塞,sleep()结束后当前线程进入就绪状态,等待调度。
线程调度会自行选择一个线程执行。
如上代码只能保证每个线程都能运行完整个run函数,但线程的启动顺序、run函数中每次循环的执行顺序都不能确定。
综上总结:
在一个进程中的所有线程共享全局变量,很方便在多个线程间共享数据;然而,线程对全局变量随意更改可能造成多线程之间对全局变量的混乱。
示例1:多线程共享全局变量
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 = %d"%g_num)
def work2():
global g_num
print("----in work2,g_num = %d"%g_num)
print("----线程创建之前g_num = %d"%g_num)
t1 = Thread(target=work1)
t1.start()
time.sleep(1)
t2 = Thread(target=work2)
t2.start()
显示结果,可以看到在work1()中改变了全局变量g_num的值,在work2中g_num也显示了变化。
示例2:列表当作实参传递到线程中
from threading import Thread
import threading
import time
def work1(nums):
nums.append(44)
print("----in work1----",nums)
def work2(nums):
time.sleep(1)
print("----in work2----",nums)
g_nums = [11,22,33]
t1 = threading.Thread(target=work1,args=(g_nums,))
t1.start()
t2 = threading.Thread(target=work2,args=(g_nums,))
t2.start()
显示结果:
----in work1---- [11, 22, 33, 44]
----in work2---- [11, 22, 33, 44]
多线程开发可能遇到的问题:假设线程t1和t2都要对全局变量g_num(默认是0)进行加1运算,t1和t2都各对g_num加10次,g_num的最终结果应该为20。
现在测试下
import threading
import time
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 %d---"%g_num)
print("---创建线程之前g_num is %d"%g_num)
t1 = threading.Thread(target=work1,args=(100000,))
t1.start()
t2 = threading.Thread(target=work2,args=(100000,))
t2.start()
while len(threading.enumerate()) != 1:
time.sleep(1)
print("---两个线程对同一个全局变量操作之后的最终结果是:%d"%g_num)
显示结果,t1和t2都对g_num进行加1运算,各进行100000次,那么期待的最终结果应该是200000,然而实际测试的结果不是200000,很显然是有问题的。
---创建线程之前g_num is 0
----in work1,g_num is 100000---
----in work2,g_num is 155034---
---两个线程对同一个全局变量操作之后的最终结果是:155034
多线程同时操作时,会出现资源竞争问题,从而导致数据结果不正确。可能会出现如下情况:
同步标识按预定的先后次序进行运行。如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。
使用Thread对象的Lock和RLock可以简单实现线程同步。这两个对象都有acquire方法和release方法,对于需要每次只允许一个线程操作的数据,可以将其操作放到acquire()和release()之间。
对于上述资源竞争导致的数据结果不正确的问题,可以通过线程同步解决,如下:
多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁为资源引入一个状态:锁定/非锁定。
某个线程要更改共享数据时,先将其锁定,此时资源的状态为【锁定】,其它线程不能更改;直到该线程释放资源,将资源的状态编程【非锁定】,其它线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
threading模块定义了Lock类,处理锁/解锁很方便。
# 创建锁
mutex = threading.Lok()
# 加锁
mutex.acquire()
#解锁
mutex.release()
需要注意的是:
使用示例:
import threading
import time
g_num = 0
def work1(num):
global g_num
for i in range(num):
mutex.acquire() # 上锁
g_num += 1
mutex.release() # 解锁
print("----in work1,g_num is %d---"%g_num)
def work2(num):
global g_num
for i in range(num):
mutex.acquire()
g_num += 1
mutex.release()
print("----in work2,g_num is %d---"%g_num)
print("---创建线程之前g_num is %d"%g_num)
# 创建互斥锁,默认是未上锁状态
mutex = threading.Lock()
t1 = threading.Thread(target=work1,args=(100000,))
t1.start()
t2 = threading.Thread(target=work2,args=(100000,))
t2.start()
while len(threading.enumerate()) != 1:
time.sleep(1)
print("---两个线程对同一个全局变量操作之后的最终结果是:%d"%g_num)
显示结果,发现加入互斥锁后,其结果与预期相符。
---创建线程之前g_num is 0
----in work1,g_num is 183100---
----in work2,g_num is 200000---
---两个线程对同一个全局变量操作之后的最终结果是:200000
上锁解锁过程:
使用锁可以确保某段关键代码只能由一个线程从头到尾完整地执行;但是使用锁终止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就下降了,并且由于可以存在多个所,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
死锁一旦发生,就会造成应用停止响应。
如下演示了死锁:
import threading
import time
class MyThread1(threading.Thread):
def run(self):
# 对mutex1上锁
mutex1.acquire()
# mutex1上锁后延时,等待另一个线程对mutex2上锁
print(self.name + "---do1---lock 1---")
time.sleep(1)
# 对mutex2上锁,此时会阻塞,因为mutex2已经被另外的线程上锁了
mutex2.acquire()
print(self.name + "---do1---lock 2---")
mutex2.release() # 解锁mutex2
# 解锁mutex1
mutex1.release()
class MyThread2(threading.Thread):
def run(self):
# 对mutex2上锁
mutex2.acquire()
# mutex2上锁后延时,等待另一个线程对mutex1上锁
print(self.name + "---do2---lock 2---")
time.sleep(1)
# 对mutex1上锁,此时会阻塞,因为mutex1已经被另外的线程上锁了
mutex1.acquire()
print(self.name + "---do2---lock 1---")
mutex1.release() # 解锁mutex1
# 解锁mutex2
mutex2.release()
mutex1 = threading.Lock()
mutex2 = threading.Lock()
if __name__ == "__main__":
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
显示结果:
避免死锁,程序设计时要尽量避免可能出现死锁的情况;添加超时时间等待。
全文参考:100天精通Python(进阶篇)——第37天:多线程(threading模块)_无 羡ღ的博客-CSDN博客https://blog.csdn.net/yuan2019035055/article/details/122656131