Day35 of learning python --认识线程

1.为什么要使用线程?

  1)进程在同一时间只能做一件事,如果想要同时做两件事或者更多事,进程就无法实现了。

  2)进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将就无法执行。

  3)由于进程是资源拥有者,创建、撤销与切换存在比较大的时空开销,因而需要引入轻型进程。

  4)由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。

线程和进程的区别:

  1)地址空间和其他资源:进程间相互独立,同一进程的个线程间共享。某进程内的线程在其他进程不可见。

  2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来通信--需要进程同步和互斥手段的辅助,以保证数据的一致性

  3)调度和切换:线程上下文切换比进程上下文切换要快的多。

  4)在多线程操作系统中,进程不是一个可执行的实体。

2.线程的特点

在多线程的操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。

  1)轻型实体

  线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。

TCB包括以下信息:
(1)线程状态。
(2)当线程不运行时,被保存的现场资源。
(3)一组执行堆栈。
(4)存放每个线程的局部变量主存区。
(5)访问同一个进程中的主存和其它资源。
用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。

   2)独立调度和分派的基本单位

  在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。

  3)共享进程资源

  线程在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的进程id,这意味着,线程可以访问该进程的每一个内存资源;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。

  4)可并发执行

  在一个进程中的多个线程执行,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。

3.内存中的线程

  对一台计算机上多个进程,则共享物理内存、磁盘、打印机等其他物理资源。多线程的运行也多进程的运行类似,是cpu在多个线程之间的快速切换。类似于进程,每个线程也有自己的堆栈,不同于进程,线程库无法利用时钟中断强制线程让出CPU,可以调用thread_yield运行线程自动放弃cpu,让另外一个线程运行。同时,不同的进程之间是充满敌意的,彼此是抢占、竞争cpu的关系,如果迅雷会和QQ抢资源。而同一个进程是由一个程序员的程序创建,所以同一进程内的线程是合作关系,一个线程可以访问另外一个线程的内存地址,大家都是共享的。

线程的实现可以分为两类:用户级线程(User-Level Thread)和内核级线程(kernel-Level Thread),

  用户级线程:内核的切换由用户程序自己控制内核切换,不需要内核干涉,少了进出内核态的消耗,但不能很好的利用多核CPU。缺点:资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用。

  内核级线程:切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态;可以很好的利用smp,即利用多核cpu。windows线程就是这样的。

混合实现:

  用户级与内核级的多路复用,内核同一调度内核线程,每个内核线程对应n个用户线程。

4.线程的创建与使用

1)GIL(全局解释器锁)

   Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。Python虚拟机使用一个全局解释锁(Global Interpreter Lock)来互斥线程对Python虚拟机的使用。为了支持多线程机制,一个基本的要求就是需要实现不同线程对共享资源访问的互斥,所以引入了GIL。

GIL:在一个线程拥有了解释器的访问权之后,其他的所有线程都必须等待它释放解释器的访问权,即使这些线程的下一条指令并不会互相影响。在调用任何Python C API之前,要先获得GIL。

GIL缺点:多处理器退化为单处理器;优点:避免大量的加锁解锁操作。

2)GIL影响

  无论你启多少个线程,你有多少个CPU,Python在执行一个进程的时候会淡定的在同一时刻只允许一个线程运行。所以,Python是无法利用多核CPU实现多线程的。这样,Python对于计算密集型的任务开多线程的效率甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。

3)Threading模块

线程的创建:

import time
from threading import Thread
import os
# 多线程并发
def func(n):
    time.sleep(1)  # 子线程
    print(n)

for i in range(10):
    t = Thread(target=func,args=(i,))  # 主线程
    t.start()
import time
import threading
from threading import Thread
import os

class func(Thread):
    def __init__(self,number):
        super().__init__()
        self.number = number

    def run(self):
        time.sleep(1)
        print('子线程%s'%self.number,os.getpid())

for i in range(10):
    t = func(i)
    t.start()

