并发编程-进程和线程

进程

假如有两个程序A和B,程序A在执行到一半的过程中,需要读取大量的数据输入(I/O操作),
    而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源。
    是不是在程序A读取数据的过程中,让程序B去执行,当程序A读取完数据之后,让
    程序B暂停,然后让程序A继续执行?
    当然没问题,但这里有一个关键词:切换
    既然是切换,那么这就涉及到了状态的保存,状态的恢复,加上程序A与程序B所需要的系统资
    源(内存,硬盘,键盘等等)是不一样的。自然而然的就需要有一个东西去记录程序A和程序B
    分别需要什么资源,怎样去识别程序A和程序B等等,所以就有了一个叫进程的抽象

    进程定义:

    进程就是一个程序在一个数据集上的一次动态执行过程。
    进程一般由程序、数据集、进程控制块三部分组成。
    我们编写的程序用来描述进程要完成哪些功能以及如何完成;
    数据集则是程序在执行过程中所需要使用的资源;
    进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系
    统感知进程存在的唯一标志。

    举一例说明进程:
             想象一位有一手好厨艺的计算机科学家正在为他的女儿烘制生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需
             的原料:面粉、鸡蛋、糖、香草汁等。在这个比喻中,做蛋糕的食谱就是程序(即用适当形式描述的算法)计算机科学家就是处理器(cpu),
而做蛋糕的各种原料就是输入数据。进程就是厨师阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和。 现在假设计算机科学家的儿子哭着跑了进来,说他的头被一只蜜蜂蛰了。计算机科学家就记录下他 照着食谱做到哪儿了(保存进程的当前状态),然后拿出一本急救手册,按照其中的指示处理蛰伤。这 里,我们看到处理机从一个进程(做蛋糕)切换到另一个高优先级的进程(实施医疗救治),每个进程 拥有各自的程序(食谱和急救手册)。当蜜蜂蛰伤处理完之后,这位计算机科学家又回来做蛋糕,从他 离开时的那一步继续做下去。

线程

线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,
使到进程内并发成为可能。

假设,一个文本程序,需要接受键盘输入,将内容显示在屏幕上,还需要保存信息到硬盘中。若只有
一个进程,势必造成同一时间只能干一样事的尴尬(当保存时,就不能通过键盘输入内容)。若有多
个进程,每个进程负责一个任务,进程A负责接收键盘输入的任务,进程B负责将内容显示在屏幕上的
任务,进程C负责保存内容到硬盘中的任务。这里进程A,B,C间的协作涉及到了进程通信问题,而且
有共同都需要拥有的东西-------文本内容,不停的切换造成性能上的损失。若有一种机制,可以使
任务A,B,C共享资源,这样上下文切换所需要保存和恢复的内容就少了,同时又可以减少通信所带
来的性能损耗,那就好了。是的,这种机制就是线程。

线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序
计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发
性能。线程没有自己的系统资源。

线程进程的关系区别


1 一个程序至少有一个进程,一个进程至少有一个线程.(进程可以理解成线程的容器) 2 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。 3 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和 程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。 4 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调 度的一个独立单位. 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程
自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)但是
它可与同属一个进程的其他的线程共享进程所拥有的全部资源. 一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.
5 进程是最小的资源单位,线程是最小的执行单位。

<一 python的线程与threading模块>

一 线程的两种调用方式

      threading 模块建立在thread 模块之上。thread模块以低级、原始的方式来处理和控制线程,而threading 模块通过对thread进行二次封装,

提供了更方便的api来处理线程。

直接调用:

import threading
import time
 
def sayhi(num): #定义每个线程要运行的函数
 
    print("running on number:%s" %num)
 
    time.sleep(3)
 
if __name__ == '__main__':
 
    t1 = threading.Thread(target=sayhi,args=(1,)) #生成一个线程实例
    t2 = threading.Thread(target=sayhi,args=(2,)) #生成另一个线程实例
 
    t1.start() #启动线程
    t2.start() #启动另一个线程
 
    print(t1.getName()) #获取线程名
    print(t2.getName())

继承式调用:

import threading
import time


