Python中的线程和进程

一、概述

线程:是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程实际运作单位。一条线程指的是进程中一个单一顺序的控制流。一个进程可以并发多个线程,每条线程执行不同任务。一个线程是一堆执行指令,可以资源共享。

进程:是一堆资源的整合,管理它里面的线程。如果执行相同的东西,进程和线程之间没有谁快谁慢的说法。线程里面可以资源共享,进程就不行,进程要通过队列等方式。

区别:

1. 线程可以资源共享,进程不行。

2. 线程之间可以通信,进程不行。

3. 线程容易被创建,但是进程就不行,进程消耗较大。

4. 主线程可以影响子线程,进程之间,完全独立。

并发和并行

并发:逻辑上同时发生。例如一个处理器同时处理多个任务,但是在此过程中会抢占CPU,给人一种同时发生的感觉,实际上有先后顺序。

并行:实际上同时发生。例如两个处理器同时处理两个任务,不抢占CPU,真正意义上的两件或多件事一起干。

 GIL锁

        GIL是全局解释器锁,是cpython里面的一个bug,一次只能进去一个线程,但是可以多进程。如果想要充分利用多核CPU,可以用multprocessing(多进程的一个模块),协程是一个很好的解决方案。

其他知识

python解释器:cpython、pipy、gpython

I/O密集型任务或函数:可以利用休息时间,去干别的,此类任务可以用多线程。

CPU/计算 密集型任务或函数:因为里面东西都要跑,如果只有一个CPU,会出现抢占资源的问题,此类任务可以用协程解决,但是资源和内存都会消耗很大,最好用C来实现。

二、线程

1. 通过threading库来实现多线程

import threading
import time

begin = time.time()
def first():
    print('这是第一个人物')

def second(n):
    print('这是第二个任务')
    print('%s' % n)

t1 = threading.Thread(target=first)  # first后面加不加括号都一样,如果函数有参数,一定不能加括号
t2 = threading.Thread(target=second,args=(1,))   # 如果函数有参数,可以加args,参数要以元组形式加

t1.start()  # 用start来启动线程
t2.start()

t1.join()  # 如果 t1 不执行完,就不往下执行,起阻塞作用
t2.join()

end = time.time()
print(end - begin)

2. 守护线程(t1.setDaemon(True))

        如果将一个线程设置为守护线程,那么当主线程执行完,会强制结束守护线程。

import time
import threading

def game(n):
    print('%s---开始游戏' % n)
    time.sleep(3)
    print('游戏结束')

def movie(m):
    print('%s---电影开始' % m)
    time.sleep(5)
    print('电影结束')

t1 = threading.Thread(target=game,args=('荒野大嫖客',))
t2 = threading.Thread(target=movie,args=('黑客帝国',))
threads = []
threads.append(t1)
threads.append(t2)
if __name__ == '__main__':
    t1.setDaemon(True)   # 主线程结束,守护线程跟着结束
    for t in threads:

        t.start()
    # t.join()  # 这里的t是默认继承 t.start() 中最后的t
    print('end=======')

        在守护线程当中,只会等执行时间长的,上面例子当中,t1时间短,所以在等待t2的时候,t1会执行完,因此结果没变化。如果将t2设置为守护线程,t1结束后,会直接结束t2,不会执行t2后面的内容。 

3. 同步锁

        如果同时执行100个线程,同时去取num这一个资源,这样很容易出错,有可能第一个线程执行到一半就停下了,然后就开始执行第二个线程。要解决这种问题,就要通过上锁的方式来解决。当开启锁时,只允许当前线程执行,直到这个锁被释放,才会把CPU给下一个线程。

a = threading.Lock()    # 把锁赋值给一个对象

a.acquire()        # 开启锁

# 这里是中间内容

a.release()        # 关闭锁

import threading
import time


def addnum():
    global num
    # num -= 1       # 拆分成下面的写法,并且中间加点东西才能看出效果

    a.acquire()      # 开启锁
    temp = num

    # print(456)       # 中间加这个,或者下面的 time.sleep 都行,建议加这个
    time.sleep(0.001)

    num = temp-1
    print('hello')
    a.release()     # 关闭锁

a = threading.Lock()
num = 100
threads = []
for i in range(100):
    t = threading.Thread(target=addnum)
    t.start()
    threads.append(t)

for t in threads:   # 等100个线程跑完,再去执行主线程
    t.join()

print('finally--num:',num)

 4. 死锁与递归锁

        以下这种情况,就会出现死锁的情况