print('主线程',os.getpid())

 多线程与多进程:

import time
from threading import Thread
import os
from multiprocessing import Process

def work():
    print('everybody',os.getpid())

if __name__ == '__main__':
    #在主进程下开启多个线程,每个线程都跟主进程的pid一样
    t1 = Thread(target=work)
    t2 = Thread(target=work)
    t1.start()
    t2.start()
    print('主线程/子线程',os.getpid())

    # 开多个进程,每个进程都有不同的pid
    p1 = Process(target=work)
    p2 = Process(target=work)
    p1.start()
    p2.start()
    print('主进程',os.getpid())

 内存数据共享问题:

import time
from threading import Thread
import os

def func(a,b):
    global g
    g = 0
    print(g,os.getpid())

g = 100
t_lst = []
for i in range(10):
    t = Thread(target=func,args=(i,5))
    t.start()
    t_lst.append(t)

for t in t_lst:
    t.join()
print(g)
# 多线程之间,可以共享和修改数据的

多线程实现SOCKET:多线程能够实现子线程和用户交互(input),而多进程不行

import socket
from threading import Thread
def chat(conn):
    conn.send(b'hello')
    msg = conn.recv(1024).decode('utf-8')
    print(msg)
    conn.close()

sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()

while 1:
    conn,addr = sk.accept()
    Thread(target=chat,args=(conn,)).start()

sk.close()
Server
import socket

sk =socket.socket()
sk.connect(('127.0.0.1',8080))

msg = sk.recv(1024)
print(msg)
inp = input('>>>').encode('utf-8')
sk.send(inp)
sk.close()
client

 4)Thread类的其他方法

Thread实例对象的方法
  # isAlive(): 返回线程是否活动的。
  # getName(): 返回线程名。
  # setName(): 设置线程名。

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

def wahaha(n):
    time.sleep(0.5)
    print(n,threading.current_thread(),threading.get_ident())
# 0 <Thread(Thread-1, started 5492)> 5492
# 1 <Thread(Thread-2, started 4544)> 4544
for i in range(2):
    threading.Thread(target=wahaha,args=(i,)).start()
# time.sleep(1)
print(threading.current_thread().getName())   #MainThread
print('主线程',threading.current_thread())  #主线程 <_MainThread(MainThread, started 2148)>
print('正在运行线程数',threading.active_count())  #正在运行线程数 3
print(threading.enumerate())  #[<_MainThread(MainThread, started 2148)>, <Thread(Thread-1, started 5492)>, <Thread(Thread-2, started 4544)>]
from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    t.start()
    t.join()
    print('主线程')
    print(t.is_alive())
    '''
    egon say hello
    主线程
    False
    '''

 join的用法的是:等待子线程结束之后,才执行join后面的代码

5)守护线程

无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。需要强调的是:运行完毕并非终止运行

#1.对主进程来说,运行完毕指的是主进程代码运行完毕
#2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
from threading import Thread
import time
def func1():
    while 1:
        print('*'*10)
        time.sleep(1)
def func2():
    print('in func2')
    time.sleep(5)

t = Thread(target=func1,)
t.daemon = True    # 主线程结束,守护线程也随之结束
t.start()   # 守护线程必须在t.start()之前设置
t2 = Thread(target=func2,)
t2.start()
t2.join()
print('主线程')

# 守护线程会在主线程结束之后会等待其他子线程的结束才结束
# 守护进程随着主进程代码的结束而结束
# 主线程会等待子线程的结束而结(当没有设置子线程为守护线程时)

 6)锁

  GIL锁的是线程,避免同一时间对CPU进行抢占资源,但是不能避免因为时间片的轮转,而造成对数据的不安全性操作。

引发的问题:

from threading import Thread
import os,time
def work():
    global n
    temp=n
    time.sleep(0.1)
    n=temp-1
