线程

想要了解线程的意义,首要咱们先看一下百度百科的界说:

线程(英语:thread)是操作体系能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实践运作单位。一条线程指的是进程中一个单一顺序的操控流,一个进程中能够并发多个线程,每条线程并行履行不同的使命。

简略来讲,当你打开电脑中的一个应用程序,其实此刻计算机就为你创立了一个进程,体系会为其进行资源分配而且对其进行调度。而线程便是比进程还要小的单位,多个线程完结不同的作业组成了咱们宏观上能够得到呼应的作业成果。

举个比如,进程就像一个大的工厂,工厂中有很多机床设备和场所。而不同的线程就像工厂中作业的工人,工厂为其分配不同的作业来完结一个最终的出产方针。咱们能够指派不同的工人做不同的作业或增加工人进步咱们的出产功率。

在编程中,线程能够由咱们启用帮助咱们完结不同的作业实现多线程并发,进步咱们的代码功率。

Python中的多线程

在python中主要有两种实现多线程的方式:

  • 经过threading.Thread () 办法创立线程

  • 经过承继 threading.Thread 类的承继重写run办法

接下来咱们分别说一下多线程的两种实现方式。

threading.Thread () 创立线程

为了更直观的了解这个过程,首要咱们先编写一个正常的函数,完结倒数5个数的功用,其中间隔一秒钟。

def fuc():
    for i in range(5):
        time.sleep(1)

在主函数中,咱们调用Thread()来实例化两个线程,让他们一同运转。

if __name__ == '__main__':
    t1 = threading.Thread(target=fuc, args=(1,), daemon=True)
    t2 = threading.Thread(target=fuc, args=(2,), daemon=True)
    t2.start()
    t1.start()

整体代码如下所示:

import threading
import time
def fuc():
    for i in range(5):
        time.sleep(1)
if __name__ == '__main__':
    t1 = threading.Thread(target=fuc)
    t2 = threading.Thread(target=fuc)
    t2.start()
    t1.start()

咱们先不讨论调用的函数以及传入的参数,先来看一下运转作用:

0
0
11
22
33
44

能够看到,两个打印的成果基本上是一同呈现的,而且呈现了混合的状况,证明两个打印的函数正在一同进行。

接下来咱们就来介绍一下类的初始化参数以及咱们调用的函数:

thread.Thread(group=Nore,targt=None,args=(),kwargs={},*,daemon=None)

在该类中主要由以下几个参数组成:

  • group:与ThreadGroup类相关,一般不运用。

  • target:线程调用的方针,便是方针函数,在上述的比如中咱们传入的是咱们编写的函数fuc。

  • name:线程的姓名,默许是Tread-x。

  • args:为方针函数传递关键字参数,字典。

  • daemon:用来设置线程是否随主线程退出而退出,涉及到主线程相关常识,咱们稍后介绍。

接下来介绍咱们常用的几个办法:

  • run():表示线程发动的活动,在第二种承继写法中会用到。

  • start():激活线程,使其能够被调度。

  • join():等候至线程停止,这个办法涉及到主线程的常识,咱们稍后介绍。

  • isAlive():回来线程是否活动。

  • getName():回来线程名称。

  • setName() : 设置线程名称。

接下来,咱们运用上述参数更改示例,让函数获取一个参数,并为不同的线程设置姓名。代码如下:

import threading
import time
def fuc(num):
    for i in range(5):
        print('接收到参数{}:'.format(num), i)
        time.sleep(1)
if __name__ == '__main__':
	# 传入参数及姓名
    t1 = threading.Thread(target=fuc, args=(1,), name='t1')
    t2 = threading.Thread(target=fuc, args=(2,), name='t2')
    t1.start()
    print(t1.getName(), '开端运转...')
    t2.start()
    print(t2.getName(), '开端运转...')

运转成果如下:

接收到参数1:t1 开端运转...
0
接收到参数2: t20 开端运转...
接收到参数1:接收到参数21 
1
接收到参数1:接收到参数22
2
接收到参数1:接收到参数233
接收到参数1:接收到参数24
4

能够看到,尽管成果很紊乱,但是咱们传入的参数以及获取的姓名都被打印出来了。

别的,这儿有两个留意:

  • trgat参数承受的是函数姓名不需求加括号。
  • args传入的履行函数参数要加括号和逗号,保证其是一个元组。

承继 threading.Thread 类的线程创立

在上面的比如中,咱们已经了解了多线程的一种创立办法。接下来咱们来介绍第二种办法,这也是众多大佬很喜欢的一种办法,经过承继 threading.Thread 类的线程创立。

class MyThread(threading.Thread):
    def run(self) -> None:
        for i in range(5):
            print(i)
            time.sleep(1)
if __name__ == '__main__':
    t1 = MyThread(name='t1')
    t2 = MyThread(name='t2')
    t1.start()
    t2.start()

运转成果如下:

0
0
11
22
33
44

留意:这儿调用的是start办法而不是run办法,否则会编程单线程履行。

主线程

在了解了多线程的编程办法之后,咱们来介绍一下主线程及相关参数和办法。