class MyThread(threading.Thread):
    def __init__(self,num):
        threading.Thread.__init__(self)
        self.num = num

    def run(self):#定义每个线程要运行的函数

        print("running on number:%s" %self.num)

        time.sleep(3)

if __name__ == '__main__':

    t1 = MyThread(1)
    t2 = MyThread(2)
    t1.start()
    t2.start()
    
    print("ending......")

二 threading.thread的实例方法

join&Daemon方法:

join():在子线程完成运行之前,这个子线程的父线程将一直被阻塞。

setDaemon(True):

         将线程声明为守护线程,必须在start() 方法调用之前设置, 如果不设置为守护线程程序会被无限挂起。这个方法基本和join是相反的。

         当我们 在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行,那么当主线程完成

         想退出时,会检验子线程是否完成。如 果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是 只要主线程

         完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon方法啦

# def music():
#     print('music start')
#     time.sleep(3)
#     print('stop music')
#
# def game(num):
#     print('start to play game',num)
#     time.sleep(5)
#     print('stop game')
#
# t1 = threading.Thread(target=music)
# t1.start()
#
# t1.join()  ## 若放在这,t1 未执行完,t2也不能运行
#
# t2 = threading.Thread(target=game,args=(3,)) #得到一个对象
# t2.start()     ## 开始
#
# # t1.join()  ##t1没运行完,谁都不能运行
# #
# # t2.join()  ## ##t1没运行完,谁都不能运行
#
# print('主线程')
join
def music():
    print('music start')
    time.sleep(3)
    print('stop music')

def game(num):
    print('start to play game',num)
    time.sleep(5)
    print('stop game')

t1 = threading.Thread(target=music)

t1.start()

t2 = threading.Thread(target=game,args=(3,)) #得到一个对象
t2.setDaemon(True)   ##要是主线程结束了,不管t2结束没结束,都结束
t2.start()     ## 开始
setDaemon

其它方法

# run():  线程被cpu调度后自动执行线程对象的run方法
# start():启动线程活动。
# isAlive(): 返回线程是否活动的。
# getName(): 返回线程名。
# setName(): 设置线程名。

threading模块提供的一些方法:
# threading.currentThread(): 返回当前的线程变量。
# threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
# threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

python的GIL(全局解释锁)

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

上面的核心意思就是,无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行。

 

注意: 不用的线程,只能通过不断的切换来运行。

 

1 并发:是指系统具有处理多个任务(动作)的能力。(切换)
2 并行:是指系统具有同时处理多个任务(动作)的能力。
3 并行是并发的子集
4 
5 同步:程序若是遇到IO阻塞(如recv,input等),会一直等待这个数据,不会往下执行。
6 异步:程序若是遇到IO阻塞(如recv,input等),不会等待,而是继续往下执行,等会再切换回来。
7 
8 任务分为IO阻塞型和计算密集型:
9 python中的,对于IO密集型任务,python的多线程就是有意义的,可以在遇到IO阻塞时,执行另外一个线程,不让CPU空闲;但是对于计算密集型,python的多线程就没有意义了。

 三 同步锁(Lock)

 1 ## 做一个100依次减一,作成多线程方式
 2 
 3 import time
 4 import threading
 5 # num = 100
 6 # def sub():
 7 #     global num
 8 #     temp = num
 9 #     time.sleep(0.01)   ## 因为这里每一个线程都要停0.01秒,时间太长,大于一个时间轮循,则一个线程还没执行完就被时间
10 #     num = temp-1     ###  轮循到下一个线程中了,故实际上第一个线程拿到num 后 还没进行减一的操作呢;
11 #                             ###  直到等了0.01秒,第一个线程执行完后 就可再一次申请gil,让CPU回来再调用他
12 # l =[]
13 # for i in range(100):
14 #     t = threading.Thread(target=sub)
15 #     t.start()
16 #     l.append(t)
17 #
18 # for t in l:
19 #     t.join()
20 #
21 # print(num)

多个线程都在同时操作同一个共享资源,所以造成了资源破坏,怎么办呢?(join会造成串行,失去所线程的意义)

我们可以通过同步锁来解决这种问题:

###加上锁就可以解决