if __name__ == '__main__':
    n=100
    l=[]
    for i in range(100):
        p=Thread(target=work)
        l.append(p)
        p.start()
    for p in l:
        p.join()

    print(n) #结果可能为99

 上述问题解决办法:加锁,由原来的并发执行变成串行

from threading import Thread,Lock
import os,time
def work(lock):
    global n
    lock.acquire()  #拿钥匙开锁
    temp=n
    time.sleep(0.1)
    n=temp-1
    lock.release()  #归还钥匙
if __name__ == '__main__':
    n=100
    l=[]
    lock = Lock()
    for i in range(100):
        p=Thread(target=work,args=(lock,))
        l.append(p)
        p.start()
    for p in l:
        p.join()

    print(n) #0

加锁lock与join的区别:未加锁部分并发执行,加锁部分串行执行。使用join则任务内的代码都是串行执行的。

7)死锁与递归锁

所谓的死锁:是指两个或者两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的想象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程或者线程称为死锁。

比如科学家吃面:

from threading import Thread,Lock
import time

#科学家吃面,形成一个死锁的情况
noodle_lock = Lock()  # 互死锁
fork_lock = Lock()
def eat1(name):
    noodle_lock.acquire()
    print('%s拿到面条了'%name)
    fork_lock.acquire()
    print('%s拿到叉子了'%name)
    print('%s吃面'%name)
    fork_lock.release()
    noodle_lock.release()

def eat2(name):
    fork_lock.acquire()
    print('%s拿到叉子了'%name)
    time.sleep(1)
    noodle_lock.acquire()
    print('%s拿到面条了'%name)
    print('%s吃面'%name)
    noodle_lock.release()
    fork_lock.release()

Thread(target=eat1,args=('alex',)).start()
Thread(target=eat2,args=('egon',)).start()
Thread(target=eat1,args=('jingboss',)).start()

结果:
alex拿到面条了
alex拿到叉子了
alex吃面
egon拿到叉子了
jingboss拿到面条了

解决的方法:使用递归锁,在Python中为了在同一线程中多次请求同一资源,python提供了可重入锁RLock。这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquier的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

上面的例子如果使用了RLock代替Lock,则不会发生死锁;可以形象的理解为:一串钥匙

import time
from threading import RLock,Thread
noodle_lock = fork_lock = RLock()   # 一个钥匙串上的两把钥匙

def eat1(name):
    noodle_lock.acquire()   # 拿到一把钥匙,就是拿到一串钥匙了
    print('%s拿到面条了'%name)
    fork_lock.acquire()
    print('%s拿到叉子了'%name)
    print('%s吃面'%name)
    fork_lock.release()
    noodle_lock.release()

def eat2(name):
    fork_lock.acquire()
    print('%s拿到叉子了'%name)
    time.sleep(1)
    noodle_lock.acquire()
    print('%s拿到面条了'%name)
    print('%s吃面'%name)
    noodle_lock.release()
    fork_lock.release()

Thread(target=eat1,args=('alex',)).start()
Thread(target=eat2,args=('egon',)).start()
Thread(target=eat1,args=('jingboss',)).start()

结果:
alex拿到面条了
alex拿到叉子了
alex吃面
egon拿到叉子了
egon拿到面条了
egon吃面
jingboss拿到面条了
jingboss拿到叉子了
jingboss吃面

8)信号量 semaphore

Semaphore管理一个内置的计算器,每当调用acquire()时内置计算器-1;调用release()时内置计算器+1;计数器不能小于0;当计算器为0时,acquire()将阻塞线程直到其他线程调用release()。

from threading import Semaphore,Thread
import threading
import time
def func(sem,a,b):
    sem.acquire()
    time.sleep(1)
    print('%s get sm'%threading.current_thread().getName(),a+b)
    sem.release()

sem = Semaphore(4)  # 同一时间只能有四个线程进入执行
for i in range(10):
    t = Thread(target=func,args=(sem,i,i+5))
    t.start()

