志达IT
做快乐程序员

python多线程(Python多线程的基本概念)

python多线程

1.线程创立与办理
省流:python多线程效率堪忧,想了解这方面的去看第2末节GIL,想持续看看怎样运用的持续接着看。
1.1创立线程
Python供给了thread、threading等模块来进行线程的创立与办理,后者在线程办理能力上更进一步,因而咱们一般运用threading模块。创立一个线程需求指定该线程履行的使命(函数名)、以及该函数需求的参数,示例代码如下所示:
fromthreadingimportThread,current_thread
deftarget01(args1,args2):
print(“这儿是{}”.format(current_thread().name))
#创立线程
thread01=Thread(target=target01,args=”参数”,name=”线程1″)
#设置看护线程【可选】
thread01.setDaemon(True)
#发动线程
thread01.start()
1.2设置看护线程
线程是程序履行的最小单位,Python在进程发动起来后,会自动创立一个主线程,之后运用多线程机制能够在此基础上进行分支,产生新的子线程。子线程发动起来后,主线程默许会等候一切线程履行完结之后再退出。可是咱们能够将子线程设置为看护线程,此时主线程使命一旦完结,一切子线程将会和主线程一同结束(就算子线程没有履行完也会退出)。
看护线程能够在线程发动之前,经过setDaemon(True)的方法进行设置,或许在创立子线程目标时,以参数的方法指定:
thread01=Thread(target=target01,args=””,name=”线程1″,daemon=True)
可是需求留意,假如期望主程序不等候任何线程直接退出,只要一切的线程都被设置为看护线程才有用。java
1.3设置线程堵塞
咱们能够用join()办法使主线程堕入堵塞,以等候某个线程履行结束。因而这也是完结线程同步的一种办法。参数timeouttimeouttimeout能够用来设置主线程堕入堵塞的时间,假如线程不是看护线程,即没有设置daemon为True,那么参数timeouttimeouttimeout是无效的,主线程会一向堵塞,直到子线程履行结束。
测试代码如下:
importtime
fromthreadingimportThread,current_thread
deftarget():
ifcurrent_thread().name==”1″:
time.sleep(5)
else:
time.sleep(6)
print(“线程{}已退出”.format(current_thread().name))
thread01=Thread(target=target,daemon=True,name=”1″)
thread02=Thread(target=target,daemon=True,name=”2″)
thread01.start()
thread02.start()
print(“程序因线程1堕入堵塞”)
thread01.join(timeout=3)
print(“程序因线程2堕入堵塞”)
thread02.join(timeout=3)
print(“主线程已退出”)
1.4线程间通讯的办法
咱们知道,线程之间共享同一块内存。子线程尽管能够经过指定target来履行一个函数,可是这个函数的返回值是没有办法直接传回主线程的。咱们运用多线程一般是用于并行履行一些其他使命,因而获取子线程的履行成果十分有必要。
直接运用大局变量尽管可行,可是资源的并发读写会引来线程安全问题。下面给出常用的两种处理办法:
1.4.1线程锁
其一是能够考虑运用锁来处理,当多个线程对同一份资源进行读写操作时,咱们能够经过加锁来保证数据安全。Python中给出了多种锁的完结,例如:同步锁Lock,递归锁RLock,条件锁Condition,事件锁Event,信号量锁Semaphore,这儿只给出Lock的运用办法,其余的咱们感兴趣能够自己查阅。
能够经过threading.lock类来创立锁目标,一旦一个线程获得一个锁,会堵塞之后一切测验获得该锁目标的线程,直到它被重新开释。这儿举一个比如,经过加锁来保证两个线程在对同一个大局变量进行读写时的数据安全:
fromthreadingimportThread,Lock
fromtimeimportsleep
book_num=100#图书馆最开始有100本图书
bookLock=Lock()
defbooks_return():
globalbook_num
whileTrue:
bookLock.acquire()
book_num+=1
print(“偿还1本,现有图书{}本”.format(book_num))
bookLock.release()
sleep(1)#模仿事件产生周期
defbooks_lease():
globalbook_num
whileTrue:
bookLock.acquire()
book_num-=1
print(“借走1本,现有图书{}本”.format(book_num))
bookLock.release()
sleep(2)#模仿事件产生周期
if__name__==”__main__”:
thread_lease=Thread(target=books_lease)
thread_return=Thread(target=books_return)
thread_lease.start()
thread_return.start()
从成果中能够看出,其中没有出现由于读写冲突导致的数据过错。
1.4.2queue模块(同步行列类)
或许,咱们能够选用Python的queue模块来完结线程通讯。Python中的queuequeuequeue模块完结了多生产者、多顾客行列,特别适用于在多线程间安全的进行信息交换。该模块供给了4种咱们能够运用的行列容器,别离QueueQueueQueue(先进先出行列)、LifoQueueLifoQueueLifoQueue(先进后出行列)、PriortyQueuePriortyQueuePriortyQueue(优先级行列)、SimpleQueueSimpleQueueSimpleQueue(无界的先进先出行列,简略完结,缺少Queue中的使命盯梢等高级功能)。下面咱们以QueueQueueQueue为例介绍其运用办法,其他容器请自行查阅。
Queue(maxsize=5)#创立一个FIFO行列,并拟定行列大小,若maxsize被指定为小于等于0,则行列无限大
Queue.qsize()#返回行列的大致大小,留意并不是确切值,所以不能被用来当做后续线程是否会被堵塞的根据
Queue.empty()#判别行列为空是否建立,相同不能作为堵塞根据
Queue.full()#判别行列为满是否建立,相同不能作为堵塞根据
Queue.put(item,block=True,timeout=None)#投进元素进入行列,block为True表示假如行列满了投进失利,将堵塞该线程,timeout可用来设置线程堵塞的时间长短(秒);
#留意,假如block为False,假如行列为满,则将直接引发Full反常,timeout将被忽略(在外界用try处理反常即可)
Queue.put_nowait(item)#相当于put(item,block=False)
Queue.get(block=True,timeout=False)#从行列中取出元素,block为False而行列为空时,会引发Empty反常
Queue.get_nowait()#相当于get(block=False)
Queue.task_done()#每个线程运用get办法从行列中获取一个元素,该线程经过调用task_done()表示该元素已处理完结。
Queue.join()#堵塞至行列中一切元素都被处理完结,即行列中一切元素都已被接纳,且接纳线程全已调用task_done()。
下面给出一个比如,场景是3个厨师给4个客人上菜,这是对多生产者多顾客场景的模仿:java
importqueue
fromrandomimportchoice
fromthreadingimportThread
q=queue.Queue(maxsize=5)
dealList=[“红烧猪蹄”,”卤鸡爪”,”酸菜鱼”,”糖醋里脊”,”九转大肠”,”阳春面”,”烤鸭”,”烧鸡”,”剁椒鱼头”,”酸汤肥牛”,”炖羊肉”]
defcooking(chefname:str):
foriinrange(4):
deal=choice(dealList)
q.put(deal,block=True)
print(“厨师{}给咱们带来一道:{}”.format(chefname,deal))
defeating(custname:str):
foriinrange(3):
deal=q.get(block=True)
print(“顾客{}吃掉了:{}”.format(custname,deal))
q.task_done()
if__name__==”__main__”:
#创立并发动厨师ABC线程,创立并发动顾客1234线程
threadlist_chef=[Thread(target=cooking,args=chefname).start()forchefnamein[“A”,”B”,”C”]]
threadlist_cust=[Thread(target=eating,args=str(custname)).start()forcustnameinrange(4)]
#行列堵塞,直到一切线程对每个元素都调用了task_done
q.join()
上述程序履行成果如下图所示:
1.5杀死线程
在一些场景下,咱们可能需求杀死某个线程,可是在这之前,请仔细的考量代码的上下文环境。强制杀死线程可能会带来一些意想不到的成果,并且从程序设计来讲,这本身就是不合理的。而且,锁资源并不会由于当时线程的退出而开释,这在程序运转进程中,可能会成为典型的死锁场景。所以杀死线程之前,请一定稳重。杀死线程的办法网上有好几种,我这儿给出一种我觉得比较保险的办法。
前面咱们提到过如何做线程通讯,这儿能够用大局变量给出一个flag,线程使命选用循环方法进行,每次循环都会查看该flag,外界能够经过修正这一flag来通知这一线程退出循环,结束使命,然后起到杀死线程的目的,但请留意,为了线程安全,退出前一定要开释这一线程所占用的资源。下面给出一个示例程序:
fromthreadingimportLock,Thread
fromtimeimportsleep
flag=True
lock=Lock()
deftar():
globalflag,lock
whileTrue:
lock.acquire()
“线程使命逻辑”
ifflagisFalse:
break
lock.release()
lock.release()
if__name__==”__main__”:
thread=Thread(target=tar)
thread.start()
print(“3秒后线程会被杀死”)
sleep(3)
flag=False
print(“线程已被杀死”)
履行成果如图所示,假如需求其他的办法请自行查阅,网上有不少。
1.6线程池的运用
在程序运转进程之中,临时创立一个线程需求耗费不小的价值(包括与操作系统的交互部分),尤其是咱们只对一个线程分配一个简短的使命,此时,频频的线程创立将会严重拖垮程序的履行的效率。
因而,在这种景象下,咱们能够挑选选用线程池技术,即经过预先创立几个闲暇线程,在需求多线程来处理使命时,将使命分配给一个处于闲暇状况的线程,该线程在履行完结后,将会回归闲暇状况,而不是直接销毁;而假如申请从线程池中分配一个闲暇线程时,遇到一切线程均处于运转状况,则当时线程能够挑选堵塞来等候线程资源的闲暇。如此一来,程序对于线程的办理将会更加灵敏。
Python从3.2开始,就将线程池作为内置模块包含了进来,能够经过concurrent.futures.ThreadPoolExecutor来调用,运用办法也很简略。下面给出线程池的程序比如:
fromconcurrent.futuresimportThreadPoolExecutor
fromtimeimportsleep
tasklist=[“使命1″,”使命2″,”使命3″,”使命4″]
deftask(taskname:str):
sleep(5)
print(taskname+”已完结\n”)
returntaskname+”的履行成果”
executor=ThreadPoolExecutor(max_workers=3)#创立线程池(是一个ThreadPoolExecutor目标),线程数为3
future_a=executor.submit(task,tasklist[0])#经过submit办法向线程池提交使命,返回一个对应的Future目标
future_b=executor.submit(task,tasklist[1])
future_c=executor.submit(task,tasklist[2])
future_d=executor.submit(task,tasklist[3])#假如提交时,线程池中没有空余线程,则该线程会进入等候状况,主线程不会堵塞
print(future_a.result(),future_b.result())#经过Future目标的result()办法获取使命的返回值,若没有履行完,则会堕入堵塞
有关于线程池的详细运用办法,我后面还会出一篇文章,咱们这儿没理解的能够去看一下。
2.GIL大局解说器锁
2.1GIL是什么?
??GILGILGIL(GlobalInterpreterLockGlobalInterpreterLockGlobalInterpreterLock,大局解说器锁)是CPython中选用的一种机制,它保证同一时间只要一个线程在履行Python字节码。给整个解说器加锁使得解说器多线程运转更便利,而且开发的CPython也更易于维护,可是价值是牺牲了在多处理器上的并行性。因而,在相当多的场景中,CPython解说器下的多线程机制的功能都不尽如人意。
2.2GIL给Python带来的影响?
上图是DavidBeazley的UnderstandGIL幻灯片中的一张,用于描述GIL的履行模型。
从这套幻灯篇的介绍中,咱们能够得知,GIL本质上是条件锁与互斥锁结合的一种二值信号量类的一个实例,在程序履行进程中,一个线程经过acquire操作获得GIL,然后履行其字节码,而当其遇到IO操作时,他将会release开释掉GIL锁资源,GIL这时能够被其他线程获得以履行该线程的使命,而原先线程的IO操作将会一起进行。
由此咱们能够看到,GIL使得Python多线程程序在核算密布的场景下,不能充分运用多中心并发的优势(由于不管机器有多少中心,并且不管有多少线程来履行核算使命,一起运转的只要1个),而在IO密布的场景下,其功能受到的影响则较小。
2.3如何绕过GIL?
那么如何避免GIL对多线程功能带来的影响呢?
1.绕过CPython,运用JPython(Java完结的)等别的Python解说器
首要,GIL是CPython解说器中的完结,在JPython等其他解说器中并没有选用,因而咱们能够考虑更换解说器完结。咱们现在从官网下载,或许经过Anaconda部署的解说器遍及选用的是CPython,能够经过下面的办法安装其它完结:以JPython为例,去JPython官网下载其安装包(jar包),然后用java-jar(前提是你的电脑安装了Java环境)去履行它,最终再装备一下环境变量即可。
2.把关键功能代码,放到别的言语(一般是C++)中完结
这个是常用的一种办法,许多追求履行功能的模块例如Numpy、Pytorch等都是将本身功能代码放在C言语扩展中来完结的,如何开发Python模块的C言语扩展部分,能够参阅这个链接http://t.csdn.cn/4YuDO。
3.并行改并发,在Python中,多进程有时比多线程管用
Python程序的每个进程都有自己的GIL锁,互补干与,因而咱们也能够直接运用多线程来处理一些核算使命,Python的多线程能够运用multiprocessingmultiprocessingmultiprocessing模块来完结,示例程序如下。