import threading,time

class myThread(threading.Thread):
    def doA(self):
        lock.acquire()
        print(self.name,"gotlockA",time.ctime())
        time.sleep(3)
        lock.acquire()
        print(self.name,"gotlockB",time.ctime())
        lock.release()
        lock.release()

    def doB(self):
        lock.acquire()
        print(self.name,"gotlockB",time.ctime())
        time.sleep(2)
        lock.acquire()
        print(self.name,"gotlockA",time.ctime())
        lock.release()
        lock.release()
    def run(self):      # 这玩意默认执行,特殊方法
        self.doA()
        self.doB()
if __name__=="__main__":

    # lockA=threading.Lock()
    # lockB=threading.Lock()

    lock = threading.RLock()   # 这个是解决办法

    threads=[]
    for i in range(5):
        threads.append(myThread())   # 调用类中的run方法,这个叫做集成式调用
    for t in threads:
        t.start()
    for t in threads:
        t.join()

         死锁现象就是我们创建了两把锁,当线程一执行完doA时,回去执行doB,然后线程二又会去doA,然后lockA和lockB就乱套了,都需要对方的锁,所以就会卡在那里。

        Rlock时递归锁,它内部有个累加器,创建一次acquire,内部就会+1,可以无限加,release一次,就会释放相应的锁,这样就不会出现死锁现象。它的解释顺序相当于:

进学校门---进教室门---出教室门---出学校门

5. 信号量(Semaphore)

        它相当于是设置了房间,不管有多少个线程,房间有几个位置,每次就只能进去几个,等里面的出来后,其他线程才能继续进去。

import threading
import time

class mythread(threading.Thread):

    def run(self):
        if semaphore.acquire():
            print(self.name)  # 这里的self.name是调用对象名
            time.sleep(3)
            semaphore.release()


semaphore = threading.BoundedSemaphore(2)    # 设置两个房间,每次只能同时进两个线程
# semaphore = threading.Semaphore(5)   # 效果和上面这个一样,不过有区别
thread = []
for i in range(20):
    thread.append(mythread())
for t in thread:
    t.start()

 BoundedSemaphore与Semaphore的区别

        前者在调用release()的时候,会校验一下当前信号量的值,是否会大于初始值。

        假如只定义了5个信号量,释放了5次后,如果继续调用release,前者会抛出异常,后者只会返回None

 6. 条件变量同步

wait():条件不满足时调用,线程会释放锁,并进入等待阻塞。

notify():条件创造后调用,通知等待池激活一个线程。

notifyAll():条件创造后调用,通知等待池激活所有线程。

import threading
import time
from random import randint

class producer(threading.Thread):
    global baozi
    def run(self):
        while True:
            b = randint(1,100)
            print(self.name, ':做了这个包子--', str(b))
            if conn.acquire():
                baozi.append(b)
                conn.notify()   # 通知消费者开始吃
                conn.release()
            time.sleep(3)



class consumer(threading.Thread):
    global baozi
    def run(self):
        while True:
            conn.acquire()
            if len(baozi) == 0:
                conn.wait()
            print('消费者吃掉了',baozi[0])
            del baozi[0]
            conn.release()
            time.sleep(0.25)


baozi = []
thread = []

conn = threading.Condition()
for i in range(2):
    thread.append(producer())
thread.append(consumer())
for t in thread:
    t.start()
for t in thread:
    t.join()

        可以认为Condition对象维护了一个锁(Lock/RLock)和一个waiting池。线程通过acquire获得Condition对象,当调用wait方法时,线程会释放Condition内部的锁并进入blocked状态,同时在waiting池中记录这个线程。当调用notify方法时,Condition对象会从waiting池中挑选一个线程,通知其调用acquire方法尝试取到锁。

        Condition对象的构造函数可以接受一个Lock/RLock对象作为参数,如果没有指定,则Condition对象会在内部自行创建一个RLock。

        除了notify方法外,Condition对象还提供了notifyAll方法,可以通知waiting池中的所有线程尝试acquire内部锁。
        由于上述机制,处于waiting状态的线程只能通过notify方法唤醒,所以notifyAll的作用在于防止有的线程永远处于沉默状态。

7. 同步条件Event

event.isSet():返回event的状态值;
event.wait():如果 event.isSet()==False将阻塞线程;
event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
event.clear():恢复event的状态值为False。

import threading
import time

