老样子,先上参考连接:
https://www.cnblogs.com/jiangfan95/p/11439543.html
https://www.liaoxuefeng.com/wiki/1016959663602400/1017629247922688
https://blog.csdn.net/mr__l1u/article/details/81772073
12月1日更新,先把廖大写的认识清楚!
参考链接:https://www.liaoxuefeng.com/wiki/1016959663602400/1017629247922688
多任务可以由多进程完成,也可以由一个进程内的多线程完成。
我们前面提到了进程是由若干线程组成的,一个进程至少有一个线程。
由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。
Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。
启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:
import time, threading
# 多线程执行的函数
def loop():
# 输出线程名字
print("thread %s is running..." % threading.current_thread().name)
n= 0
while n < 5:
n += 1
print("thread %s >>> %s" % (threading.current_thread().name, n))
time.sleep(1)
# 输出主线程的名称
print('thread %s running...' % threading.current_thread().name)
# 定义一个子线程对象
t = threading.Thread(target=loop, name = "LoopThread")
# 跑起来
t.start()
# 阻塞
t.join()
print("thread %s ended." % threading.current_thread().name)
输出
thread MainThread running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread MainThread ended.
由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1,Thread-2……
Lock
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
来看看多个线程同时操作一个变量怎么把内容给改乱了:
import time, threading
balance = 0
def change_it(n):
# 定义为全局
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
# 两个线程循环执行函数,操作balance全局变量
for i in range(200000):
change_it(n)
t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
我们定义了一个共享变量balance,初始值为0,并且启动两个线程,先存后取,理论上结果应该为0,但是,由于线程的调度是由操作系统决定的,当t1、t2交替执行时,只要循环次数足够多,balance的结果就不一定是0了。
原因是因为高级语言的一条语句在CPU执行时是若干条语句,即使一个简单的计算:
balance = balance + n
也分两步:
计算balance + n,存入临时变量中;
将临时变量的值赋给balance。
也就是可以看成:
x = balance + n
balance = x
由于x是局部变量,两个线程各自都有自己的x,当代码正常执行时:
初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2 # balance = 0
结果 balance = 0
但是t1和t2是交替运行的,如果操作系统以下面的顺序执行t1、t2:
初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance - 8 # x2 = 0 - 8 = -8
t2: balance = x2 # balance = -8
结果 balance = -8
如果我们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过threading.Lock()来实现:
# 全局变量锁
lock = threading.Lock()
def run_thread(n):
# 两个线程循环执行函数,操作balance全局变量
for i in range(2000000):
# 通过try与finally的方式还是不错的,必须解锁,避免死锁
lock.acquire()
try:
change_it(n)
finally:
lock.release()
当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。
获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally来确保锁一定会被释放。
锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
多核CPU
如果你不幸拥有一个多核CPU,你肯定在想,多核应该可以同时执行多个线程。
如果写一个死循环的话,会出现什么情况呢?
打开Mac OS X的Activity Monitor,或者Windows的Task Manager,都可以监控某个进程的CPU使用率。
我们可以监控到一个死循环线程会100%占用一个CPU。
如果有两个死循环线程,在多核CPU中,可以监控到会占用200%的CPU,也就是占用两个CPU核心。
要想把N核CPU的核心全部跑满,就必须启动N个死循环线程。
试试用Python写个死循环:
import threading, multiprocessing
def loop():
x = 0
while True:
x = x ^ 1
for i in range(multiprocessing.cpu_count()):
t = threading.Thread(target=loop)
t.start()
启动与CPU核心数量相同的N个线程,在4核CPU上可以监控到CPU占用率仅有102%,也就是仅使用了一核。
但是用C、C++或Java来改写相同的死循环,直接可以把全部核心跑满,4核就跑到400%,8核就跑到800%,为什么Python不行呢?
因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。
不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
小结
多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。
Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。
ThreadLocal
先全篇读了一遍,主要解决了多线程上下级函数间的数据传递问题。
在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。
但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦:
# 准备后续多线程使用的对象。
def process_student(name):
std = Student(name)
# std是局部变量,但是每个函数都要用它,因此必须传进去:
do_task_1(std)
do_task_2(std)
def do_task_1(std):
do_subtask_1(std)
do_subtask_2(std)
def do_task_2(std):
do_subtask_2(std)
do_subtask_2(std)
每个函数一层一层调用都这么传参数那还得了?用全局变量?也不行,因为每个线程处理不同的Student对象,不能共享。
如果用一个全局dict存放所有的Student对象,然后以thread自身作为key获得线程对应的Student对象如何?
global_dict = {}
# 后期被多线程使用的函数
def std_thread(name):
std = Student(name)
# 把std放到全局变量global_dict中:
global_dict[threading.current_thread()] = std
do_task_1()
do_task_2()
def do_task_1():
# 不传入std,而是根据当前线程查找:,查找全局变量dict
std = global_dict[threading.current_thread()]
...
def do_task_2():
# 任何函数都可以查找出当前线程的std变量:
std = global_dict[threading.current_thread()]
...
这种方式理论上是可行的,它最大的优点是消除了std对象在每层函数中的传递问题,但是,每个函数获取std的代码有点丑。
有没有更简单的方式?
ThreadLocal应运而生,不用查找dict,ThreadLocal帮你自动做这件事:
import threading
# 定义一个全局可使用的被线程赋予属性的对象
local_school = threading.local()
def process_student():
# 获取当前线程处理的学生
std = local_school.student
print("Hello, %s (in %s)" % (std, threading.current_thread().name))
# 被多线程调用的函数
def process_thread(name):
# 绑定参数,给调用的子函数可以调用该参数
local_school.student = name
process_student()
t1 = threading.Thread(target=process_thread, args=("Alice",), name="Thread-A")
t2 = threading.Thread(target=process_thread, args=("Bob",), name="Thread-B")
t1.start()
t2.start()
t1.join()
t2.join()
输出
Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)
全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。
可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。
ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
小结
一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。
进程 vs. 线程
我们介绍了多进程和多线程,这是实现多任务最常用的两种方式。现在,我们来讨论一下这两种方式的优缺点。
首先,要实现多任务,通常我们会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务,因此,多任务环境下,通常是一个Master,多个Worker。
如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。
如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。
多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。
多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。
多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。
在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache现在又有多进程+多线程的混合模式,真是把问题越搞越复杂。
线程切换
无论是多进程还是多线程,只要数量一多,效率肯定上不去,为什么呢?
我们打个比方,假设你不幸正在准备中考,每天晚上需要做语文、数学、英语、物理、化学这5科的作业,每项作业耗时1小时。
如果你先花1小时做语文作业,做完了,再花1小时做数学作业,这样,依次全部做完,一共花5小时,这种方式称为单任务模型,或者批处理任务模型。
假设你打算切换到多任务模型,可以先做1分钟语文,再切换到数学作业,做1分钟,再切换到英语,以此类推,只要切换速度足够快,这种方式就和单核CPU执行多任务是一样的了,以幼儿园小朋友的眼光来看,你就正在同时写5科作业。
但是,切换作业是有代价的,比如从语文切到数学,要先收拾桌子上的语文书本、钢笔(这叫保存现场),然后,打开数学课本、找出圆规直尺(这叫准备新环境),才能开始做数学作业。操作系统在切换进程或者线程时也是一样的,它需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。
所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。
计算密集型 vs. IO密集型
是否采用多任务的第二个考虑是任务的类型。我们可以把任务分为计算密集型和IO密集型。
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
异步IO
考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。
现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。
对应到Python语言,单线程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。我们会在后面讨论如何编写协程。
先复制一篇进程与线程的特点。
1> 进程、线程和协程的认识:
进程是系统进行资源分配和调度的独立单位;
线程是进程的实体,是CPU调度和分派的基本单位;
协程也是线程,称微线程,自带CPU上下文,是比线程更小的执行单元;
2> 区别
一个程序至少有一个进程,一个进程至少有一个线程;
线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高;
进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率;
线程不能够独立执行,必须依存在进程中;
3> 优缺点:线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反
4> 协程:我们假设把一个进程比作我们实际生活中的一个兰州拉面馆,那么负责保持拉面馆运行的服务员就是线程了,每个餐桌代表要完成的任务。
当我们用多线程完成任务时,模式是这样的==》每来一桌的客人,就在那张桌子上安排一个服务员,即有多少桌客人就得对应多少个服务员;
而当我们用协程来完成任务时,模式却有所不同了==》 就安排一个服务员,来吃饭得有一个点餐和等菜的过程,当A在点菜,就去B服务,B叫了菜在等待,我就去C,当C也在等菜并且A点菜点完了,赶紧到A来服务… …依次类推。
从上面的例子可以看出,想要使用协程,那么我们的任务必须有等待。当我们要完成的任务有耗时任务,属于IO密集型任务时,我们使用协程来执行任务会节省很多的资源(一个服务员和多个服务员的区别。##能一个人服务一个店铺,这样的超人给我来一打 (-..-)), 并且可以极大的利用到系统的资源。
上面的介绍,我觉的还是比较不错的,第三个链接可以点进去看原文。
上代码,两种不同的多线程开启模式:
import threading
import time
import logging
import random
class MyThreading(threading.Thread): # 通过继承的方式
def __init__(self, num):
super(MyThreading, self).__init__()
self.num = num
def run(self) -> None:
time.sleep(random.random())
logging.debug(str(self.__class__.__name__) + '======>' + 'Work: %s' % self.num)
def worker(num):
"""thread worker function"""
time.sleep(random.random())
# 通过threading.current_thread().getName()获取线程的名字
logging.debug(threading.current_thread().getName() + ':' + 'Worker: %s' % num)
#通过logging来打印显示具体信息
logging.basicConfig(level=logging.DEBUG, format="[%(levelname)s] (%(threadName)-10s) %(message)s")
threads = []
for i in range(5):
my = MyThreading(i)
my.start()
t = threading.Thread(target=worker, args=(i,), name='sidian') # 通过直接调用使用多线程
t.start()
threads.append(t)
threads.append(my)
for i in threads: # 加了阻塞让新建的多线程执行。
i.join()
logging.info('__main__:' + threading.current_thread().getName())
/usr/local/bin/python3.7 /Users/shijianzhong/study/t_threading/t1.py
[DEBUG] (Thread-5 ) MyThreading======>Work: 4
[DEBUG] (sidian ) sidian:Worker: 0
[DEBUG] (Thread-1 ) MyThreading======>Work: 0
[DEBUG] (Thread-4 ) MyThreading======>Work: 3
[DEBUG] (sidian ) sidian:Worker: 4
[DEBUG] (sidian ) sidian:Worker: 2
[DEBUG] (sidian ) sidian:Worker: 1
[DEBUG] (Thread-2 ) MyThreading======>Work: 1
[DEBUG] (sidian ) sidian:Worker: 3
[DEBUG] (Thread-3 ) MyThreading======>Work: 2
[INFO] (MainThread) __main__:MainThread
Process finished with exit code 0
从代码看,多线程的写法与多进程有很多相同之处。
后面我写个案例看多线程与多进程的一些区别。
import multiprocessing
import threading
import time
n = 0
def run():
global n
for i in range(10000):
n += i
return n
def s_time(func):
def wrap():
t1 = time.perf_counter()
res = func()
print(f'({func.__name__})cost_time:{time.perf_counter()-t1:0.5f}')
return res
return wrap
@s_time
def run_threads():
threads = []
for i in range(10000):
t = threading.Thread(target=run)
t.start()
threads.append(t)
for i in threads:
i.join()
@s_time
def run_process():
threads = []
for i in range(10000):
t = multiprocessing.Process(target=run)
t.start()
threads.append(t)
for i in threads:
i.join()
run_threads()
run_process()
/usr/local/bin/python3.7 /Users/shijianzhong/study/t_threading/t2.py
(run_threads)cost_time:8.54433
(run_process)cost_time:20.15610
Process finished with exit code 0
上面是同样开一万个进程与一万的线程的事件对比查,明显多线程快多了。
后面我是我分别开1000个与100个的效果。
/usr/local/bin/python3.7 /Users/shijianzhong/study/t_threading/t2.py
(run_threads)cost_time:0.86798
(run_process)cost_time:1.20561
Process finished with exit code 0
/usr/local/bin/python3.7 /Users/shijianzhong/study/t_threading/t2.py
(run_threads)cost_time:0.10313
(run_process)cost_time:0.12918
Process finished with exit code 0
总体来说,针对IO任务,多线程的开启速度明显比多进程快。
下面讲一下守护线程,跟守护进程一样,守护线程不阻塞主线程的运行,但随着主线程的运行结束而结束。默认情况下线程不作为守护线程。
import threading
import time
import logging
def daemon():
logging.debug('Starting')
time.sleep(0.2)
logging.debug('Exiting')
def non_daemon():
logging.debug('Starting')
logging.debug('Exiting')
logging.basicConfig(
level=logging.DEBUG,
format="(%(threadName)-10s) %(message)s",
)
d = threading.Thread(target=daemon, name='daemon', daemon=True) # 开启守护线程
# d.setDaemon(True) # 第二种方式
t = threading.Thread(target=non_daemon, name='non_daemon')
d.start()
t.start()
d.join(0.1) # 设置守护线程的阻塞时间,如果不设置时间,讲长期阻塞
logging.info('d.isAlive' + str(d.is_alive())) # 判断该线程是否还活着
/usr/local/bin/python3.7 /Users/shijianzhong/study/t_threading/t3.py
(daemon ) Starting
(non_daemon) Starting
(non_daemon) Exiting
(MainThread) d.isAliveTrue
Process finished with exit code 0
下面再介绍一个感觉不是很实用的枚举所有线程的方法。
import random
import threading
import time
import logging
def worker():
"""thread worker function"""
pause = random.randint(1, 5) / 10
logging.debug('sleeping %0.2f', pause)
time.sleep(pause)
logging.debug('ending')
logging.basicConfig(
level=logging.DEBUG,
format="[%(levelname)s] (%(threadName)-10s) %(message)s",
)
for i in range(3):
t = threading.Thread(target=worker)
t.setDaemon(True)
t.start()
main_thread = threading.main_thread()
logging.info(main_thread) # 主线程
logging.info(threading.enumerate())
for t in threading.enumerate(): # 枚举所有的线程
if t is main_thread: # 如果是主线程就跳过
continue
logging.debug(f'joining {t.getName()}') # 非主线程就阻塞
t.join()
/usr/local/bin/python3.7 /Users/shijianzhong/study/t_threading/t4.py
[DEBUG] (Thread-1 ) sleeping 0.20
[DEBUG] (Thread-2 ) sleeping 0.50
[DEBUG] (Thread-3 ) sleeping 0.30
[INFO] (MainThread) <_mainthread started>
[INFO] (MainThread) [<_mainthread started>, , , ]
[DEBUG] (MainThread) joining Thread-1
[DEBUG] (Thread-1 ) ending
[DEBUG] (MainThread) joining Thread-2
[DEBUG] (Thread-3 ) ending
[DEBUG] (Thread-2 ) ending
[DEBUG] (MainThread) joining Thread-3
Process finished with exit code 0
其实我感觉为什么这么麻烦,其实直接做个空列表,把需要阻塞的线程放进去再循环不是一样吗?
比如下面的:
import random
import threading
import time
import logging
def worker():
"""thread worker function"""
pause = random.randint(1, 5) / 10
logging.debug('sleeping %0.2f', pause)
time.sleep(pause)
logging.debug('ending')
logging.basicConfig(
level=logging.DEBUG,
format="[%(levelname)s] (%(threadName)-10s) %(message)s",
)
threads = []
for i in range(3):
t = threading.Thread(target=worker)
t.setDaemon(True)
t.start()
threads.append(t)
main_thread = threading.main_thread()
logging.info(main_thread) # 主线程
logging.info(threading.enumerate())
# for t in threading.enumerate(): # 枚举所有的线程
# if t is main_thread: # 如果是主线程就跳过
# continue
# logging.debug(f'joining {t.getName()}') # 非主线程就阻塞
# t.join()
for t in threads:
t.join()
这样不是更加简单吗?
下面上一个更加无聊的定时器线程,实在想不通,这种定时器线程一般用在什么地方。
import threading
import time
import logging
def delayed():
logging.debug('worker running')
logging.basicConfig(
level=logging.DEBUG,
format="[%(levelname)s] (%(threadName)-10s) %(message)s",
)
t1 = threading.Timer(0.3, delayed) # 0.3秒后启动线程
t1.setName('t1')
t2 = threading.Timer(0.3, delayed)
t2.setName('t2')
logging.debug('starting timers')
t1.start()
t2.start()
logging.debug(f'waiting before canceling {t2.getName()}')
time.sleep(0.2)
logging.debug(f'canceling {t2.getName()}')
t2.cancel() # 取消线程
logging.debug('done')
/usr/local/bin/python3.7 /Users/shijianzhong/study/t_threading/threading_timer.py
[DEBUG] (MainThread) starting timers
[DEBUG] (MainThread) waiting before canceling t2
[DEBUG] (MainThread) canceling t2
[DEBUG] (MainThread) done
[DEBUG] (t1 ) worker running
Process finished with exit code 0
从代码可以看出,Timer我只看到了延迟启动线程任务与中途可以取消任务的功能,但实在很难想象使用的场景。
下面介绍一个多线程里面的Event线程通讯管理,其实多线程共享数据,我觉设置全局变量作为通讯管理应该也是不错的选择。
import logging
import threading
import time
def wait_for_event(e):
"""wait for the event to be set before doing anything"""
logging.debug('wait for event starting') # 线程启动最开始启动
event_is_set = e.wait() # 这个还是比较有意思的,竟然还有返回值。
logging.debug(f'event set: {event_is_set}')
logging.debug(str(e.is_set()))
def wait_for_event_timeout(e, t):
"""Wait t seconds and then timeout"""
while not e.is_set(): # 默认事件为关闭
logging.debug('wait_for_event_timeout starting')
event_is_set = e.wait(t) # 线程开始进来等待阻塞,可以设置阻塞时间,返回值为时间是否为设置的值
logging.debug('event set : %s', event_is_set)
if event_is_set:
logging.debug('processing event')
else:
logging.debug('doing other work')
logging.basicConfig(
level=logging.DEBUG,
format="[%(levelname)s] (%(threadName)-10s) %(message)s",
)
e = threading.Event()
t1 = threading.Thread(
name='block',
target=wait_for_event,
args=(e,),
)
t1.start()
t2 = threading.Thread(
name='nonblock',
target=wait_for_event_timeout,
args=(e, 2),
)
t2.start()
logging.debug('Waiting before calling Event.set()')
time.sleep(3)
e.set()
logging.debug('Event is set')
/usr/local/bin/python3.7 /Users/shijianzhong/study/t_threading/thread_event.py
[DEBUG] (block ) wait for event starting
[DEBUG] (nonblock ) wait_for_event_timeout starting
[DEBUG] (MainThread) Waiting before calling Event.set()
[DEBUG] (nonblock ) event set : False
[DEBUG] (nonblock ) doing other work
[DEBUG] (nonblock ) wait_for_event_timeout starting
[DEBUG] (MainThread) Event is set
[DEBUG] (block ) event set: True
[DEBUG] (block ) True
[DEBUG] (nonblock ) event set : True
[DEBUG] (nonblock ) processing event
Process finished with exit code 0
从代码可以看出e.wait()还是非常好用的一个方法。
接下来讲一下,多线程操作中的锁。这一点很重要,上面讲的是同步线程操作,这里是要能控制对共享资源的访问,从而避免破坏或丢失数据。
Python的内置数据结构(列表、字典等)是线程安全的,这是Python使用原子字节码来管理这些数据结构的一个副作用(全局解释器锁的一个好处)
Python中实现的其他数据结构或更简单的类型(如整数和浮点数)则没有这个保护。要保证同时安全地访问一个对象,可以使用Lock对象。
锁尽量用with,这样避免锁忘记关闭,防止死锁。
import logging
import threading, time
logging.basicConfig(level=logging.INFO)
# 10 -> 100cups
cups = []
lock = threading.Lock()
def worker(lock = threading.Lock, task=100):
while True:
count = len(cups) # 当多个最后count为99的时候,如果有多个线程进入的话,cups数据个数将出现问题。
if count >= task:
break
logging.info(count)
cups.append(1)
logging.info("{} make 1........ ".format(threading.current_thread().getName()))
logging.info("{} ending=======>".format(len(cups)))
for x in range(10):
threading.Thread(target=worker, args=(lock, 100)).start()
INFO:root:104 ending=======>
INFO:root:104 ending=======>
INFO:root:Thread-6 make 1........
INFO:root:105 ending=======>
INFO:root:105 ending=======>
INFO:root:105 ending=======>
import logging
import threading, time
logging.basicConfig(level=logging.INFO)
# 10 -> 100cups
cups = []
lock = threading.Lock()
def worker(lock = None, task=100):
with lock: # 加锁以后
while True:
count = len(cups) # 当多个最后count为99的时候,如果有多个线程进入的话,cups数据个数将出现问题。
if count >= task:
break
logging.info(count)
cups.append(1)
logging.info("{} make 1........ ".format(threading.current_thread().getName()))
logging.info("{} ending=======>".format(len(cups)))
for x in range(10):
threading.Thread(target=worker, args=(lock, 100)).start()
INFO:root:Thread-1 make 1........
INFO:root:96
INFO:root:Thread-1 make 1........
INFO:root:97
INFO:root:Thread-1 make 1........
INFO:root:98
INFO:root:Thread-1 make 1........
INFO:root:99
INFO:root:Thread-1 make 1........
INFO:root:100 ending=======>
INFO:root:100 ending=======>
INFO:root:100 ending=======>
INFO:root:100 ending=======>
INFO:root:100 ending=======>
INFO:root:100 ending=======>
INFO:root:100 ending=======>
INFO:root:100 ending=======>
INFO:root:100 ending=======>
INFO:root:100 ending=======>
加锁以后数据就正确了,但加锁真的是一件非常让人蛋疼的事情,这样的话,基本多线程的效果等于0,因为加锁了以后会让并行的线程变成串行,效率会低很多。
Lock的方法:
acquire(blocking=True,timeout=-1) 加锁。默认True阻塞,阻塞可以设置超时时间。非阻塞时成功获取锁返回True,否则返回False。
这个还是非常有意思的,当你再acquire里面填参数0的时候,这个锁大家都可以拿,但假如返回是Flase说明你拿的是假锁。
l = threading.Lock()
r1 = l.acquire(0)
r2 = l.acquire(0)
l.release()
r3 = l.acquire(0)
r4 = l.acquire(0)
logging.info(f'r1>{r1};r2>{r2};r3>{r3};r4>{r4}')
INFO:root:r1>True;r2>False;r3>True;r4>False
这个还是蛮有意思的,可以通过返回值来判断,谁拿到的锁是真的锁,而且大家可以重复去拿锁,当那把锁解锁以后,第一个拿到的锁的,就是True
插播一条廖大的threading.local代码:
import threading
# 创建全局ThreadLocal对象(在实际使用中,其实你创建了几个线程后期就有几个实际的对象):
local_school = threading.local()
def process_student():
# 获取当前线程关联的student:
std = local_school.student
print('Hello, %s (in %s)' % (std, threading.current_thread().getName()))
def process_thread(name):
# 绑定ThreadLocal的student:
local_school.student = name
process_student()
t1 = threading.Thread(target=process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
这个threading.loacl,其实在任何一条线程里面都有一个对立的对象,说实话,要是我用,还不如函数里面层层传递比较,又直接,又清爽。
而且我感觉这个对象创建还会比较消耗内存。
后面用的比较少模块我觉的,我讲抄袭Python3标准库的代码,做好相关解释。
还有由于多线程是共享内存的,如果为了防止用锁带来的效率底下,可以使用queue模块。
queue队列 :使用import queue,用法与进程Queue一样
class queue.Queue(maxsize=0) #先进先出
importqueue
q=queue.Queue()
q.put('first')
q.put('second')
q.put('third')
print(q.get())
print(q.get())
print(q.get())
'''结果(先进先出):
first
second
third
'''
class queue.LifoQueue(maxsize=0) #last in fisrt out
importqueue
q=queue.LifoQueue()
q.put('first')
q.put('second')
q.put('third')
print(q.get())
print(q.get())
print(q.get())
'''结果(后进先出):
third
second
first
'''
class queue.PriorityQueue(maxsize=0) #存储数据时可设置优先级的队列
importqueue
q=queue.PriorityQueue()
#put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
q.put((20,'a'))
q.put((10,'b'))
q.put((30,'c'))
print(q.get())
print(q.get())
print(q.get())
'''结果(数字越小优先级越高,优先级高的优先出队):
(10, 'b')
(20, 'a')
(30, 'c')
'''
ok
同样你可以用queue写一个生产者与消费者的模型。以下我先插入写一个生产者,消费者模型吧。
import threading
import queue
import logging
import time
logging.basicConfig(
level=logging.DEBUG,
format='[%(levelname)s] (%(threadName)s) => %(message)s'
)
def consumer(q):
while True:
res = q.get()
if res is None:
logging.debug(f'你没东西给我吃了,我走了')
break
logging.debug(f'我开始吃{res}了')
def product(q):
for i in ('猪头','milk', 'egg'):
logging.debug(f'我放入了{i}')
q.put(i)
time.sleep(2)
q.put(None) # 给一个结束信号
if __name__ == '__main__':
q = queue.Queue()
c = threading.Thread(target=consumer, args=(q,))
p = threading.Thread(target=product, args=(q,))
c.start()
p.start()
/usr/local/bin/python3.7 /Users/shijianzhong/study/t_threading/threading_queue.py
[DEBUG] (Thread-2) => 我放入了猪头
[DEBUG] (Thread-1) => 我开始吃猪头了
[DEBUG] (Thread-2) => 我放入了milk
[DEBUG] (Thread-1) => 我开始吃milk了
[DEBUG] (Thread-2) => 我放入了egg
[DEBUG] (Thread-1) => 我开始吃egg了
[DEBUG] (Thread-1) => 你没东西给我吃了,我走了
Process finished with exit code 0
接下来,我开始抄写,注释代码了。
首先是同步线程除了使用Event还能使用Condition对象来同步线程。
import logging
import threading
import time
def consumer(cond):
"""wait for the condition and use the resource"""
logging.debug('Starting consumer thread')
with cond: # 必须再condition的环境下才等待
cond.wait() # 开始等待
logging.debug('Resource is available to consumer')
def producer(cond):
"""set up the resource to be used by the consumer"""
logging.debug('Starting producer thread')
with cond: # 必须在condition环境下菜能设置condition
logging.debug('Making resource available')
cond.notifyAll() # 给条件通知
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s (%(threadName)-2s) %(message)s',
)
condition = threading.Condition()
# print(dir(condition))
c1 = threading.Thread(name='c1', target=consumer,
args=(condition,))
c2 = threading.Thread(name='c2', target=consumer,
args=(condition,))
p = threading.Thread(name='p', target=producer,
args=(condition,))
c1.start()
time.sleep(0.2)
c2.start()
time.sleep(0.2)
p.start()
/usr/local/bin/python3.7 /Users/shijianzhong/study/t_threading/threding_condition.py
2019-12-12 02:16:24,131 (c1) Starting consumer thread
2019-12-12 02:16:24,334 (c2) Starting consumer thread
2019-12-12 02:16:24,535 (p ) Starting producer thread
2019-12-12 02:16:24,536 (p ) Making resource available
2019-12-12 02:16:24,536 (c1) Resource is available to consumer
2019-12-12 02:16:24,536 (c2) Resource is available to consumer
Process finished with exit code 0
屏障(barrier)是另外一种线程同步机制。Barrier会建立一个控制点,所有参与的线程会在这里阻塞,直到所有这些参与线程到达这个点。
当线程的数量等于你的设置量时候,线程开始工作。感觉非常有意思的玩意。
import threading
import time
def work(barrier):
print(threading.current_thread().getName(),
'waiting for barrier with {} others'.format(barrier.n_waiting))
worker_id = barrier.wait() # 设置线程数量阻塞,返回值会阻塞数量,跟列表有点像从从0开始,到2返回值,就说明三个线程阻塞完成。
# print('worker_id_num====>',worker_id)
print(threading.current_thread().getName(), 'after barrier', worker_id)
NUM_THREADS = 3
barrier = threading.Barrier(NUM_THREADS) # 外部定义barrier传入运作函数里面。
threads = [
threading.Thread(
name=f'worker-{i}',
target=work,
args=(barrier,),
)
for i in range(6) # 定一6个线程,如果定义的线程数量如阻塞的数量为非整除,整个线程将阻塞。
]
for t in threads:
print(t.getName(),'starting')
t.start()
time.sleep(0.1)
for t in threads:
t.join()
/usr/local/bin/python3.7 /Users/shijianzhong/study/t_threading/threading_barrier.py
worker-0 starting
worker-0 waiting for barrier with 0 others
worker-1 starting
worker-1 waiting for barrier with 1 others
worker-2 starting
worker-2 waiting for barrier with 2 others
worker-2 after barrier 2
worker-0 after barrier 0
worker-1 after barrier 1
worker-3 starting
worker-3 waiting for barrier with 0 others
worker-4 starting
worker-4 waiting for barrier with 1 others
worker-5 starting
worker-5 waiting for barrier with 2 others
worker-5 after barrier 2
worker-4 after barrier 1
worker-3 after barrier 0
Process finished with exit code 0
后面将介绍一个在使用abort()来避免由于线程数量问题导致的阻塞。
import threading
import time
def work(barrier):
print(threading.current_thread().getName(),
'waiting for barrier with {} others'.format(barrier.n_waiting))
try:
worker_id = barrier.wait() # 设置线程数量阻塞,返回值会阻塞数量,跟列表有点像从从0开始,到2返回值,就说明三个线程阻塞完成。
# print('worker_id_num====>',worker_id)
except threading.BrokenBarrierError:
print(threading.current_thread().getName(), 'aborting')
else:
print(threading.current_thread().getName(), 'after barrier', worker_id)
NUM_THREADS = 3
barrier = threading.Barrier(NUM_THREADS) # 外部定义barrier传入运作函数里面。
threads = [
threading.Thread(
name=f'worker-{i}',
target=work,
args=(barrier,),
)
for i in range(4) # 定一6个线程,如果定义的线程数量如阻塞的数量为非整除,整个线程将阻塞。
]
for t in threads:
print(t.getName(),'starting')
t.start()
time.sleep(0.1)
barrier.abort() # 定义abort取消符后,就非常方便,当最后的线程无法满足条件,将自动报错,可以通过except接收。
for t in threads:
t.join()
/usr/local/bin/python3.7 /Users/shijianzhong/study/t_threading/threading_barrier_abort.py
worker-0 starting
worker-0 waiting for barrier with 0 others
worker-1 starting
worker-1 waiting for barrier with 1 others
worker-2 starting
worker-2 waiting for barrier with 2 others
worker-2 after barrier 2
worker-0 after barrier 0
worker-1 after barrier 1
worker-3 starting
worker-3 waiting for barrier with 0 others
worker-3 aborting
Process finished with exit code 0
还有最后两个,再说一个限制资源的并发访问。有时候可能需要允许多个工作线程同时访问一个资源,但要限制总数。列如,连接池支持同时链接,但数目可能固定,或者一个网络应用可能支持固定数目的并发下载。这些可以用Semaphore来管理。
import logging
import random
import threading
import time
class ActivePool:
def __init__(self):
self.active = []
self.lock = threading.Lock()
def makeActive(self,name): # 不加锁的情况下
# with self.lock:
self.active.append(name)
time.sleep(1)
logging.debug(f'A{threading.current_thread().getName()}-Running: {self.active}')
def makeInactive(self,name):
# with self.lock:
self.active.remove(name)
# time.sleep(0.1)
logging.debug(f'I{threading.current_thread().getName()}-Running: {self.active}')
def worker(s, pool):
logging.debug('Waiting to join the pool')
with s: # 可以使用acquire与release
# time.sleep(0.1)
name = threading.current_thread().getName()
print(f'{name} is coming.')
pool.makeActive(name)
# time.sleep(0.2)
pool.makeInactive(name)
# time.sleep(0.1)
print(f'{name} is out.')
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s (%(threadName)-2s) %(message)s',
)
pool = ActivePool()
s = threading.Semaphore(2) # 设置最大连接数量
def spend_time(func):
def warp():
t1 = time.perf_counter()
func()
t2 = time.perf_counter()
print(f'speng_time is :{t2 -t1:0.5f}')
return warp
@spend_time
def run():
threads = []
for i in range(4):
t = threading.Thread(
target=worker,
name=str(i),
args=(s, pool),
)
t.start()
threads.append(t)
for t in threads:
t.join()
run()
/usr/local/bin/python3.7 /Users/shijianzhong/study/t_threading/therading_semaphonre.py
0 is coming.
1 is coming.
2019-12-13 03:22:01,429 (0 ) Waiting to join the pool
2019-12-13 03:22:01,429 (1 ) Waiting to join the pool
2019-12-13 03:22:01,430 (2 ) Waiting to join the pool
2019-12-13 03:22:01,430 (3 ) Waiting to join the pool
0 is out.
2 is coming.1 is out.
3 is coming.
2019-12-13 03:22:02,429 (0 ) A0-Running: ['0', '1']
2019-12-13 03:22:02,429 (0 ) I0-Running: ['1']
2019-12-13 03:22:02,430 (1 ) A1-Running: ['1']
2019-12-13 03:22:02,430 (1 ) I1-Running: []
2019-12-13 03:22:03,430 (3 ) A3-Running: ['2', '3']
2019-12-13 03:22:03,431 (3 ) I3-Running: ['2']
2019-12-13 03:22:03,431 (2 ) A2-Running: ['2']
2019-12-13 03:22:03,431 (2 ) I2-Running: []
3 is out.
2 is out.
speng_time is :2.00197
Process finished with exit code 0
加锁的情况下:
import logging
import random
import threading
import time
class ActivePool:
def __init__(self):
self.active = []
self.lock = threading.Lock()
def makeActive(self,name): # 不加锁的情况下
with self.lock:
self.active.append(name)
time.sleep(1)
logging.debug(f'A{threading.current_thread().getName()}-Running: {self.active}')
def makeInactive(self,name):
with self.lock:
self.active.remove(name)
# time.sleep(0.1)
logging.debug(f'I{threading.current_thread().getName()}-Running: {self.active}')
def worker(s, pool):
logging.debug('Waiting to join the pool')
with s: # 可以使用acquire与release
# time.sleep(0.1)
name = threading.current_thread().getName()
print(f'{name} is coming.')
pool.makeActive(name)
# time.sleep(0.2)
pool.makeInactive(name)
# time.sleep(0.1)
print(f'{name} is out.')
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s (%(threadName)-2s) %(message)s',
)
pool = ActivePool()
s = threading.Semaphore(2) # 设置最大连接数量
def spend_time(func):
def warp():
t1 = time.perf_counter()
func()
t2 = time.perf_counter()
print(f'speng_time is :{t2 -t1:0.5f}')
return warp
@spend_time
def run():
threads = []
for i in range(4):
t = threading.Thread(
target=worker,
name=str(i),
args=(s, pool),
)
t.start()
threads.append(t)
for t in threads:
t.join()
run()
/usr/local/bin/python3.7 /Users/shijianzhong/study/t_threading/therading_semaphonre.py
2019-12-13 03:24:15,506 (0 ) Waiting to join the pool
2019-12-13 03:24:15,506 (1 ) Waiting to join the pool
2019-12-13 03:24:15,506 (2 ) Waiting to join the pool
2019-12-13 03:24:15,507 (3 ) Waiting to join the pool
0 is coming.1 is coming.
2019-12-13 03:24:16,507 (1 ) A1-Running: ['1']
2019-12-13 03:24:16,507 (1 ) I1-Running: []
1 is out.
2 is coming.
2019-12-13 03:24:17,509 (0 ) A0-Running: ['0']
2019-12-13 03:24:17,510 (0 ) I0-Running: []
0 is out.
3 is coming.
2019-12-13 03:24:18,512 (2 ) A2-Running: ['2']
2019-12-13 03:24:18,512 (2 ) I2-Running: []
2 is out.
2019-12-13 03:24:19,514 (3 ) A3-Running: ['3']
2019-12-13 03:24:19,515 (3 ) I3-Running: []
3 is out.
speng_time is :4.00896
Process finished with exit code 0
其实根据我实际的操作来看,是否需要加锁应该也看实际需要,像Python保护的类型,操作列表,字典就无需加锁,要不然效率非常低。
在测试多线程运行的过程中,当跑的线程中,外部传入的函数的参数,是属于每个函数内部的一部分,并不会相互干扰,属于局部变量。
最后对于前面的local类的使用,参照Python3标准库的介绍,再详细记录一下。(主要用在函数内部调用函数,每个人操作一份具体的属性值,可以参考操作个人的存款)
有些资源需要锁定以便于多个线程使用,另外一些资源则需要保护,让他们能够对非这些线程的"所有者"隐藏。
local()类会创建一个对象,,它能隐藏值,使其再不同的线程中无法被看到。(一半这种用在多层函数使用中,一层需要使用的函数就可以设置好对象属性,后面所有的函数对可以直接调用这个对象属性)
对比廖大的解释,感觉还是这个专业书籍翻译过来的解释更加让人理解。
import random
import threading
import logging
def show_value(data):
try:
val = data.value
except AttributeError:
logging.debug('No value yet')
else:
logging.debug(f'value={val}')
def worker(data):
show_value(data)
data.value = random.randint(1, 100) # 对对象的属性进行赋值
show_value(data)
logging.basicConfig(
level=logging.DEBUG,
format='(%(threadName)-10s) %(message)s',
)
local_data = threading.local()
show_value(local_data) # 主线程开始先跑起来
local_data.value = 1000
show_value(local_data)
for i in range(2):
t = threading.Thread(target=worker, args=(local_data,)) # 开启两个线程,并传入local对象。
t.start()
/usr/local/bin/python3.7 /Users/shijianzhong/study/t_threading/thread_local_book.py
(MainThread) No value yet
(MainThread) value=1000
(Thread-1 ) No value yet
(Thread-1 ) value=90
(Thread-2 ) No value yet
(Thread-2 ) value=62
Process finished with exit code 0
上面的代码测试了,不同的线程执行可以拥有各自线程的对象属性。但如果希望这个对象一出来就自带一个默认的线程属性,
可以继承local,并再__init__初始化过程中给予对象设置属性。(银行用户操作,其实用起来蛮好了,可以混合继承该类,用于多线程操作,操作起来会更加方便)
import random
import threading
import logging
def show_value(data):
try:
val = data.value
except AttributeError:
logging.debug('No value yet')
else:
logging.debug(f'value={val}')
def worker(data):
show_value(data)
data.value = random.randint(1, 100) # 对对象的属性进行赋值
show_value(data)
class MyLocal(threading.local): # 通过继承的方式
def __init__(self,value):
super(MyLocal, self).__init__()
logging.debug('Initializing %r', self)
self.value = value # 给予对象初始值
logging.basicConfig(
level=logging.DEBUG,
format='(%(threadName)-10s) %(message)s',
)
local_data = MyLocal(1000)
show_value(local_data)
for i in range(2):
t = threading.Thread(target=worker, args=(local_data,)) # 开启两个线程,并传入local对象。
t.start()
/usr/local/bin/python3.7 /Users/shijianzhong/study/t_threading/thread_loacl_defaults.py
(MainThread) Initializing <__main__.mylocal object at>
(MainThread) value=1000
(Thread-1 ) Initializing <__main__.mylocal object at>
(Thread-1 ) value=1000
(Thread-1 ) value=53
(Thread-2 ) Initializing <__main__.mylocal object at>
(Thread-2 ) value=1000
(Thread-2 ) value=99
Process finished with exit code 0
最后给自己提个醒,无论是Event事件,还是condition,又或者是semaphore,local等等,杜需要将该对象放入函数中,再外面对该对象设置好相关参数,再函数中执行该对象具体方法。
初步的就记录到这里,总体感觉多线程与多进程很多操作都是非常相似的,平时一半并发操作,应该还是多线程比较好,至少内存使用少,启动快。
而且对比方法里面线程也比较多,也不存在数据通信问题。