与进程池是不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程。

9)事件

  为了解决程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行。

event.isSet():返回event的状态值;
event.wait():如果 event.isSet()==False将阻塞线程;
event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
event.clear():恢复event的状态值为False。
# 连接数据库,检测数据库的可连接情况
# 起两个线程,第一个线程连接数据库,连接之前,等待一个信号,告诉我,我们之间的网络是通的
# 第二个线程:检测与数据库之间的网络是否连通,time.sleep(0,2) 2秒之内,将事件的状态设置为True
import time
import random
from threading import Thread,Event
def connect_db(e):
    count = 0
    while count<3:
        e.wait(1)   # 状态为False的时候,我只等待1s就结束
        if e.is_set()  == True:
            print('连接数据库')
            break
        else:
            print('第%d次连接失败'%count)
            count +=1
    else:
        raise TimeoutError('数据库连接超时')
def check_web(e):
    time.sleep(random.randint(0,3))
    e.set()

e = Event()
t1 = Thread(target=connect_db,args=(e,))
t2 = Thread(target=check_web,args=(e,))
t1.start()
t2.start()

10)条件

使得线程等待,只有满足某条件时,才释放n个线程

Condition被称为条件变量,提供了acquire,release,wait,notify 方法。线程首先acquire一个条件变量,然后判断一些条件。如果条件不满足则wait;如果条件满足,进行一些处理改变条件后,通过notify方法来通知其他得线程,其他处于wait状态得线程接到通知后会重新判断条件。不断重复这一过程,从而解决复杂得同步问题。wait()和notify()函数必须放到acquire和release之内

# 条件,更复杂的锁
from threading import Condition
# 提供方法,acquire release wait notify
# 一个条件被创建之初,默认有一个False状态,这个会影响wait一直处于等待状态
# notify(int数据类型)制造一串钥匙
from threading import Thread
import time
def func(con,i):
    con.acquire()
    con.wait()   # 等制造好的钥匙,才能往下走
    # time.sleep(3)
    print('在第%d个循环里 '%i)
    con.release()
con = Condition()
for i in range(10):
    Thread(target=func,args=(con,i)).start()
while 1:
    num = int(input('>>>'))
    con.acquire()
    con.notify(num)
    con.release()

11)定时器

定时器,指定n秒后执行某个操作;

from threading import Timer
import time
def func():
    print('时间同步')

while True:
    t = Timer(5,func).start()   # 非阻塞
    time.sleep(5)

主线程和子线程各自等待5秒钟。

12)线程队列

queue队列:使用import queue

先进先出

import queue

q = queue.Queue()

q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
print(q.get())  #阻塞,直到队列里面有数据

后进先出(栈)

import queue

q = queue.LifoQueue()

q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
print(q.get())  #阻塞,直到队列里面有数据

设置优先级

q = queue.PriorityQueue()  # 优先级队列
q.put((20,'a'))
q.put((10,'b'))
q.put((30,'c'))
q.put((1,'z'))
q.put((1,'d')) # 当优先级相等时,按照ACSII排,越小越好


print(q.get())

元组得第一个元素是优先级(通常是数字,也可以是非数字之间得比较),数字越小优先级越高

13)Python标准模块--concurrent.futures

#1 介绍
concurrent.futures模块提供了高度封装的异步调用接口
ThreadPoolExecutor:线程池,提供异步调用
ProcessPoolExecutor: 进程池,提供异步调用
Both implement the same interface, which is defined by the abstract Executor class.

#2 基本方法
#submit(fn, *args, **kwargs)
异步提交任务

#map(func, *iterables, timeout=None, chunksize=1) 
取代for循环submit的操作

#shutdown(wait=True) 
相当于进程池的pool.close()+pool.join()操作
wait=True,等待池内所有任务执行完毕回收完资源后才继续
wait=False,立即返回,并不会等待池内的任务执行完毕
但不管wait参数为何值,整个程序都会等到所有任务执行完毕
submit和map必须在shutdown之前

