当前位置: 首页 > 后端技术 > Python

Multitasking(二):多线程

时间:2023-03-25 21:46:52 Python

代码环境:python3.6上一篇介绍了python中multiprocessing的使用:点击阅读,现在说说多线程。一个进程由多个线程组成,一个进程至少有一个线程。当任何一个进程启动时,都会默认启动一个线程,我们称之为主线程,然后主线程会创建其他新的子线程。简单的多线程例子常用的多线程模块是threading,例子:fromthreadingimportcurrent_thread,Thread,Locknum=0defworker1(lock):globalnumthread_name=current_thread().nameprint(f'childthread{th??read_name}running...')foriinrange(100000):withlock:num+=1print(f'子线程{thread_name}结束。')defworker2(lock):globalnumthread_name=current_thread().nameprint(f'子线程{thread_name}running...')foriinrange(100000):withlock:num+=1print(f'sub-thread{th??read_name}ends.')defthread_main():lock=Lock()print(f'currentmainthread:{current_thread().name}')t1=Thread(target=worker1,args=(lock,))t2=Thread(target=worker2,args=(lock,))t1.start()t2.start()t1.join()t2.join()print(f'finalnumvalue:{num}')print(f'mainthread{current_thread().name}结束。')if__name__=='__main__':thread_main()在上面的例子中,我们最终的期望是num=200000。如果我们去掉withlock:操作数据的时候再运行代码几次,可能就得不到我们的预期了。这是因为:同一个进程的多个线程共享除栈和寄存器以外的所有数据,多线程对同一个全局变量的操作造成数据冲突。为了避免这种冲突,我们需要在操作全局变量时使用threading.Lock来锁定数据。另外,如果去掉join()方法,我们会发现主线程可能会提前结束,但是子线程仍然可以继续执行。这与多进程不同:主线程的终止不会引起子线程的终止。ThreadLocal在多线程环境下,每个线程都有自己的数据。线程使用自己的局部变量比使用全局变量要好,因为局部变量只能被线程自己看到,不会影响其他线程,而且对全局变量的修改必须加锁。但是局部变量在调用函数时传递起来比较麻烦,所以我们改用threading.local()创建的ThreadLocal对象。例子:fromthreadingimportcurrent_thread,Thread,local#创建一个全局的ThreadLocalthread_local=local()defcall_student():#可以直接访问当前线程关联的studentprint(f'{current_thread().name}:Iamastudent{thread_local.student}')defstudent_worker(name):#给执行这个方法的线程绑定一个属性student,这个线程的所有方法都可以访问thread_local.student=namecall_student()defthread_main():t1=Thread(target=student_worker,args=('Amy',))t2=Thread(target=student_worker,args=('Tom',))t1.start()t2.start()t1.join()t2.并且随意写没有干扰,而且ThreadLocal内部已经自动处理了锁的问题。ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接、HTTP请求、用户身份信息等,以便一个线程调用的所有处理函数都可以方便地访问这些资源。线程间通信:生产者-消费者模型线程间通信常用queue.Queue。在多线程开发中,假设一个线程负责生产,一个线程负责消费。如果生产速度快于消费速度,那么生产者需要等待消费者处理完,再继续生产数据。为了解决这种处理能力不均衡的问题,我们引入生产者-消费者模型,在两者之间建立一个缓冲Queue进行通信。例子:fromthreadingimportThread,current_thread,localfromqueueimportQueueimporttime,random#buffer,storeproductqueuestore_queue=Queue(5)defproducer():thread_name=current_thread().name#productnumber#localvariable不需要locksn=0whilesn<10:ifnotstore_queue.full():msg=thread_name+':number'+str(sn)store_queue.put(msg)print(f'{thread_name}threadproduced{msg}')sn+=1defconsumer():thread_name=current_thread().name#统计消费的产品数量,方便后面终止循环consume_num=0whileconsume_num<20:ifnotstore_queue.empty():msg=store_queue.get()print(f'{thread_name}线程消耗{msg}')consume_num+=1time.sleep(random.random())defthread_main():#2producerthreadsforiinrange(2):t_p=Thread(target=producer,name=f'Producer-{i}')t_p.start()#1个消费者线程t_c=Thread(target=consumer,name='Consumer')t_c.start()if__name__=='__主要的__':thread_main()globalinterpreterlockGILpython解释器在执行代码时,有一个GIL:任何python线程在执行之前,都必须先获取GIL,然后每执行100个字节码就自动释放GIL,让其他线程要有机会执行GIL,这样多个线程只能交替执行。即使一台机器的CPU有100个核心,一个进程的100个线程也只能使用1个核心。所以,如果真的想让多线程满核运行,一种方法是使用C语言写的python库来管理GIL,但是这样会大大增加代码的复杂度,我们一般不做这。多线程没用吗?不是!python标准库中所有执行阻塞IO操作的函数都会在等待操作系统返回结果时释放GIL,time.sleep()函数也会释放。因此,多线程可以在IO密集型操作中发挥作用。综上所述,我们常用的threading模块使用的是多线程,多线程在同一个进程中共享数据;在多线程中,更推荐使用变量:ThreadLocal对象和局部变量,操作同一个全局变量容易造成数据冲突,需要锁隔离;线程间通信使用queue.Queue;GIL的存在使得多线程无法利用多核,但是多线程可以在IO密集型操作中发挥作用。如果必须使用多核,可以使用多进程。