一、进程、线程、协程

1、GIL:全局解释器锁

python执行程序需要解释器。

GIL:当执行多线程,由GIL控制同一时刻只有一个线程能够运行。

为了更有效的利用多核处理器的性能,就出现了多线程的编程方式,而随之带来的就是线程间数据一致性和状态同步的困难。为了利用多核,Python开始支持多线程。解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。  于是有了GIL这把超级大锁。GIL无疑就是一把全局排他锁。毫无疑问全局锁的存在会对多线程的效率有不小影响。甚至就几乎等于Python是个单线程的程序。

2、并行、并发、进程、线程、协程

并行:不同代码块同时执行,多核CPU,每个CPU单独执行一个程序,各CPU之间的数据相互独立。

并发:不同代码块交替执行,一个CPU为基础的,使用多线程等方式提高CPU效率,线程之间会相互切换,轮流被Python解释器执行。

进程:一个实体。进程只能在一个时间干一件事。一个进程中可以并发多个线程,每条线程并行执行不同的任务。

线程:进程中的一个实体。同一进程中的多个线程并发执行,这些线程共享进程所拥有的资源。

协程:子程序调用是通过栈实现的,一个线程就是执行一个子程序。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

协程又称为微线程,它是实现多任务的另一种方式,只不过是比线程更小的执行单元。因为它自带CPU的上下文,这样只要在合适的时机,我们可以把一个协程切换到另一个协程。

对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。

有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。

由于每个进程至少要干一件事,所以,一个进程至少有一个线程。当然,像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。

多任务的实现有3种方式:

  • 多进程模式;
  • 多线程模式;
  • 多进程+多线程模式。

二、线程实例

(1.)使用线程

使用线程threading.Thread()方法、start()方法开始执行线程。

使用threading.Thread()创建对象后,还要由该对象调用start()方法开始执行线程,并且程序会等待线程执行完毕才会终止

import time
import threading

#用以线程活动的方法
def run(n, **kwargs):
    print('name', kwargs.get('name'))
    print('task', n)
    time.sleep(1)


if __name__ == '__main__':
    #target是要执行的函数名(不是函数),args是函数对应的参数,以元组的形式存在
    t1 = threading.Thread(target=run, args=('t1',), kwargs={'name': 'Tom'})
    t2 = threading.Thread(target=run, args=('t2',), kwargs={'name': 'Lily'})
    #启动线程
    t1.start()
    t2.start()
    print('主程序已执行完')

运行结果如下:

name Tom
 task t1
 name Lily


task主程序已执行完

t2
Process finished with exit code 0

自定义线程类是继承threading.Thread类来定义线程类的,其本质是重构Thread类的实例方法run(),将实例方法run()改为我们需要执行的代码。

import time
import threading


class MyThreading(threading.Thread):
    def __init__(self, n):
        super().__init__()
        self.n = n

    #重构实例方法run()
    def run(self):
        print('task', self.n)
        time.sleep(1)


if __name__ == '__main__':
    t1 = MyThreading('t1')
    t2 = MyThreading('t2')
    t1.start()
    t2.start()

运行结果如下:

task t1
 task t2Process finished with exit code 0

补充知识点:

1.类中初始化形式def __init__(self)和def __init__(self, 参数1,参数2,···,参数n) 

  • 形式1:def __init__(self)
  • 形式2:def __init__(self, 参数1,参数2,···,参数n)
class Student:
	#初始化形式1
    def __init__(self):
        self.name = None
        self.score = None
    #初始化形式2
    def __init__(self, name, score):
        self.name = name
        self.score = score
  • 前者在__init__方法中,只有一个self,指的是实例的本身,但是在方法的类部,包含两个属性,name, score
  • 后者即是在定义方法时,就直接给定了两个参数。

第一种,需要实例化之后,对属性进行赋值。
第二种,就是直接实例化时,传入相应的参数