#result(timeout=None)
取得结果

#add_done_callback(fn)
回调函数

线程池的作用:

线程池的作用就是限制系统中执行线程的数量,根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果。少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

为什么要用到线程池:

1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

import time
from concurrent.futures import ThreadPoolExecutor

def func(n):
    time.sleep(2)
    print(n)
    return n*n
tpool = ThreadPoolExecutor(max_workers=30)  # 不要超过cpu个数*5
t_lst = []
for i in range(60):
    t = tpool.submit(func,i)
    t_lst.append(t)

tpool.shutdown()  # close+join
print('主线程')
for t in t_lst:print('***',t.result())  # 这个结果必须是按照顺序来打印的

# tpool = ThreadPoolExecutor(max_workers=5)
# tpool.map(func,range(20))  # 异步执行,没有返回值 #map取代了for+submit

 回调函数

def func(n):
    time.sleep(2)
    print(n)
    return n*n

def call_back(m):
    print('结果是%s'%m.result())
tpool = ThreadPoolExecutor(max_workers=5)  # 不要超过cpu个数*5
for i in range(20):
    tpool.submit(func,i).add_done_callback(call_back)

 5.测试

IO密集型测试,多线程和协程

from gevent import monkey;monkey.patch_all()
import gevent
import time
import threading
import urllib2
def urllib2_(url):
    try:
        urllib2.urlopen(url,timeout=10).read()
    except Exception,e:
        print e
def gevent_(urls):
    jobs=[gevent.spawn(urllib2_,url) for url in urls]
    gevent.joinall(jobs,timeout=10)
    for i in jobs:
        i.join()
def thread_(urls):
    a=[]
    for url in urls:
        t=threading.Thread(target=urllib2_,args=(url,))
        a.append(t)
    for i in a:
        i.start()
    for i in a:
        i.join()
if __name__=="__main__":
    urls=["https://www.bing.com/"]*10      
    t1=time.time()
    gevent_(urls)
    t2=time.time()
    print 'gevent-time:%s' % str(t2-t1)
    thread_(urls)
    t4=time.time()
    print 'thread-time:%s' % str(t4-t2)

从结果可以看出,当并发数不断增大时,协程的效率确实比多线程要高,但在并发数不是那么高时,两者差异不大。

CPU密集型测试

主要测试:主要测试单线程、多线程、协程、多进程

from multiprocessing import Process as pro
from multiprocessing.dummy import Process as thr
from gevent import monkey;monkey.patch_all()
import gevent
def run(i):
    lists=range(i)
    list(set(lists))
    
if __name__=="__main__":
    '''
    多进程
    '''
    for i in range(30):      ##10-2.1s 20-3.8s 30-5.9s
        t=pro(target=run,args=(5000000,))
        t.start()
    '''
    多线程
    '''
    # for i in range(30):    ##10-3.8s  20-7.6s  30-11.4s
    #     t=thr(target=run,args=(5000000,))
    #     t.start()
    '''
    协程
    '''
    # jobs=[gevent.spawn(run,5000000) for i in range(30)]  ##10-4.0s 20-7.7s 30-11.5s
    # gevent.joinall(jobs)
    # for i in jobs:
    #     i.join()
    '''
    单线程
    '''
    # for i in range(30):  ##10-3.5s  20-7.6s 30-11.3s
    #     run(5000000)

  在CPU密集型的测试下,多进程效果明显比其他的好,多线程、协程与单线程效果差不多。这是因为只有多进程完全使用了CPU的计算能力。在代码运行时,我们也能够看到,只有多进程可以将CPU使用率占满。

  总结:需要编写并发爬虫等IO密集型的程序时,应该选用多线程或者协程,当我们需要科学计算,设计CPU密集型程序,应该选用多进程

posted on 2018-12-25 09:53  smile大豆芽  阅读(143)  评论(0)    收藏  举报