python之多线程和多进程

元气满满的学习,我是一致勤奋的小黄鸭,冲鸭

一、基础概念

1.1线程

什么是线程?

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个 单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一个线程是一个execution context(执行上下文),即一个cpu执行时所需要的一串指令。

理解线程的工作方式

假设你正在读一本书,没有读完,你想休息一下,但是你想在回来时恢复到当时读的具体进度。有一个方法就是记下页数、行数与字数这三个数值,这些数值 就是execution context。如果你的室友在你休息的时候,使用相同的方法读这本书。你和她只需要这三个数字记下来就可以在交替的时间共同阅读这本书了。

线程的工作方式与此类似。CPU会给你一个在同一时间能够做多个运算的幻觉,实际上它在每个运算上只花了极少的时间,本质上CPU同一时刻只干了一 件事。它能这样做就是因为它有每个运算的execution context。就像你能够和你朋友共享同一本书一样,多任务也能共享同一块CPU。

1.2进程

什么是进程?

进程是指一个程序在给定数据集合上的一次执行过程,是系统进行资源分配和运行调用的独立单位。可以简单地理解为操作系统中正在执行的程序。也就说,每个应用程序都有一个自己的进程。每一个进程启动时都会最先产生一个线程,即主线程。然后主线程会再创建其他的子线程。

1.3进程和线程的区别

  • 线程必须在某个进程中执行。
  • 一个进程可包含多个线程,其中有且只有一个主线程。
  • 多线程共享同个地址空间、打开的文件以及其他资源。
  • 多进程共享物理内存、磁盘、打印机以及其他资源。
  • 同一个进程中的线程共享同一内存空间,但是进程之间是独立的。
  • .同一个进程中的所有线程的数据是共享的(进程通讯),进程之间的数据是独立的。
  • 对主线程的修改可能会影响其他线程的行为,但是父进程的修改(除了删除以外)不会影响其他子进程
  • 进程是操作系统进行资源分配的最小单元,资源包括CPU、内存、磁盘等IO设备等等,而线程是CPU调度的基本单位

二、多线程

2.1相关知识

1、单线程:单线程是一个人干一件事,也是主线程,从上到下有顺序的去干,python解释器就是个单线程(主线程),所以当事情多啦,一个人也办法,就等着

2、多线程:有2个线程以及以上的叫多线程,分为主线程和子线程 (主线程和子线程是相对的,正在干活的是主线程),有一大堆的事情,很多人一起干,当主线程空档期,子线程抢占活,但是始终只有一个人在干活(主线程)。 (主线程累啦,子线程抢占活,这是子线程就变成主线程。

多线程特点:共同干一件事情,抢占资源,利用了主线程的空档期,这个Cpython的特性,就是Cpython慢的原因,说白了伪并发,全局解释器) 应用:一般是用于IO的读写操作,详细的看博客:http://www.cnblogs.com/Eva-J/p/5109737.html

python的多线程的应用于 数据库操作,socket等

Python提供两个模块进行多线程的操作,分别是threadthreading
前者是比较低级的模块,用于更底层的操作,一般应用级别的开发不常用。

2.2 创建多线程的2种方法

方法1:直接使用threading.Thread()

import threading

# 这个函数名可随便定义
def run(n):
    print("current task:", n)

if __name__ == "__main__":
    t1 = threading.Thread(target=run, args=("thread 1",))
    t2 = threading.Thread(target=run, args=("thread 2",))
    t1.start()
    t2.start()

方法2:继承threading.Thread,重新定义一个类,重写run方法(其本质是重构Thread类中的run方法)

import threading

class MyThread(threading.Thread):
    def __init__(self, n):
        super(MyThread, self).__init__()  # 重构run函数必须要写
        self.n = n

    def run(self):
        print("current task:", n)

if __name__ == "__main__":
    t1 = MyThread("thread 1")
    t2 = MyThread("thread 2")

    t1.start()
    t2.start()

2.3多线程的执行顺序

程序1:

import time
import threading
def show(a):
    time.sleep(1)
    print(a,threading.current_thread().name)

for i in range(4):
    t=threading.Thread(target=show,args=(i,),name='threading_%s'%i)
    #定义一个线程  给线程加一个名字 name
    t.start()  #线程开始执行

print('hello,Threading')

执行结果
hello,Threading
3 threading_3
1 threading_1
0 threading_0
2 threading_2

Process finished with exit code 0

结论:主线程会直接执行,不会等待子程序执行完毕再去执行,但是主线程执行完毕后,还是等待子线程结束完毕之后才退出,有个等待的过程。(setdasemon 默认是(false),只要将setDaemon 设置成(true),就可以不用等待子线成啦)

程序2:守护线程(主线程执行完毕之后,不管子线程是否执行完毕都随着主线程一起结束。)

import time
import threading
def show(a):
    time.sleep(1)
    print(a,threading.current_thread().name)

for i in range(4):
    t=threading.Thread(target=show,args=(i,),name='threading_%s'%i)
    #定义一个线程  给线程加一个名字 name
    t.setDaemon(True)  #对比上一个程序,加个这句话,主线程执行完毕后不再等待子线程
    t.start()  #线程开始执行

print('hello,Threading')

输出:
hello,Threading

Process finished with exit code 0

程序3:通过join控制线程的执行顺序

join:等待上个线程完毕之后再执行主线程,学名就是阻塞,这样就可以手动控制这个线程直接的执行顺序

程序先执行的子线程,再执行的主线程,都是join()的功劳

import time
import threading
def show(a):
    time.sleep(1)
    print(a,threading.current_thread().name)

for i in range(4):
    t=threading.Thread(target=show,args=(i,),name='threading_%s'%i)
    #定义一个线程  给线程加一个名字 name
    t.start()  #线程开始执行
    t.join()  #堵塞的作用

print('hello,Threading')

代码:
0 threading_0
1 threading_1
2 threading_2
3 threading_3
hello,Threading

Process finished with exit code 0

2.4 线程中的锁

由于线程之间是进行随机调度,并且每个线程可能只执行n条执行之后,当多个线程同时修改同一条数据时可能会出现脏数据,所以,出现了线程锁,即同一 时刻允许一个线程执行操作。线程锁用于锁定资源,你可以定义多个锁, 当你需要独占某一资源时,任何一个锁都可以锁这个资源,就好比你用不同的锁都可以把相同的一个门锁住是一个道理。

由于线程之间是进行随机调度,如果有多个线程同时操作一个对象,如果没有很好地保护该对象,会造成程序结果的不可预期,我们也称此为“线程不安全”。

threading模块中定义了Lock 类,提供了互斥锁的功能来保证多线程情况下数据的正确性。

用法的基本步骤:

#创建锁

mutex=threading.Lock()

#锁定   (有一个超时时间的可选参数timeout。如果设定了timeout,则在超时后通过返回值可以判断是否得到了锁,从而可以进行一些其他的处理)

mutex.acquire([timeout])

#释放

mutex.release()

互斥锁

gl_num = 0
lock = threading.Lock()
def Func():
    lock.acquire()      #给线程上锁
    global gl_num
    gl_num +=1
    time.sleep(1)
    print (gl_num,threading.current_thread().name)
    lock.release()   ##给线程解锁,如果不解锁,就会将线程卡注,程序就会卡在哪里
for i in range(5):
    t = threading.Thread(target=Func,name='thread_%s'%i)
    t.start()

执行结果:
1 thread_0
2 thread_1
3 thread_2
4 thread_3
5 thread_4

Process finished with exit code 0

死锁

死锁是指一个资源被多次调用,而多次调用方都未能释放该资源就会造成一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。

import threading

n1, n2 = 0, 0
lock = threading.Lock()

def show():
    global n1, n2
    lock.acquire()  # 加锁
    n1 += 1
    print(threading.current_thread().name + ' set n1 to ' + str(n1))
    lock.acquire()  # 再次加锁
    n2 += n1
    print(threading.current_thread().name + ' set n2 to ' + str(n2))
    lock.release()
    lock.release()