两种方法的区别在于定义函数时属性赋值是否允许为空和实例化时是否直接传入参数。

形式1:def init(self)

这种形式在__init__方法中,只有一个self,指的是实例的本身,但是在方法的类部,包含两个属性,name, grade。它允许定义一个空的结构当新数据来时,可以直接添加
实例化时,需要实例化之后,再进行赋值

class Student:  
    def __init__(self): 
        self.name = None
        self.score = None
        
    def print_score(self):
        print("%s score is %s" % (self.name,self.score))
        
if __name__ == '__main__':
	s1 = Student()  # 创建对象s1
	s1.name = "Tom"
	s1.score = 98

	s2 = Student()  # 创建对象s2
	s2.name = "Jerry"
	s2.score = 76

	s1.print_score()
	s2.print_score()

运行结果如下:

Tom score is 98
Jerry score is 76

Process finished with exit code 0

形式2:def init(self, 参数1,参数2,···,参数n)
这种形式在定义方法时,就直接给定了两个参数name和grade,且属性值不允许为空。
实例化时,直接传入参数。

self是形式参数,

当执行s1 = Student(“Tom”, 88)时,self=s1;
 当执行s2 = Student(“sunny”, 76)时,self=s2。


 

class Student:
    def __init__(self, name, score):
        self.name = name
        self.score = score

    def print_score(self):
        print("%s score is %s" % (self.name,self.score))

if __name__ == '__main__':
    s1 = Student("Tom", 88)  # 创建对象s1
    s2 = Student("Jerry", 76)  # 创建对象s2

    s1.print_score()
    s2.print_score()

运行结果如下:

Tom score is 88
 Jerry score is 76Process finished with exit code 0

2.super().__init__()

super().__init__(),就是继承父类的init方法,同样可以使用super()点 其他方法名,去继承其他方法。

1、从实例中对比

1.1、实例

#先写一个父类

class Person:
    def __init__(self, name='Person'):
        self.name = name

#再写三个继承自该父类的子类

#第一个子类Puple直接继承
class Puple(Person):
    pass

#第二个子类Puple_Init继承并写了init方法
class Puple_Init(Person):
    def __init__(self, age):
        self.age = age

#第三个子类Puple_Super继承并写了init方法,还添加了super().__init__()
class Puple_Super(Person):
    def __init__(self, name, age):
        self.age = age
        super().__init__(name)

#分别创建三个类的对象
pp = Puple()
pp_i = Puple_Init(10)
pp_s = Puple_Super('Puple_Super', 12)

(2.)守护线程setDaemon()方法

join()方法:主线程A中,创建了子线程B,并且在主线程中调用了B.join()方法,那么主线程A会在调用的地方等待,直到子线程B完成操作后,才可以接着往下执行。

setDaemon()方法:主线程A中,创建了子线程B,并且在主线程A中调用了B.setDaemon()方法,这个意思是把主线程A设置为守护线程,这个时候,要是主线程A执行结束了,就不用管线程B是否完成,一并和主线程A退出。

# 注意:setDaemon() 必须在start()方法调用之前设置。

import time
import threading


def run(n):
    time.sleep(3)
    print('task', n)


if __name__ == '__main__':
    t = threading.Thread(target=run, args=('t1',))
    t.setDaemon(True)
    t.start()
    print('end')
运行结果如下:

end

Process finished with exit code 0

(2.)等待线程结束join()方法
import time
import threading
def run(n):
    time.sleep(5)
    print('task',n)

if __name__=='__main__':
    t=threading.Thread(target=run,args=('t1',))
    t.start()
    t.join()
    print('end')

运行结果如下:

task t1
 endProcess finished with exit code 0


 

(3.)多线程共享全局变量

import threading

g_num = 100


def work1():
    global g_num
    for i in range(3):
        g_num += 1
    print('in work1 g_num is : %d' % g_num)