在咱们履行多线程程序的过程中,存在一个主线程,而咱们开辟的其他线程其实都是它的子线程。由主线程主导的作业有以下两种状况:

  • 因为主线程完毕了,强制中止其它线程的作业,但此刻其他线程有可能还没有完毕自己的作业。
  • 主线程完毕后,等候其他线程完毕作业,再中止一切线程的作业。

能够简略地了解为包工头,它是这些线程的头子!其从微观角度上讲掌管了必定的作业流程,它能够挑选是否等候其它工人完毕作业再完毕整个作业。

而咱们能够运用参数或者办法操控这个过程。

运用daemon参数操控过程

在上边的函数参数介绍中,提到了daemon参数,其为False时,线程不会随主线程完毕而退出,主线程会等候其完毕后再退出。而为True时则不论子线程是否完结了相关作业都会直接退出。

接下来咱们看两个示例,咱们修改方才的示例代码的daemon参数为True,表示不论子线程是否完结了作业都强制退出。

import threading
import time
def fuc(num):
    for i in range(5):
        print('接收到参数{}:'.format(num), i)
        time.sleep(1)
if __name__ == '__main__':
    t1 = threading.Thread(target=fuc, args=(1,), name='t1', daemon=True)
    t2 = threading.Thread(target=fuc, args=(2,), name='t2', daemon=True)
    t1.start()
    print(t1.getName(), '开端运转...')
    t2.start()
    print(t2.getName(), '开端运转...')
    print("我是主线程,都给我停下!")

成果如下:

接收到参数1:t1 0
开端运转...
接收到参数2:t2  0
开端运转...
我是主线程,都给我停下!

能够看到,子线程的倒数还没有完毕,因为主线程完毕了,一切线程一同完毕了。
这儿要留意以下几点:

  • daemon特点必须在start( )之前设置。
  • 从主线程创立的一切线程不设置daemon特点,则默许都是daemon=False。

运用.join()堵塞线程

除此之外,咱们还能够调用.join()办法堵塞线程,调用该办法的时候,该办法的调用者线程完毕后程序才会停止。

#timeout参数标明等候的时长,不设置该参数则默许为一直等候。
join(timeout-=None)

咱们来看下面这个示例,咱们更改了两个函数的倒计时时刻,使第一个线程的倒计时时刻更长,并对第二个线程进行了堵塞操作。代码如下:

import threading
import time
def fuc1():
    for i in range(10):
        print(i)
        time.sleep(1)
def fuc2():
    for i in range(5):
        print(i)
        time.sleep(1)
if __name__ == '__main__':
    t1 = threading.Thread(target=fuc1, name='t1', daemon=True)
    t2 = threading.Thread(target=fuc2, name='t2', daemon=True)
    t1.start()
    print(t1.getName(), '开端运转...')
    print('我是二儿子,等等我!')
    t2.start()
    print(t2.getName(), '开端运转...')
    t2.join()
    print("我是主线程,都给我停下!")

成果如下:

0t1
开端运转...
我是二儿子,等等我!
0t2 
开端运转...
11
22
33
44
我是主线程,都给我停下!5

咱们能够看到,上述代码中线程一还没有完毕倒数十个数,程序就完毕了。在此过程中,主线程只等候了第二个线程完毕,整个程序就完毕了。

线程同步

在多个线程同步运转的状况下,会呈现多个线程一同操作一个数据的状况。如果两个线程一同操作同一个变量的话,很简单呈现紊乱的状况。所以,咱们需求一个工具来保证在同一时刻只能有一个线程处理数据。

线程类供给了锁来解决问题,当线程申请处理某个数据时申请一个锁来操控住当前数据,完毕处理时即将锁释放。

threading中的锁

python的threading中为咱们供给了RLock锁来解决多线程一同处理一个数据的问题。在某个时刻,咱们能够让线程申请锁来维护数据此刻只能供该线程运用。

为了更好的了解该过程,咱们界说一个大局变量,让每一个线程都对其操作但不设置锁,调查变量的改变:

R_LOCK = threading.Lock()
COUNT = 100
class MyThread(threading.Thread):
    def run(self) -> None:
        global COUNT
        #R_LOCK.acquire()
        COUNT -= 10
        time.sleep(1)
        print(self.getName(), COUNT)
        #R_LOCK.release()
if __name__ == '__main__':
    threads = [MyThread() for i in range(10)]
    for t in threads:
        t.start()

成果如下:

Thread-3Thread-10  0Thread-8Thread-7 0Thread-6 0Thread-5Thread-9
Thread-1 0Thread-2 00  0
Thread-4 000

能够看到,咱们的数据发生了反常,这并不是咱们想要得到的成果,若把锁给关闭注释让其正常运转能够看到以下的正常成果:

Thread-1 90
Thread-2 80
Thread-3 70
Thread-4 60
Thread-5 50
Thread-6 40
Thread-7 30
Thread-8 20
Thread-9 10
Thread-10 0

结语

多线程编程是一个非常重要的编程思想,了解多线程编程有助于咱们更好的了解规划模式。

当然,python中的编程并不是真实的多线程履行,这涉及到GIL大局解说锁相关的常识。所以其针对CPU密集型使命来说并没有很好的作用,接下来我将会更新相关的内容进行更多的阐明。

如果有什么问题能够私信我或者是在谈论区留言与我一同交流,如果你觉得我写的不错,费事你帮我点个赞吧!