if __name__ == '__main__':
    thread_list = []
    for i in range(5):
        t = threading.Thread(target=show)
        t.start()
        thread_list.append(t)
    for t in thread_list:
        t.join()
    print('final num:%d ,%d' % (n1, n2))

结果:
Thread-1 set n1 to 1
#会一直等待

递归锁

rlock=threading.RLock()

为了满足在同一线程中多次请求同一资源的需求,Python 提供了可重入锁(RLock)。
Rlock内部维护着一个Lock和一个counter变量,counter 记录了 acquire 的次数,从而使得资源可以被多次 require。直到一个线程所有的 acquire 都被 release,其他的线程才能获得资源。

import threading

n1, n2 = 0, 0
lock = threading.RLock()
def show():
    global n1, n2
    lock.acquire()  # 加锁
    n1 += 1
    print(threading.current_thread().name + ' set n1 to ' + str(n1))
    lock.acquire()  # 再次加锁
    n2 += n1
    print(threading.current_thread().name + ' set n2 to ' + str(n2))
    lock.release()
    lock.release()

if __name__ == '__main__':
    thread_list = []
    for i in range(5):
        t = threading.Thread(target=show)
        t.start()
        thread_list.append(t)
    for t in thread_list:
        t.join()
    print('final num:%d ,%d' % (n1, n2))

执行输出:
Thread-1 set n1 to 1
Thread-1 set n2 to 1
Thread-2 set n1 to 2
Thread-2 set n2 to 3
Thread-3 set n1 to 3
Thread-3 set n2 to 6
Thread-4 set n1 to 4
Thread-4 set n2 to 10
Thread-5 set n1 to 5
Thread-5 set n2 to 15
final num:5 ,15

Process finished with exit code 0

2.5 GIL

在非python环境中,单核情况下,同时只能有一个任务执行。多核时可以支持多个线程同时执行。但是在python中,无论有多少核,同时只能执行一个线程。究其原因,这就是由于GIL的存在导致的。

GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是 “通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。GIL只在cpython中才有,因为 cpython调用的是c语言的原生线程,所以他不能直接操作cpu,只能利用GIL保证同一时间只能有一个线程拿到数据。而在pypy和jpython 中是没有GIL的。

Python多线程的工作过程:
python在使用多线程的时候,调用的是c语言的原生线程。

  1. 拿到公共数据
  2. 申请gil
  3. python解释器调用os原生线程
  4. os操作cpu执行运算
  5. 当该线程执行时间到后,无论运算是否已经执行完,gil都被要求释放
  6. 进而由其他进程重复上面的过程
  7. 等其他线程执行完后,又会切换到之前的线程(从他记录的上下文继续执行)
    整个过程是每个线程执行自己的运算,当执行时间到就进行切换(context switch)

2.6 事件(Event类)

python线程的事件用于主线程控制其他线程的执行,事件是一个简单的线程同步对象,其主要提供以下几个方法:

方法注释
clear将flag设置为“False”
set将flag设置为“True”
is_set判断是否设置了flag
wait会一直监听flag,如果没有检测到flag就一直处于阻塞状态

事件处理的机制:全局定义了一个“Flag”,当flag值为“False”,那么event.wait()就会阻塞,当flag值为“True”,那么event.wait()便不再阻塞。

#利用Event类模拟红绿灯
import threading
import time

event = threading.Event()
def lighter():
    count = 0
    event.set()     #初始值为绿灯
    while True:
        if 5 < count <=10 :
            event.clear()  # 红灯,清除标志位
            print("\33[41;1mred light is on...\033[0m")
        elif count > 10:
            event.set()  # 绿灯,设置标志位
            count = 0
        else:
            print("\33[42;1mgreen light is on...\033[0m")

        time.sleep(1)
        count += 1