class boss(threading.Thread):
    def run(self):
        print('今晚大家要加班!!!!')
        event.set() or event.isSet()
        time.sleep(5)
        print('可以下班回家了')
        event.set() or event.isSet()

class worker(threading.Thread):
    def run(self):
        event.wait()
        # lock.acquire()  # 加这个的原因时看着输出同一行,很不爽
        print('我***,****')
        # lock.release()
        time.sleep(0.5)
        event.clear()
        event.wait()
        time.sleep(0.5)
        # lock.acquire()
        print('终于下班了')
        # lock.release()

event = threading.Event()
lock = threading.RLock()
thread = []
for i in range(3):
    thread.append(worker())
thread.append(boss())
for t in thread:
    t.start()
    # time.sleep(0.01)  # 加这个的原因时看着输出同一行,很不爽
for t in thread:
    t.join()

8. 多线程利器 queue 

        它时一种数据结构,用来储存数据的。它定义了三种队列模式:

1.Queue(maxsize):FIFO队列模式(first in first out),先入先出,缺省参数为0,无穷大
2.lifoQueue(maxsize):LIFO队列模式(last in first out),后入先出
3.PriorityQueue(maxsize):优先级队列模式,不常用,使用它时,项目应是(priority,data)的形式

import queue

q = queue.Queue(3)  # 括号里可以设置放入数据的多少,这种方式创建的时线程队列
                    # 如果不设长度,默认无限长
q = queue.Queue(maxsize=3)  # 与上等价

q.put(item, block=True, timeout=None)    # 将Item放入队列
                                         # block : Ture    队列已满,full方法会异常
                                         #          False   队列未满,立即使用
                                         # timeout表示阻塞队列时长
q.put(2)
q.put(2)
q.put(2)
q.put(2)


q.get(block=True, timeout=None)    # 从对列中移除并返回一个数据。当队列为空值,将一直等待。
print(q.get())
print(q.get())
print(q.get())
print(q.get())

# 如果存4个,取3个,什么都不打印,程序会一直处于等待状态,不会停止
# 如果存3个,取4个,打印3个,程序会一直等待放入第四个,不会停止


q.full()    # 当队列任务已满时,返回True,否则返回False。
q.empty()   # 队列为空返回True,否则返回False。
q.qsize()   # 返回队列的大小

三、进程

        由于GIL的存在,python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。Python提供了非常好用的多进程包multiprocessing,只需要定义一个函数,Python会完成其他所有事情。借助这个包,可以轻松完成从单进程到并发执行的转换。
        multiprocessing支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。

        multiprocessing包是Python中的多进程管理包。与threading.Thread类似,它可以利用multiprocessing.Process对象来创建一个进程。该进程可以运行在Python程序内部编写的函数。
该Process对象与Thread对象的用法相同,也有start(), run(), join()的方法。
        此外multiprocessing包中也有Lock/Event/Semaphore/Condition类 (这些对象可以像多线程那样,通过参数传递给各个进程),用同步进程,其用法与threading包中的同名类一致。所以,multiprocessing的很大一部份与threading使用同一套API,只不过换到了多进程的情境。

        在UNIX平台上,当某个进程终结之后,该进程需要被其父进程调用wait,否则进程成为僵尸进程(Zombie)。所以,有必要对每个Process对象调用join()方法 (实际上等同于wait)。对于多线程来说,由于只有一个进程,所以不存在此必要性。

        multiprocessing提供了threading包中没有的IPC(比如Pipe和Queue),效率上更高。应优先考虑Pipe和Queue,避免使用Lock/Event/Semaphore/Condition等同步方式 (因为它们占据的不是用户进程的资源)。

        多进程应该避免共享资源。在多线程中,我们可以比较容易地共享资源,比如使用全局变量或者传递参数。在多进程情况下,由于每个进程有自己独立的内存空间,以上方法并不合适。此时我们可以通过共享内存和Manager的方法来共享资源。但这样做提高了程序的复杂度,并因为同步的需要而降低了程序的效率。

创建进程的方式一

from multiprocessing import Process
import time

def f(name):
    time.sleep(1)
    print('hello',name,time.ctime())

if __name__ == '__main__':
    list = []
    for i in range(3):
        p = Process(target=f,args=('NFTS',))
        list.append(p)
        p.start()
    for i in list:
        p.join()
    print('运行结束')

        Process.PID中保存有PID,如果进程还没有start(),则PID为None。
        Windows系统下,需要注意的是要想启动一个子进程,必须加上那句if __name__ == "main",进程相关的要写在这句下面,不加会报错。