Python多线程的基本概念

1.GIL锁
python的多线程,并不是真正的多线程,因为有GlobalInterpreterLock这个bug一般的大局锁存在,这使得同一时刻,只能有一个线程在履行。
需要留意的是,GIL锁并不是python言语的特性,它是完成CPython时引入的概念。一门言语的代码,能够由多种编译器来编译,比方C++的代码,你能够用GCC来编译,也能够用VisualC++,同理,一段python代码也能够在不同的履行环境来履行,比方CPython,PyPy,JPython,这其间,JPython就没有GIL锁,因为CPython是默认的履行环境,因而,给大家造成了误会,以为python这门言语很蛋疼的弄了一个GIL锁。
因为有大局锁的存在,所以,python很难有效的利用多核,但也不是一点用处都没有了,在IO密集型的使命里,仍是有用武之地的,比方你写一个多线程的爬虫,一个线程的恳求发出去今后,需要等候服务器返回数据,其他的线程就能够继续履行了,充分利用网络IO。
2.线程ID
有很多文章告诉你怎么获取线程的id,办法就是threading.currentThread().ident,但这是不对的,ident仅仅线程的标识,而非线程id,正确的做法是运用ctypes库获取,办法如下
importthreadingimportctypes
SYS_gettid=186libc=ctypes.cdll.LoadLibrary(‘libc.so.6’)
tid=libc.syscall(SYS_gettid)print(tid)print(threading.currentThread().ident)
上述代码要在linux环境下才干履行,当然,你也能够将线程标识用于区分线程。
3.发动多线程
importthreadingimporttimedefmy_print():foriinrange(10):print(i)
time.sleep(0.5)
t=threading.Thread(target=my_print)
t.start()print(‘主线程完毕’)
你所看到的,是一个非常简单的发动多线程的办法
运用threading.Thread创建一个线程,target参数指定的是线程要履行的使命
运用start()办法发动一个线程
调查打印内容能够发现,整个进程要比及子线程t完毕后才会完毕
仰赖于python言语的简洁性,发动一个多线程非常的简单,程序输出成果
0
主线程完毕
4.承继threading.Thread
除了第3小节所展示的办法以外,还能够经过承继threading.Thread来创建一个线程
importthreadingimporttimedefmy_print():foriinrange(10):print(i)
time.sleep(0.5)classPringThread(threading.Thread):def__init__(self,count):super().__init__()
self.count=countdefrun(self):foriinrange(self.count):print(i)
time.sleep(0.5)
t=PringThread(10)
t.start()print(‘主线程完毕’)
选用这种办法时,必须完成run办法
5.传入参数
修改my_print办法
defmy_print(count):foriinrange(count):print(i)
time.sleep(0.5)
经过args向线程传入参数
t=threading.Thread(target=my_print,args=(5,))
t.start()print(‘主线程完毕’)
args需要传入一个元组,因而,尽管只要一个参数,也要写一个逗号
6.后台线程
importthreadingimporttimedefmy_print(count):foriinrange(count):print(i)
time.sleep(0.5)
t=threading.Thread(target=my_print,args=(5,))
t.setDaemon(True)
t.start()print(‘主线程完毕’)
运用setDaemon办法将线程设置为后台线程,这意味着,主线程就不会等候它完毕,履行程序,输出成果为
0
主线程完毕
线程发动后刚刚输出一个0,主线程就现已完毕了,因为子线程是后台线程,因而输出内容不会在控制台上显示,假如你不喜欢主线程等候子线程运转的成果,那么就能够将子线程设置成后台线程
7.join
之前的示例代码中,发动线程后,立刻履行主线程里的代码,在实践应用中,一般,会运用join办法,等候子线程履行完毕
importthreadingimporttimedefmy_print(n):foriinrange(n):print(i)
time.sleep(0.5)
t_lst=[]foriinrange(3):
t=threading.Thread(target=my_print,args=(5,))
t_lst.append(t)fortint_lst:
t.start()fortint_lst:
t.join()print(‘主线程’)
三个子线程都是用join()办法,只要他们都履行完毕今后才会履行print(‘主线程’),等候的时刻是能够设置的,比方将上面的程序改为t.join(0.1),那么主线程会每个程序都等候0.1秒钟的时刻,0.3秒钟今后,主线程就不再继续等了,开始履行自己的代码,假如不设置时刻就表示一向等候,直到所有子线程完毕
扫描重视,与我技术互动
QQ交流群:2

赞(0)
未经允许不得转载:志达IT网站 » python多线程(Python多线程的基本概念)
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

志达IT网站 每天分享编程和互联网的IT技术博客

登录/注册联系我们