def car(name):
    while True:
        if event.is_set():      #判断是否设置了标志位
            print("[%s] running..."%name)
            time.sleep(1)
        else:
            print("[%s] sees red light,waiting..."%name)
            event.wait()   #会一直监听flag,如果没有检测到flag就一直处于阻塞状态
            print("[%s] green light is on,start going..."%name)

light = threading.Thread(target=lighter,)
light.start()

car = threading.Thread(target=car,args=("MINI",))
car.start()

三、多进程

3.1创建多进程

方法1:os.fork(只在linux系统中有用,在windows中无用)

fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。

子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。

import os

print('Process (%s) start...' % os.getpid())
# Only works on Unix/Linux/Mac:
pid = os.fork()
if pid == 0:
    print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
    print('I (%s) just created a child process (%s).' % (os.getpid(), pid))

执行结果:
Process (876) start...
I am child process (877) and my parent is 876.
I (876) just created a child process (877).

方法2:使用multiprocessing模块: 创建Process的实例,传入任务执行函数作为参数

from multiprocessing import Process
import os

# 子进程要执行的代码
def run_proc(name):
    print('Run child process %s (%s)...' % (name, os.getpid()))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    #创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例
    p = Process(target=run_proc, args=('test',))
    print('Child process will start.')
    #用start()方法启动,
    p.start()
    #join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
    p.join()
    print('Child process end.')

执行结果:
Parent process 928.
Process will start.
Run child process test (929)...
Process end.

方法3:Pool 如果要启动大量的子进程,可以用进程池的方式批量创建子进程:

from multiprocessing import Pool
import os, time, random