num = 100
def sub():
    global num
    lock.acquire()
    temp = num
    time.sleep(0.01)   ## 因为这里每一个线程都要停0.01秒,时间太长,大于一个时间轮循,则一个线程还没执行完就被时间
    num = temp-1     ###  轮循到下一个线程中了,故实际上第一个线程拿到num 后 还没进行减一的操作呢;
    lock.release()                       ###  直到等了0.01秒,第一个线程执行完后 就可再一次申请gil,让CPU回来再调用他
l =[]
lock = threading.Lock()
for i in range(100):
    t = threading.Thread(target=sub)
    t.start()
    l.append(t)

for t in l:
    t.join()

print(num)

四 线程死锁和递归锁 :

## 利用继承的方式写几个线程

 ###  -------------------死锁-----------------
import threading
import time

class Mythead(threading.Thread):
    def actionA(self):
        A.acquire()
        print(self.name,'gotA',time.asctime())
        time.sleep(2)

        B.acquire()
        print(self.name,'gotB',time.asctime())
        time.sleep(1)
        B.release()
        A.release()


    def actionB(self):
        B.acquire()
        print(self.name, 'gotB', time.asctime())
        time.sleep(2)

        A.acquire()
        print(self.name, 'gotA', time.asctime())
        time.sleep(1)
        A.release()
        B.release()

    def run(self):
        self.actionA()   ## 在第一个线程开始执行actionA时时,先拿到A锁,在拿到B锁,然后依次解锁,
        self.actionB()     ## 此过程,其余四个线程都在等待,无法获取A锁,只有线程一把A锁,释放之后才能竞争
                         ## 第一个线程执行完actionA,还要继续执行actionB,需要获取B锁,然后获取A锁,同时
                      ## 线程2竞争到A锁,开始执行actionA,需要A锁,和B锁,此时线程1和线程2就会冲突,互相不给锁
                      ## 造成死锁
                  ## 解决方法 ,把A,B两个锁换成一个可以递归的锁,r_lock = theading.RLock()(内部使用计数的方式实现)

l = []
A = threading.Lock()
B = threading.Lock()
for i in range(5):
    t = Mythead()
    t.start()
    l.append(t)

for i in l:
    i.join()

print('enging...')

同步条件(Event)

import threading,time
class Boss(threading.Thread):
    def run(self):
        print("BOSS:今晚大家都要加班到22:00。")
        print(event.isSet())
        event.set()
        time.sleep(5)
        print("BOSS:<22:00>可以下班了。")
        print(event.isSet())
        event.set()
class Worker(threading.Thread):
    def run(self):
        event.wait()
        print("Worker:哎……命苦啊!")
        time.sleep(1)
        event.clear()
        event.wait()
        print("Worker:OhYeah!")
if __name__=="__main__":
    event=threading.Event()
    threads=[]
    for i in range(5):
        threads.append(Worker())
    threads.append(Boss())
    for t in threads:
        t.start()
    for t in threads:
        t.join()

注:event = threading.Event()   ## 创建一个Event对象

event.wait()    ## 等待一个flag

event.set()     ## 设置一个flag

event.clear()     ## 取消一个flag

信号量(Semaphore)

  信号量用来控制线程并发数的,BoundedSemaphore或Semaphore管理一个内置的计数 器,每当调用acquire()时-1,调用release()时+1。

      计数器不能小于0,当计数器为 0时,acquire()将阻塞线程至同步锁定状态,直到其他线程调用release()。(类似于停车位的概念)

      BoundedSemaphore与Semaphore的唯一区别在于前者将在调用release()时检查计数 器的值是否超过了计数器的初始值,如果超过了将抛出一个异常。

## 可以指定同时有几个线程去跑
import threading,time
class MyThead(threading.Thread):
    def run(self):
        if semaphore.acquire():   ### semaphore
            print(self.name)
            time.sleep(2)
            semaphore.release()


if __name__ == '__main__':
    semaphore = threading.Semaphore(5)  ## 默认为1  semaphore也是一把锁
    thrs = []
    for i in range(50):
        t = MyThead()
        thrs.append(t)
        t.start()
        
    for i in thrs:
        i.join()

 多线程利器---队列(queue)

创建一个“队列”对象
import queue   ##python2中是Queue
q = queue.Queue(maxsize = 10)
queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。可通过Queue的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。

