当前位置:博客首页 > Python > 正文

第18课 多线程编程

作者: Jarvan 分类: Python 发布时间: 2019-06-06 10:22 百度已收录

一、线程的概念

  1. 线程是一种轻量级的进程,它是在进程中存在的。
  2. 多线程的实现仍然是通过时间片的切换进行调度的。
  3. 线程跟进程的区别在于,线程是系统中最小的执行单位,而进程是系统中最小的资源分配单位。
  4. 同一个进程内的线程共享同一块内存空间。也就是说多线程是共享内存的,而多进程是不共享内存的。

多线程的概念
– 线程是在进程中运行的
– 所有的线程是共享内存的
– 程序启动的时候,默认就有一个线程(主线程)
– 可以通过threading模块的Thread类来创建多线程
– 每个创建出来的线程,都称为子线程
– 主线程和子线程是”同时”运行的

二、线程的创建和使用

python中有两个模块可以创建线程,一个是thread模块(在python3中不可用),还有一个是threading模块,threading模块是对thread模块进行了一系列列的包装,可以更加方便的创建和使用线程,因为thread模块是比较底层的模块,很多东⻄用起来比较麻烦,所以这里我们只用threading模块就可以了。

threading模块的使用

  1. 创建单线程

我们平时写的程序,如果没有使⽤用多进程和多线程的话,那么其实本质就是一个单线程(单进程)程序。单线程程序执⾏行行的话都是按顺序执行的,而且如果要进行大量的IO操作的话,由于IO延迟的原因,会显得非常的慢。

# -*- coding: utf-8 -*-
import time
def work(num):
    time.sleep(1) # 模拟IO延迟
    print("work num %d" % num)
if __name__ == "__main__":
# 调⽤用5次work函数,模拟IO请求5次
    for i in range(5):
        work(i)
# -*- coding: utf-8 -*-
"""
http://www.budejie.com/
"""

from urllib import request,error,parse
import re
import time
from threading import Thread

HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 '
                  '(KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'
}

def download(url,encoding='utf-8'):
    """
    下载函数
    :param url: 需要抓取源码的页面url
    :param encoding: 网页编码格式
    :return: 页面源码
    """
    try:
        req = request.Request(url,headers=HEADERS)
        resp = request.urlopen(req)
    except (error.HTTPError,error.URLError) as err:
        print(url,'download error',err)
        html = None
    else:
        html = resp.read().decode(encoding)
        return html

def extract(html,base_url):
    """
    解析函数,提取urls
    :param html: 网页源代码
    :param base_url:
    :return:
    """
    urls = re.findall(r'<div class="j-r-list-c-desc">.*?<a href="(/detail-\d+\.html)">[^<]+</a>',html, re.S)
    return [parse.urljoin(base_url, url) for url in urls]

def parse_detail(html):
    """
    提取详情
    :param html:
    :return:
    """
    h1 = re.findall('<h1>([^<]+)</h1>',html,re.S)
    print(''.join(h1))

def get_detail(url):
    html = download(url)
    if not html:
        return
    parse_detail(html)

if __name__ == '__main__':
    start = time.time()
    query = 'http://www.budejie.com/'
    source = download(query)
    detail_urls = extract(source, query)
    threads = []
    for link in detail_urls:
        th = Thread(target=get_detail,args=(link,))
        th.start()
        threads.append(th)
    for t in threads:
        t.join()
    print('总耗时:',time.time()-start)

for循环创建多线程的时候,不要在循环里面start之后直接调用join方法阻塞

因为这样跟单线程是没有什么区别的,而是应该把所有启动的线程,先放进一个列表里面去,然后再遍历列表,调用join方法

守护线程

  • 如果主线程退出了,子线程还没执行完成,那么主线程会等待子线程的执行完成(非守护)
  • 如果子线程并不重要,当主线程退出的时候,不管子线程是否执行完成,那么也会随着主线程退出而退出的线程,就是守护线程(说明该线程不重要)
  • 通过setDaemon(True) 来设置一个线程是守护线程或者x.daemon = True来设置
  • 一定要在start之前设置
# -*- coding: utf-8 -*-
from threading import Thread
import time
def work(num):
    time.sleep(1) # 模拟IO延迟
    print("work num %d" % num)
if __name__ == "__main__":
# 开启5个线程,模拟IO请求5次
    for i in range(5):
        t = Thread(target=work, args=(i,))
        t.start() # 调⽤用start⽅方法,开始执⾏行行线程
    print("主线程执⾏行完毕")
# -*- coding: utf-8 -*-
from threading import Thread
import time


def work(num):
    print('I am working with {}'.format(num))
    time.sleep(1)


if __name__ == '__main__':
    # d = {'num': 10}
    w1 = Thread(target=work, args=(1,))
    w1.start()
    print(w1.getName())  # 获取到线程的名称
    print(w1.isAlive())  # 获取线程是否活着
    print('daemo:', w1.isDaemon())  # 判断线程是否是守护线程
    w1.join()  # 阻塞等待线程执行完成(直到死为止)
    print(w1.is_alive())  # 另一种获取线程是否活着的方法

三、线程同步问题

3.1 多线程共享全局变量量问题

虽说多线程是共享同一块内存空间的,但是由于每个线程的执行时间是不确定的,都是由CPU来分配的,这就造成在处理理全局变量的时候,有可能线程t1和t2都同时对全局变量量num进程操作,比如num原来是10,同时进程加1之后,由于他们获取到的num值都是10,因此同时进行加1的时候就只能让num的值变为11,假如是当有一个线程在对num进程操作时,另一个线程等待之前的线程操作完成再去操作的话就不会出现上面的情况。