def long_time_task(name):
    print('Run task %s (%s)...' % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()
    print('Task %s runs %0.2f seconds.' % (name, (end - start)))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    p = Pool(4)
    for i in range(5):
        p.apply_async(long_time_task, args=(i,))
    print('Waiting for all subprocesses done...')
    p.close()
    p.join()
    print('All subprocesses done.')

执行结果:
Parent process 11868.
Waiting for all subprocesses done...
Run task 0 (13760)...
Run task 1 (13536)...
Run task 2 (14956)...
Run task 3 (11124)...
Task 1 runs 0.55 seconds.
Run task 4 (13536)...
Task 0 runs 1.11 seconds.
Task 2 runs 2.10 seconds.
Task 3 runs 2.90 seconds.
Task 4 runs 2.54 seconds.
All subprocesses done.

Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。

注意:Pool(3):表示进程池中最多有3个进程一起执行。如果添加的线程数量超过了上面的3,不会导致添加失败,而是得等待某个线程完成后再执行。

Pool默认的大小是CPU的核数

Pool.close():关闭进程池,相当于不能再次添加新的任务了

Pool.join():主进程等子进程完成之后再执行

方法4:使用multiprocessing模块: 派生Process的子类,重写run方法

import os,time
from multiprocessing import Process
class MyProcess(Process):
    def __init__(self):
        Process.__init__(self)
    def run(self):
        print("子进程开始>>> pid=%s,ppid=%s"%(os.getpid(),os.getppid()))
        time.sleep(2)
        print("子进程终止>>> pid=%s"%os.getpid())
if __name__=='__main__':
    print("主进程开始>>> pid=%s"%os.getpid())
    p=MyProcess()
    p.start()
    p.join()
    print("主进程终止")

执行结果:
主进程开始>>> pid=7128
子进程开始>>> pid=15420,ppid=7128
子进程终止>>> pid=15420
主进程终止

3.2进程间通信

进程之间不共享数据的。如果进程之间需要进行通信,则要用到Queue模块或者Pipi模块等来实现。

其中Queue主要用来在多个进程之间实现通信。Pipe常用来在两个进程之间实现通信。

Queue是多进程安全队列,Queue通过put和get方法来实现多进程之间的数据传递。

put方法用于将数据插入到队列,有两个可选参数,一个是blocked,一个时timeout,

如果blocked为True,那么这个方法就会阻塞timeout指定的时间,直到队列有剩余的

空间。如果超时,则抛出Queue.Full异常。

get方法用于将数据从队列中取出,也有两个可选参数,blocked和timeout,如果blocked

为True,那么这个方法会阻塞timeout指定的时间,直到队列中有数据。如果超时,则抛出

Queue.Empty异常。

import os,time,random
from multiprocessing import Process,Queue
def write_proc(q,urls):
    print("Process %s Running..."%(os.getpid()))
    for url in urls:
        q.put(url)
        print("put %s in queue..."%url)
        time.sleep(random.random()*3)
def read_proc(q):
    print("Process %s Running..."%(os.getpid()))
    while True:
        url=q.get(True)
        print("get %s from queue..."%url)
if __name__=='__main__':
    print("Main Process %s Running..."%(os.getpid()))
    q=Queue()
    writer_proc1=Process(target=write_proc,args=(q,[1,2,3,4]))
    writer_proc2=Process(target=write_proc,args=(q,[5,6,7,8]))
    reader_proc=Process(target=read_proc,args=(q,))
    writer_proc1.start()
    writer_proc2.start()
    reader_proc.start()
    writer_proc1.join()
    writer_proc2.join()
    #在这里由于reader_proc执行函数时一个死循环,所以只能通过手动终结进程
    reader_proc.terminate()
    print("Main Process end")

执行结果:
Main Process 16884 Running...
Process 7208 Running...
put 1 in queue...
Process 14680 Running...
get 1 from queue...
Process 15456 Running...
put 5 in queue...
get 5 from queue...
put 6 in queue...
get 6 from queue...
put 7 in queue...
get 7 from queue...
put 2 in queue...
get 2 from queue...
put 3 in queue...
get 3 from queue...
put 8 in queue...
get 8 from queue...
put 4 in queue...
get 4 from queue...
Main Process end

Pipe常用来在两个进程之间进行通信。该方法返回一个二元元组 (conn1,conn2),代表一个管道的两端。Pipe方法有个duplex参数。默认为True,当其为True时,表示该管道处于全双工模式下。conn1和conn2都可以进行收发。当期为False时,表示该管道处于半双工模式下,conn只能进行接收消息,conn2只能发送消息。

send和recv方法分别是发送和接收消息的方法。
 

import os,time,random
from multiprocessing import Process,Pipe
def proc_send(p,urls):
    for url in urls:
        print("Process %s send %s..."%(os.getpid(),url))
        p.send(url)
        time.sleep(random.random()*4)
def proc_recv(p):
    while True:
        url=p.recv()
        print("Process %s recv %s..."%(os.getpid(),url))
if __name__=='__main__':
    print("Main Process %s Running..."%(os.getpid()))
    pipe=Pipe()
    p1=Process(target=proc_send,args=(pipe[0],[1,2,3,4]))
    p2=Process(target=proc_recv,args=(pipe[1],))
    p1.start()
    p2.start()
    p1.join()
    p2.terminate()
    print("Main Process End....")

执行结果:
Main Process 1332 Running...
Process 4832 send 1...
Process 14520 recv 1...
Process 4832 send 2...
Process 14520 recv 2...
Process 4832 send 3...
Process 14520 recv 3...
Process 4832 send 4...
Process 14520 recv 4...
Main Process End....

为了让p2能够一直接收从管道传过来的数据,在接收数据时,将过程放在一个循环中,监听pipe,所以

最后p2的终结只能通过terminate来强制终结,如果使用join方法,那么主进程会一直阻塞。

四、选择多进程还是多线程

在这个问题上,首先要看下你的程序是属于哪种类型的。一般分为两种 CPU 密集型 和 I/O 密集型。

  • CPU 密集型:程序比较偏重于计算,需要经常使用 CPU 来运算。例如科学计算的程序,机器学习的程序等。

  • I/O 密集型:顾名思义就是程序需要频繁进行输入输出操作。爬虫程序就是典型的 I/O 密集型程序。

如果程序是属于 CPU 密集型,建议使用多进程。而多线程就更适合应用于 I/O 密集型程序。


 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值