def work2():
    global g_num
    g_num += 1
    print('in work2 g_num is : %d' % g_num)


if __name__ == '__main__':
    t1 = threading.Thread(target=work1)
    t2 = threading.Thread(target=work2)
    t1.start()
    t2.start()
运行结果如下:
in work1 g_num is : 103
 in work2 g_num is : 104Process finished with exit code 0

(4.)互斥锁

import threading
import time


def work(t, s):
    time.sleep(s)
    f = open('test.txt', 'a+')
    f.write(f'This is {t}\n')
    f.close()


if __name__ == '__main__':
    t1 = threading.Thread(target=work, args=('t1', 3))
    t2 = threading.Thread(target=work, args=('t2', 1))
    t1.start()
    t2.start()
运行结果如下:
This is t2
This is t1

(5.)信号量

信号量是限制程序同时能执行多少条线程,它的使用与互斥锁(递归锁)大致相同,也是在线程执行的函数中调用相应的函数方法

概念:信号量和锁相似,锁同一时间只允许一个对象(进程)通过,信号量同一时间允许多个对象(进程)通过。

格式:导入信号量模块;实例化信号量对象,可以规定信号量的个数;传递对象;拿到一把钥匙;释放一把钥匙。

过程:获得钥匙,当钥匙串没钥匙时,其它进程要在外面等候,当释放一把钥匙时,一个进程进入。

import threading
import time


def run(n, semaphore):
    # 加锁
    semaphore.acquire()
    time.sleep(3)
    print('run the thread:%s\n' % n)
    # 释放
    semaphore.release()


if __name__ == '__main__':
    # 最多允许两个线程同时运行
    semaphore = threading.BoundedSemaphore(2)
    for i in range(4):
        t = threading.Thread(target=run, args=(f't-{i}', semaphore))
        t.start()

运行结果如下:

run the thread:t-0
 run the thread:t-1 run the thread:t-2
 run the thread:t-3Process finished with exit code 0

(6.)线程事件

线程事件是程序控制线程的执行,事件是一个简单的线程同步对象,其主要提供以下几个方法:(1)clear()将全局变量flag设置为False。(2)set()将flag设置为True。(3)is_set()判断全局变量flag的值是否为True。(4)wait()是一直监听全局变量flag,如果flag等于False,就一直处于阻塞状态。

线程事件的处理机制是定义一个全局变量flag,如果flag的值为False,event.wait()就处于阻塞状态,如果flag的值为True,event.wait()就不再阻塞

当我们同时启动线程t1和t2的时候,线程t1会根据循环次数设置线程事件event的clear()和set(),并且每次循环都会调用is_set()检测全局变量flag的值,当全局变量flag的值等于True的时候,线程t1将终止循环,完成线程t1的执行过程。与此同时,线程t2在循环过程中也是不停地判断全局变量flag的值,当flag的值为True的时候,线程t2也会终止循环。

import threading
import time

# 定义事件对象
event = threading.Event()

def t1():
    i = 0
    while 1:
        if i < 3:
            #clear()将全局变量flag设置为False
            event.clear()
            print('阻塞中')
        elif i == 4:
            #set()将flag设置为True
            event.set()
            print('阻塞结束')
        time.sleep(1)
        #is_set()判断全局变量flag的值是否为True,若是则终止循环
        if event.is_set():
            break
        i += 1

def t2():
    while 1:
        print('This is T2')
        if event.is_set():
            break
        time.sleep(2)

t1 = threading.Thread(target=t1, )
t2 = threading.Thread(target=t2, )
# 启动线程
t1.start()
t2.start()
# 监听事件.wait()是一直监听全局变量flag,如果flag等于False,就一直处于阻塞状态,如果flag的值为True,event.wait()就不再阻塞
event.wait()

运行结果如下:

阻塞中
 This is T2
 阻塞中
 This is T2
 阻塞中
 This is T2
 阻塞结束
 This is T2Process finished with exit code 0