将一个值放入队列中
q.put(10)
调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;第二个block为可选参数,默认为
1。如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空出一个数据单元。如果block为0,put方法将引发Full异常。

将一个值从队列中取出
q.get()
调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。如果队列为空且block为True,
get()就使调用线程暂停,直至有项目可用。如果队列为空且block为False,队列将引发Empty异常。

Python Queue模块有三种队列及构造函数:
1、Python Queue模块的FIFO队列先进先出。   class queue.Queue(maxsize)
2、LIFO类似于堆,即先进后出。               class queue.LifoQueue(maxsize)
3、还有一种是优先级队列级别越低越先出来。        class queue.PriorityQueue(maxsize)

此包中的常用方法(q = Queue.Queue()):
q.qsize() 返回队列的大小
q.empty() 如果队列为空,返回True,反之False
q.full() 如果队列满了,返回True,反之False
q.full 与 maxsize 大小对应
q.get([block[, timeout]]) 获取队列,timeout等待时间
q.get_nowait() 相当q.get(False)
非阻塞 q.put(item) 写入队列,timeout等待时间
q.put_nowait(item) 相当q.put(item, False)
q.task_done() 在完成一项工作之后,q.task_done() 函数向任务已经完成的队列发送一个信号
q.join() 实际上意味着等到队列为空,再执行别的操作
import queue

q = queue.Queue(4)   #先进先出模式,可加参数,表示q可以放几个值
q.put(12)
q.put('sfsad')
q.put({'name':'alex'})
print(q.qsize())
print(q.empty())
print(q.full())

while True:
    date = q.get() #可加参数False,表示若没有数据可拿时,就报错,没有参数,一直卡着
    print(date)
import queue

#先进后出

q=queue.LifoQueue()

q.put(34)
q.put(56)
q.put(12)

#优先级
# q=queue.PriorityQueue()
# q.put([5,100])
# q.put([7,200])
# q.put([3,"hello"])
# q.put([4,{"name":"alex"}])

while 1:

  data=q.get()
  print(data)
其他模型

生产者消费者模型:

为什么要使用生产者和消费者模式

在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。

什么是生产者消费者模式

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

这就像,在餐厅,厨师做好菜,不需要直接和客户交流,而是交给前台,而客户去饭菜也不需要不找厨师,直接去前台领取即可,这也是一个结耦的过程。

import queue,threading,time

q = queue.Queue()

def producter(name):
    count = 0
    while count<10:
        print('making....')
        time.sleep(2)
        q.put(count)
        print('producter%s has produced %s baozi'%(name,count))
        q.task_done() ## 告诉对列q,包子做好了,再由对列传递给消费者
        # q.join()
        print('ok')
        count += 1



def consumer(name):
    count = 0
    while count < 10:
        time.sleep(3)
        print('waiting....')
        q.join()
        date = q.get()
        # time.sleep(4)
        # q.task_done()
        print('\033[43;1m consumer %s has eat %s baozi\033[0m'%(name,date))
        count += 1

p1 = threading.Thread(target=producter,args=('A君',))
c1 = threading.Thread(target=consumer,args=('B君',))
c2 = threading.Thread(target=consumer,args=('C君',))
c3 = threading.Thread(target=consumer,args=('D君',))


p1.start()
c1.start()
c2.start()
c3.start()
生产者消费者模型

<二 多进程模块 multiprocessing>

由于GIL的存在,python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。

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

一 进程的调用

调用方式1

from multiprocessing import Process
import time
def f(name):
    time.sleep(1)
    print('hello', name,time.ctime())

if __name__ == '__main__':
    p_list=[]
    for i in range(3):
        p = Process(target=f, args=('alvin',))
        p_list.append(p)
        p.start()
    for i in p_list:
        p.join()
    print('end')

调用方式2

from multiprocessing import Process
import time

class MyProcess(Process):
    def __init__(self):
        super(MyProcess, self).__init__()
        #self.name = name

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


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

    for p in p_list:
        p.join()

    print('end')

 

二 Process类 

构造方法:

