本文参考:
https://blog.csdn.net/youngwyj/article/details/124720041
https://blog.csdn.net/youngwyj/article/details/124833126
前言
在日常的开发中经常会用到多线程和多进程编程,使用多线程编程可降低程序的复杂度,使程序更简洁高效。
线程是程序执行流的最小单元,是进程的一个实体,一个进程可以拥有多个线程,多个线程可以共享进程所拥有的资源。
线程可以提升程序的整体性能,一般分为内核线程和用户线程,内核线程由操作系统内核创建和撤销,用户线程不需要内核支持,是在用户程序中实现的线程。
需要注意的是:线程不能独立运行,他必须依附于进程;线程可以被抢占(中断)和暂时搁置(休眠)。
threading模块
1.简介
在 python3 中有两种实现多线程编程的模块。
- 1._thread模块。python2中使用了thread模块实现,而 python3 不再使用该模块,为实现兼容在 python3 中将该模块改为了 _thread 模块。
- 2.threading模块。因为 _thread 模块提供的是一个简单低级的线程和锁,但 threading 模块是基于 Java 的线程模块设计,相较于 _thread 更高级,所以本文只讲解 threading 模块。
其实python并不适合做多线程的开发,因为 python 解释器使用了内部的全局解释器锁(GIL锁),使得在无论何时计算机只会允许在处理器上运行单个线程,即便多核GPU也是如此(即GIL锁同一时刻只允许一个进程只有一个线程被CPU调度),所以它的效率较其他语言低。
threading 模块创建线程的方式与 Java 类似,都是采用的类来创建线程。
threading模块的函数:
函数 | 说明 |
---|---|
threading.active_count() | 返回正运行的线程数量(包括主线程),与len(threading.enumerate())结果相同 |
threading.current_thread() | 返回当前线程的变量 |
threading.main_thread() | 返回主线程 |
threading.enumerate() | 返回一个正在运行的线程的列表 |
先来对单线程和多线程做一个对比,代码示例如下:
不使用多线程的操作:
import time
def worker():
print("worker")
time.sleep(1)
return
if __name__ == "__main__":
for i in range(5):
worker()
运行结果:
"""
worker
worker
worker
worker
worker
"""
使用多线程并发的操作,花费时间要短很多:
import threading
import time
def worker():
print("worker")
time.sleep(1)
return
if __name__ == "__main__":
for i in range(5):
t = threading.Thread(target=worker)
t.start()
运行结果:
"""
worker
worker
worker
worker
worker
"""
2.创建线程————start()方法
方法一:
在创建线程时必须先定义一个继承threading.Thread的类,然后重写子类中的run()方法,使用start()方法启动线程。
import threading
# 类必须继承threading.Thread
class threadTest(threading.Thread):
# args为传入线程的参数,可根据自己的需求进行定义
def __init__(self,args) -> None:
# 初始化super()内的必须和类名一样
super(threadTest,self).__init__()
self.args = args
# 定义run()方法,主要写线程的执行内容
def run(self) -> None:
print('test thread running...')
print('args: ',self.args)
return super().run()
if __name__ == "__main__":
# 实例化
test = threadTest('just test')
# 启动线程。即运行run()方法
test.start()
# 上述代码等于
import threading
import time
class threadTest(threading.Thread):
def __init__(self,args):
super(threadTest,self).__init__()
self.args = args
def run(self):
print('test thread running...')
print('args: ',self.args)
return super().run()
if __name__ == '__main__':
test = threadTest('worker')
test.start()
方法二:
使用函数的方式创建线程。
import threading
def threadTest(arg):
print("test thread running...")
print("args: ",arg)
if __name__ == "__main__":
# target传入函数名,注意不要写参数
# args target传入的函数需要传入的参数,注意传入参数以元组的形式
thread = threading.Thread(target=threadTest,args=('just test',))
# 启动线程
thread.start()
两种方式的运行结果都一样:
"""
test thread running...
args: just test
"""
需要注意的是:创建的线程为子线程,是主线程创建的子线程。
除 start() 方法外,threading.Thread类还提供了以下方法:
方法 | 说明 |
---|---|
join(timeout) | 表示主线程等待子线程timeout时长(s)后子线程若还未结束,就强制结束子线程,不设置则主线程会一直等待子线程结束后再结束 |
getName() | 获取线程名 |
setName() | 设置线程名 |
isAlive() | 返回线程是否正在运行 |
ident() | 获取线程标识符 |
setDaemon(bool) | 设置主,子线程运行时的关系。bool为True时主线程结束,子线程立即结束;为false主,子线程运行毫不相关,独立运行 |
3.join()方法
join()常用于控制线程的运行方法,只能在线程启动后使用,设置主线程是否等待子线程的结束。
未使用join()方法:
import threading
import time
# 类必须继承threading.Thread
class threadTest(threading.Thread):
def __init__(self) -> None:
# 初始化super()内的必须和类名一样
super(threadTest,self).__init__()
# 定义run()方法,主要写线程的执行内容
def run(self) -> None:
print('子线程' + self.getName() + '开始: ' + str(time.time()))
# 休眠1s
time.sleep(1)
print('子线程' + self.getName() + '结束: ' + str(time.time()))
return super().run()
if __name__ == "__main__":
# 创建线程列表
threads = []
for i in range(1,5):
test = threadTest()
# 设置线程名称
test.setName('Thread - %s' %(str(i)))
threads.append(test)
for thread in threads:
# 启动线程
thread.start()
print("主线程已结束: %s" %(str(time.time())))
运行结果:
"""
子线程Thread - 1开始: 1663308417.2721999
子线程Thread - 2开始: 1663308417.2724535
子线程Thread - 3开始: 1663308417.2726364
子线程Thread - 4开始: 1663308417.2727745
主线程已结束: 1663308417.272802
子线程Thread - 1结束: 1663308418.2736351
子线程Thread - 2结束: 1663308418.273736
子线程Thread - 3结束: 1663308418.2737775
子线程Thread - 4结束: 1663308418.2737951
"""
或者这里可以将打印的时间戳写为具体的时间,这样效果更加明显,几个print的地方改为:
import threading
import time
class threadTest(threading.Thread):
def __init__(self):
super(threadTest,self).__init__()
def run(self):
print('子线程' + self.getName() + '开始: ' + time.strftime('%T', time.localtime()))
time.sleep(1)
print('子线程' + self.getName() + '结束: ' + time.strftime('%T', time.localtime()))
return super().run()
if __name__ == "__main__":
threads = []
for i in range(1,5):
test = threadTest()
test.setName('Thread - %s' %(str(i)))
threads.append(test)
for thread in threads:
thread.start()
print('主线程已结束: %s' %(time.strftime('%T',time.localtime())))
运行结果:
子线程Thread - 1开始: 13:33:48
子线程Thread - 2开始: 13:33:48
子线程Thread - 3开始: 13:33:48
子线程Thread - 4开始: 13:33:48
主线程已结束: 13:33:48
子线程Thread - 1结束: 13:33:49
子线程Thread - 3结束: 13:33:49
子线程Thread - 2结束: 13:33:49
子线程Thread - 4结束: 13:33:49
可以看到未使用join()方法时,多个子线程几乎是同时创建,同时结束的,并且主线程并没有等子线程结束就已经结束了。
下面是使用了join()方法。
import threading
import time
# 类必须继承threading.Thread
class threadTest(threading.Thread):
def __init__(self) -> None:
# 初始化super()内的必须和类名一样
super(threadTest,self).__init__()
# 定义run()方法,主要写线程的执行内容
def run(self) -> None:
print('子线程' + self.getName() + '开始: ' + time.strftime('%T', time.localtime()))
# 休眠1s
time.sleep(1)
print('子线程' + self.getName() + '结束: ' + time.strftime('%T', time.localtime()))
return super().run()
if __name__ == "__main__":
# 创建线程列表
threads = []
for i in range(1,5):
test = threadTest()
# 设置线程名称
test.setName('Thread-%s' %(str(i)))
threads.append(test)
for thread in threads:
# 启动线程
thread.start()
thread.join()
print('主线程已结束: %s' %(time.strftime('%T', time.localtime())))
运行结果:
"""
子线程Thread-1开始: 13:45:14
子线程Thread-1结束: 13:45:15
子线程Thread-2开始: 13:45:15
子线程Thread-2结束: 13:45:16
子线程Thread-3开始: 13:45:16
子线程Thread-3结束: 13:45:17
子线程Thread-4开始: 13:45:17
子线程Thread-4结束: 13:45:18
主线程已结束: 13:45:18
"""
可以看到使用了join()后线程是依次运行的,且主线程是等待子线程结束后再结束的。
4.setDaemon(bool)方法
setDaemon(bool)方法用于设置主线程的守护线程,在启动线程启动之前使用。
当bool为True时,该线程为守护线程,主线程结束,子线程立即结束;为false主,子线程运行毫不相关,独立运行,子线程继续运行到结束。
先来看一个简单的例子:
# threading.setDaemon()的使用,设置后台进程。
import time
import threading
def worker():
time.sleep(3)
print("worker")
t = threading.Thread(target=worker)
t.setDaemon(True)
t.start()
print("haha")
运行结果:
"""
haha
可以看出worker()方法中的打印操作并没有显示出来,说明已经成为后台进程。
"""
示例如下:
import threading
import time
# 类必须继承threading.Thread
class threadTest(threading.Thread):
def __init__(self) -> None:
# 初始化super()内的必须和类名一样
super(threadTest,self).__init__()
# 定义run()方法,主要写线程的执行内容
def run(self) -> None:
print('子线程' + self.getName() + '开始: ' + time.strftime('%T', time.localtime()))
# 休眠3s
time.sleep(3)
print('子线程' + self.getName() + '结束: ' + time.strftime('%T', time.localtime()))
return super().run()
if __name__ == "__main__":
print('主线程已开始: %s' %(time.strftime('%T',time.localtime())))
thread = threadTest()
# 设置线程名称
thread.setName('Thread-1')
# 参数设置为False
thread.setDaemon(False)
thread.start()
time.sleep(1)
print('主线程已结束: %s' %(time.strftime('%T',time.localtime())))
运行结果:
"""
主线程已开始: 14:43:25
子线程Thread-1开始: 14:43:25
主线程已结束: 14:43:26
子线程Thread-1结束: 14:43:28
"""
可以看到bool为false主线程结束,子线程继续运行到结束。
import threading
import time
# 类必须继承threading.Thread
class threadTest(threading.Thread):
def __init__(self) -> None:
# 初始化super()内的必须和类名一样
super(threadTest,self).__init__()
# 定义run()方法,主要写线程的执行内容
def run(self) -> None:
print('子线程' + self.getName() + '开始: ' + time.strftime('%T', time.localtime()))
# 休眠3s
time.sleep(3)
print('子线程' + self.getName() + '结束: ' + time.strftime('%T', time.localtime()))
return super().run()
if __name__ == "__main__":
print('主线程已开始: %s' %(time.strftime('%T',time.localtime())))
thread = threadTest()
# 设置线程名称
thread.setName('Thread-1')
# 参数设置为True
thread.setDaemon(True)
# 启动线程
thread.start()
time.sleep(1)
print('主线程已结束: %s' %(time.strftime('%T',time.localtime())))
运行结果:
"""
主线程已开始: 14:44:03
子线程Thread-1开始: 14:44:03
主线程已结束: 14:44:04
"""
可以看到bool为True时,主线程一结束,子线程就立即结束。
5.activeCount()方法
threading.activeCount()的使用,此方法返回当前进程中线程的个数。返回的个数中包含主线程。
示例如下:
import threading
import time
def worker():
print("test")
time.sleep(1)
return
if __name__ == "__main__":
for i in range(5):
t = threading.Thread(target=worker)
t.start()
print("current has %d threads" %(threading.activeCount() - 1))
运行结果:
"""
test
test
test
test
test
current has 5 threads
"""
6.enumerate()方法
threading.enumerate()的使用,此方法返回当前运行中的Thread对象列表。
import time
import threading
def worker():
print("test")
time.sleep(2)
threads = []
if __name__ == "__main__":
for i in range(5):
t = threading.Thread(target=worker)
threads.append(t)
t.start()
for item in threading.enumerate():
print(item)
print
运行结果:
"""
test
test
test
test
test
<_MainThread(MainThread, started 139837462284096)>
<Thread(Thread-1, started 139837333047040)>
<Thread(Thread-2, started 139837324654336)>
<Thread(Thread-3, started 139837316261632)>
<Thread(Thread-4, started 139837307868928)>
<Thread(Thread-5, started 139837299476224)>
"""
线程同步是多线程中很重要的概念,当多个线程需要共享数据时,如果不使用线程同步,就会存在数据不同步的情况。
要做到线程同步有两种方法,线程锁和条件变量Condition。
线程锁
1.Lock锁
threading模块中Lock锁和_thread模块中的锁是一样的。
代码示例:
import threading
import time
num = 0
# 申请线程锁
lock = threading.Lock()
# 类必须继承threading.Thread
class threadTest(threading.Thread):
def __init__(self) -> None:
# 初始化super()内的必须与类名一样
super(threadTest,self).__init__()
def run(self) -> None:
# 声明全局变量num
global num
# 申请线程锁
# lock.acquire()
print('子线程' + self.getName() + '开始: ' + time.strftime('%T',time.localtime()))
while num < 5:
# 休眠2s
time.sleep(2)
print(self.getName(), 'num: ', num)
num += 1
print('子线程' + self.getName() + '结束: ' + time.strftime('%T',time.localtime()))
# 释放线程锁
# lock.release()
return super().run()
if __name__ == "__main__":
print('主线程开始: %s' %(time.strftime('%T',time.localtime())))
thread1 = threadTest()
# 设置线程名称
thread1.setName('Thread-1')
thread2 = threadTest()
# 设置线程名称
thread2.setName('Thread-2')
# 启动线程
thread1.start()
# 启动线程
thread2.start()
time.sleep(1)
thread1.join()
thread2.join()
print('主线程已结束: %s' %(time.strftime('%T',time.localtime())))
运行结果:
"""
主线程开始: 16:19:31
子线程Thread-1开始: 16:19:31
子线程Thread-2开始: 16:19:31
Thread-1 num: 0
Thread-2 num: 1
Thread-1 num: 2
Thread-2 num: 3
Thread-1 num: 4
Thread-2 num: 4
子线程Thread-1结束: 16:19:37
子线程Thread-2结束: 16:19:37
主线程已结束: 16:19:37
"""
可以看到在未使用锁线程时,线程1和线程2对num操作出现了混乱。
将上面代码 lock.acquire() lock.release()
这两行代码的注释去掉,将线程锁添加。
运行结果如下:可以看到不会像上面出现混乱的情况。
import threading
import time
num = 0
# 申请线程锁
lock = threading.Lock()
# 类必须继承threading.Thread
class threadTest(threading.Thread):
def __init__(self) -> None:
# 初始化super()内的必须与类名一样
super(threadTest,self).__init__()
# 定义run()方法,主要写线程的执行内容
def run(self) -> None:
# 声明全局变量num
global num
# 申请线程锁
lock.acquire()
print('子线程' + self.getName() + '开始: ' + time.strftime('%T',time.localtime()))
while num < 5:
time.sleep(2)
print(self.getName(), 'num: ', num)
num += 1
print('子线程' + self.getName() + '结束: ' + time.strftime('%T',time.localtime()))
# 释放线程锁
lock.release()
return super().run()
if __name__ == "__main__":
print('主线程开始: %s' %(time.strftime('%T',time.localtime())))
thread1 = threadTest()
# 设置线程名称
thread1.setName('Thread-1')
thread2 = threadTest()
# 设置线程名称
thread2.setName('Thread-2')
# 启动线程
thread1.start()
# 启动线程
thread2.start()
time.sleep(1)
thread1.join()
thread2.join()
print('主线程已结束: %s' %(time.strftime('%T',time.localtime())))
运行结果:
"""
主线程开始: 16:25:36
子线程Thread-1开始: 16:25:37
Thread-1 num: 0
Thread-1 num: 1
Thread-1 num: 2
Thread-1 num: 3
Thread-1 num: 4
子线程Thread-1结束: 16:25:47
子线程Thread-2开始: 16:25:47
子线程Thread-2结束: 16:25:47
主线程已结束: 16:25:47
"""
2.RLock锁
RLock锁又称递归锁,其与Lock锁的差别在于,Lock锁只允许在同一线程中申请一次,否则线程会进入死锁,但是RLock允许在同一线程多次调用。
使用Lock锁产生死锁示例代码:
import threading
import time
print('主线程开始: %s' %(str(time.strftime('%T', time.localtime()))))
lock = threading.Lock()
# 申请线程锁
lock.acquire()
print(threading.enumerate())
# 再次申请线程锁,产生了死锁
lock.acquire()
print(threading.enumerate())
lock.release()
lock.release()
print('主线程结束: %s' %(str(time.strftime('%T',time.localtime()))))
运行结果:
此处为我在终端中主动杀死了进程,而非程序自己结束。
"""
主线程开始: 16:29:33
[<_MainThread(MainThread, started 32092)>, <WriterThread(pydevd.Writer, started daemon 26816)>, <ReaderThread(pydevd.Reader, started daemon 3100)>, <_TimeoutThread(Thread-4, started daemon 14560)>, <PyDBCommandThread(pydevd.CommandThread, started daemon 4772)>, <CheckAliveThread(pydevd.CheckAliveThread, started 9552)>]
"""
使用RLock锁不会产生死锁示例代码:
import threading
import time
print('主线程开始: %s' %(str(time.strftime('%T', time.localtime()))))
lock = threading.RLock()
# 申请线程锁
lock.acquire()
print(threading.enumerate())
# 再次申请线程锁,不会产生死锁
lock.acquire()
print(threading.enumerate())
lock.release()
lock.release()
print('主线程结束: %s' %(str(time.strftime('%T',time.localtime()))))
运行结果:
"""
主线程开始: 16:37:45
[<_MainThread(MainThread, started 5632)>, <WriterThread(pydevd.Writer, started daemon 2252)>, <ReaderThread(pydevd.Reader, started daemon 22752)>, <_TimeoutThread(Thread-4, started daemon 30420)>, <PyDBCommandThread(pydevd.CommandThread, started daemon 27404)>, <CheckAliveThread(pydevd.CheckAliveThread, started 31420)>]
[<_MainThread(MainThread, started 5632)>, <WriterThread(pydevd.Writer, started daemon 2252)>, <ReaderThread(pydevd.Reader, started daemon 22752)>, <_TimeoutThread(Thread-4, started daemon 30420)>, <PyDBCommandThread(pydevd.CommandThread, started daemon 27404)>, <CheckAliveThread(pydevd.CheckAliveThread, started 31420)>]
主线程结束: 16:37:45
"""
从上面可以看到Lock与RLock的区别
注意线程锁需要成对出现
条件变量 Condition
Condition是python3中一种更高级的锁,除和线程锁类似的 acquire() 和 release() 函数外,还提供以下函数。
函数 | 说明 |
---|---|
wait() | 使线程挂起 |
notify() | 唤醒挂起的线程使其运行 |
notifyAll() | 唤醒所有线程使其运行 |
注意:线程使用前需要获得锁,否则会抛出RuntimeError异常
可以理解为,Condition提供了一种多线程通信机制,若线程1需要数据,线程1就会阻塞等待,线程2制造出数据,等待线程2制造好数据并通知线程1后,线程1就可以去获取数据了
下面是一个使用条件变量 Condition 模拟成语接龙示例代码:
import threading
import time
# 类必须继承threading.Thread
class Man1(threading.Thread):
def __init__(self,lock) -> None:
# 初始化super()内的必须和类名一样
super(Man1,self).__init__()
self.lock = lock
def run(self):
# 申请锁
self.lock.acquire()
print('子线程' + self.getName() + '为所欲为')
# 挂起线程,等待回答
self.lock.wait()
print('子线程' + self.getName() + '逼上梁山')
# 运行挂起的线程
self.lock.notify()
self.lock.wait()
print('子线程' + self.getName() + '尽力而为')
self.lock.notify()
self.lock.release()
# 类必须继承threading.Thread
class Man2(threading.Thread):
def __init__(self,lock) -> None:
# 初始化super()内的必须和类名一样
super(Man2,self).__init__()
self.lock = lock
def run(self):
# 申请锁
self.lock.acquire()
# 唤醒对方线程
self.lock.notify()
print('子线程' + self.getName() + '为法自弊')
# 挂起线程,等待回答
self.lock.wait()
self.lock.notify()
print('子线程' + self.getName() + '山穷水尽')
self.lock.wait()
self.lock.notify()
print('子线程' + self.getName() + '为所欲为')
self.lock.release()
if __name__ == '__main__':
lock = threading.Condition()
man1 = Man1(lock)
man2 = Man2(lock)
# 设置线程名称
man1.setName('Thread-1')
# 设置线程名称
man2.setName('Thread-2')
print('成语接龙开始: ')
man1.start()
man2.start()
运行结果:
"""
成语接龙开始:
子线程Thread-1为所欲为
子线程Thread-2为法自弊
子线程Thread-1逼上梁山
子线程Thread-2山穷水尽
子线程Thread-1尽力而为
子线程Thread-2为所欲为
"""
可以看到在条件变量的控制下,两个线程按照顺序执行,直到结束。