下⾯面的代码演示了了CPU时间片切换的明显现象:

# -*- coding: utf-8 -*-
"""
多线程CPU时间片切换演示
"""
from threading import Thread
import time
glist = []
def work(sequence):
    global glist
    for s in sequence:
        time.sleep(0.1) # 延时一下
        glist.append(s)

if __name__ == "__main__":
    string = "abcdefghijklmn"
    nums = range(20)
    t1 = Thread(target=work, args=(string,))
    t2 = Thread(target=work, args=(nums,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(glist)
    print("done")

# 输出结果为['a', 0, 'b', 1, 2, 'c', 'd', 3, 4, 'e', 'f', 5, 'g', 6, 
# 'h', 7, 8, 'i', 'j', 9, 10, 'k', 11, 'l', 'm', 12, 13, 'n', 14, 15,
# 16, 17, 18, 19]
# done

3.2 互斥锁(mutex)

互斥锁是python多线程为了解决多线程对相同资源竞争⽽而提供的一种锁机制,互斥锁有两种状态:上锁/释放锁。这两种状态是互斥的,也就是同一把锁一旦上锁就必须等待释放锁之后才能再次上锁。多线程中,同一把锁一旦有一个线程上锁,其他的线程必须等待该锁释放之后才能继续上锁。就像排队买票一样,同一个窗口,后面的人必须等待前⾯面的人买完了了才能上去买。

  • 同一把锁,同一时间只能由一个线程,其它线程要想获得该锁,那么必须等待获得锁的线程 释放锁才可以。
  • 死锁:多个线程同时想要获取对方的锁,但是又不释放自己的锁
# -*- coding: utf-8 -*-
from threading import Thread, Lock

num = 0

lock = Lock()


def add_num():
    global num
    for i in range(1000000):
        lock.acquire()  # 上锁
        num += 1
        lock.release()  # 释放锁


if __name__ == '__main__':
    print(num)
    ths = []
    for i in range(10):
        w = Thread(target=add_num)
        w.start()
        ths.append(w)

    for t in ths:
        t.join()

    print(num)

3.3 线程死锁

线程试图对同⼀一个互斥量量A加锁两次,线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁

也就是说,两个线程分别各自获得一把锁,然后在没有释放自己拥有的锁的同时想要获得对方的锁。就像两个人吵架,谁都不想先认错,互相僵持住了。

为了避免死锁,请不要让同一个线程,在同一个静态资源内请求多把锁一般我们的爬⾍虫也只是用到一把锁⽽而已。用不到两把锁的。而且大部分时间是使用消息队列来实现共享资源处理的问题。要想获得另一把锁,先把自己的锁释放掉。

四、生产者消费者模式以及消息队列

由于多线程是共享内存空间的,因此可以使用可变数据容器类型的方式来实现生产者消费者模式,我们可以使用列表,集合以及字典等可变类型来模拟队列。

可变数据类型特点:可以作为参数传入到函数内,在函数内改变会影响到外部变量的实际值。

本质:其实传入的是可变数据类型的地址(具体看课程讲解)

# -*- coding: utf-8 -*-
from threading import Thread
from queue import Queue
import time


def producer(no, q):
    for i in range(10):
        print('生产者{}号生产数字:{}'.format(no, i))
        q.put(i)
        time.sleep(1)
    q.put(None)  # 生产完成,放一个None进去,通知消费者


def consumer(no, q):
    while True:
        num = q.get()
        if num is None:  # 获取到None,知道是生产完了,那么就退出
            print('消费者{}号退出'.format(no))
            q.put(num)  # 把完成任务的信号,再放回队列里面,让其他人知道
            break
        print('消费者{}号消费数字:{}'.format(no, num))
        time.sleep(1.5)


if __name__ == '__main__':
    nums = Queue()
    p1 = Thread(target=producer, args=(1, nums))
    p2 = Thread(target=producer, args=(2, nums))
    p1.start()
    p2.start()
    c1 = Thread(target=consumer, args=(1, nums))
    c2 = Thread(target=consumer, args=(2, nums))
    c3 = Thread(target=consumer, args=(3, nums))
    c1.start()
    c2.start()
    c3.start()

使用队列实现生产者消费者模式

在多线程中使用的队列跟多进程的基本一样,但是用到的不是多进程中的队列,⽽而是python自带的queue模块。

多线程中队列Queue实例拥有的方法跟多进程中队列的方法是⼀样的:

put() 往队列里面添加一个对象(消息)
get() 从队列里面获取一个对象(消息)
join() 等待队列列所有任务执⾏行行完成,与task_done()⽅方法结合使⽤用
task_done() 每完成一个任务,就调用该方法,说明一个任务已经完成,任务数量量减一。只有用到join方法的时候才需要使用该方法进行配合使用。

Queue.Queue(maxsize=0)   FIFO, 如果maxsize小于1就表示队列长度无限
Queue.LifoQueue(maxsize=0)   LIFO, 如果maxsize小于1就表示队列长度无限
Queue.qsize()   返回队列的大小
Queue.empty()   如果队列为空,返回True,反之False
Queue.full()   如果队列满了,返回True,反之False
Queue.get([block[, timeout]])   读队列,timeout等待时间
Queue.put(item, [block[, timeout]])   写队列,timeout等待时间
Queue.queue.clear()   清空队列

发表评论