创建进程的方式二:类式调用

from multiprocessing import Process
import time

class myprocess(Process):
    def __init__(self):
        super(myprocess,self).__init__()

    def run(self):
        time.sleep(1)
        print('hello',self.name,time.ctime())

if __name__ == '__main__':
    list = []
    for i in range(3):
       p = myprocess()
       p.start()
       list.append(p)

    for i in list:
        i.join()

    print('运行结束')

查看进程的ID号

from multiprocessing import Process
import os
import time
def info(title):
    print(title)
    print('module name:', __name__)   # __name__ 就等于__main__
    print('parent process:', os.getppid())
    print('process id:', os.getpid())


def f(name):
    info('\033[31;1mfunction f\033[0m')
    print('hello', name)

if __name__ == '__main__':
    info('\033[32;1mmain process line\033[0m')
    time.sleep(100)
    p = Process(target=info, args=('bob',))
    p.start()
    p.join()

进程间通信(用Queue来实现)

from multiprocessing import Process,Queue

def f(q,n):
    q.put([n,'ppap'])

if __name__ == '__main__':
    q = Queue()
    list = []
    for i in range(3):
        p = Process(target=f,args=(q,i))
        list.append(p)
        p.start()
    print(q.get())
    print(q.get())
    print(q.get())
    for i in list:
        i.join()

Pipe管道通信

from multiprocessing import Process,Pipe
def f(conn):
    conn.send(123)
    conn.close()

if __name__ == '__main__':
    prant_conn,son_conn = Pipe()
    list = []
    p = Process(target=f,args=(son_conn,))
    p.start()
    print(prant_conn.recv())
    p.join()

Manger

from multiprocessing import Process, Manager

def f(d, l,n):
    d[n] = '1'
    d['2'] = 2
    d[0.25] = None
    l.append(n)
    print(l)

if __name__ == '__main__':
    with Manager() as manager:
        d = manager.dict()

        l = manager.list(range(5))
        p_list = []
        for i in range(10):
            p = Process(target=f, args=(d, l,i))
            p.start()
            p_list.append(p)
        for res in p_list:
            res.join()

        print(d)
        print(l)

四、协程

        协程又称为微线程、纤程,英文名Coroutine。它是一种用户态的轻量级线程,本质是一个单线程,需要和进程配合才能在CPU上运行。

特点:

1. 必须在只有一个单线程里实现并发
2. 修改共享数据不需加锁
3. 用户程序里自己保存多个控制流的上下文栈
4. 一个协程遇到IO操作自动切换到其它协程

         可以用yield和gevent来实现,gevent是一个三方库。

import gevent

def a():
    print('程序aaaaaaa开始-----------')
    gevent.sleep(1)
    print('程序aaaaaaaaaa结束')

def b():
    print('程序bbbbbbbbbbbb开始-----')
    gevent.sleep(2)
    print('程序bbbbbbbbbb结束')

gevent.joinall([
    gevent.spawn(a),
    gevent.spawn(b)
])

        下面例子中,用greenlet来实现,它是一个用C实现的协程模块。

from greenlet import greenlet
def test1():
    print(12)
    gr2.switch()
    print(34)
    gr2.switch()
def test2():
    print(56)
    gr1.switch()
    print(78)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()

        下面是一个综合比较案例

from gevent import monkey
import gevent
from urllib.request import urlopen
import time

monkey.patch_all()

def f(url):
    print('网址:%s'%url)
    resp = urlopen(url)
    data = resp.read()
    with open('校花网.html','wb') as f:
        f.write(data)
    print('结束')

start = time.time()

# 普通执行,花费:0.4717395305633545
# l = ['https://www.tupianzj.com/meinv/mm/jurumeinv/','https://mc.163.com/',
#      'http://www.gaosan.com/gaokao/76265.html']
# for url in l:
#     f(url)

# 协程执行,花费:0.2602534294128418
gevent.joinall([
    gevent.spawn(f,'https://www.tupianzj.com/meinv/mm/jurumeinv/'),
    gevent.spawn(f,'https://mc.163.com/'),
    gevent.spawn(f,'http://www.gaosan.com/gaokao/76265.html')
])

end = time.time()
print('花费了:%s'%(end-start))

        gevent库中的monkey.patch,它是一个监听I/O阻塞的补丁,它可以提高程序的执行效率,当遇到I/O阻塞,会及时切换执行。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值