Process([group [, target [, name [, args [, kwargs]]]]])

  group: 线程组,目前还没有实现,库引用中提示必须是None; 
  target: 要执行的方法; 
  name: 进程名; 
  args/kwargs: 要传入方法的参数。

实例方法:

  is_alive():返回进程是否在运行。

  join([timeout]):阻塞当前上下文环境的进程程,直到调用此方法的进程终止或到达指定的timeout(可选参数)。

  start():进程准备就绪,等待CPU调度

  run():strat()调用run方法,如果实例进程时未制定传入target,这star执行t默认run()方法。

  terminate():不管任务是否完成,立即停止工作进程

属性:

  daemon:和线程的setDeamon功能一样

  name:进程名字。

  pid:进程号。

三 进程间通讯 

    三种方式:

##第一种,利用对列通信
# import multiprocessing,time,queue
#
# def Foo(q):
#     time.sleep(1)
#     print('son q',id(q))
#     q.put(12)
#     q.put('goulonghui')
#     q.put({'name':'alex'})
#
# if __name__ == '__main__':
#     q = multiprocessing.Queue()
#
#     p = multiprocessing.Process(target=Foo,args=(q,))
#     p.start()
#
#
#     print('main process',id(q))
#     print(q.get())
#     print(q.get())
#     print(q.get())
#
#     print('main process ending...')
利用对列queue
##第二种,利用管道通信
# import multiprocessing,time,queue
#
# def foo(conn):
#     time.sleep(1)
#     conn.send(['goulognhui',123,{'name':'alex'}])
#     respons = conn.recv()
#     print(respons,time.asctime())
#     conn.close()
#     print('id2',id(conn))
#
#
#
# if __name__ == '__main__':
#     parent_conn,child_conn = multiprocessing.Pipe()  ## 得到一个双向管道
#     print('id1',id(child_conn))
#
#     p = multiprocessing.Process(target=foo,args=(child_conn,))
#     p.start()
#
#     print(parent_conn.recv(),time.asctime())
#     parent_conn.send('son,你好')
#     # p.join()
管道 通讯
# 第三种 manager 实现数据,资源共享

from multiprocessing import Process,Manager 


def foo(d,l,n):
    d[n] = '22'   ##在子进程中修改数据,主进程数据也改变
    d['2'] = '33 '
    l.append(n)


if __name__ == '__main__':
    with Manager() as manager:
        d = manager.dict()  ## 利用manager创建一个字典对象
        l = manager.list(range(5))
        thrs = []
        for i in range(10):
            p = Process(target=foo,args=(d,l,i))
            p.start()
            thrs.append(p)
        for i in thrs:
            i.join()

        print(d)
        print(l)
manager

四 进程同步

from multiprocessing import Process,Lock

def f(i,l):
    l.acquire()
    print('helloword%s'%i)  ##此时10个进程共享一个屏幕资源
    l.release()

if __name__ == '__main__':
    lock = Lock()
    for i in range(10):
        p = Process(target=f,args=(i,lock))
        p.start()

五 进程池

进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。

进程池中有两个方法:

  • apply    同步的
  • apply_async  异步的
  • from multiprocessing import Process,Pool
    import time,os
    
    def foo(i):
        time.sleep(1)
        print(i)
        print('son',os.getpid())
        return 'hello %s'%i
    
    def bar(arg):## 回调函数中的参数args就是执行的那个函数(foo)的返回值
        print('bar',os.getpid())   ##要是把这些代码直接放在foo下,就是在子进程下执行
        print(arg)  # 为foo函数的返回值
    
    
    if __name__ == '__main__':
    
        print('main',os.getpid())
        pool = Pool(5)  ## 默认为电脑有几核 就一次处理几个
        for i in range(100):
            # pool.apply(func=foo,args=(i,))   ##  同步的,线程池对其无影响
            # callback是回调函数,就是某个动作或函数执行成功后在执行这个函数
            pool.apply_async(func=foo,args=(i,),callback=bar)  ##这里的回调函数是在主进程中执行的
    
        pool.close()
        pool.join()   ## join和close 的位置是固定的,而且必须要有
    
        print('ending.....')

 

posted @ 2018-04-10 17:55  Mr.GLH  阅读(322)  评论(0)    收藏  举报
Top↑