Python中的多线程、多进程

本文的大概内容从一片技术文章中而来,再加上自己的想法。

进程

在系统中,一个任务就是一个进程,比如开启浏览器,打开微信,每打开一个任务,代表在系统中启动了一个进程,进程代表着一个资源的集合。

线程

线程是操作系统能够运行的最小度量单位,他被包含在进程当中,是进程中实际的运行单位。

例:打开浏览器,打开多个TAB页面时就是启动了多个线程。

线程与进程的区别

进程 : 对各种资源管理的集合

线程 : 操作系统最小的调度单位,是一串指令的集合

进程不能单独执行,它只是资源的一个集合,如果进程想要操作CPU,进程必须先创建一个线程,在进程中的所有线程,都同享同一块内存空间。进程中第一个线程是主线程,主线程创建其他线程,其他线程也可以创建线程,线程之间是平等的,进程有父进程、子进程,独立的内存空间,唯一的进程标识符【pid】(注:启动线程比启动进程快,线程的内存空间是共享的,进程的空间是独立的。)

在Python中,如果想要使用多线程编程,需要导入threading模块。

import threading
import time
 
 
def talk(name, age):
    print('%s说:今年我%s岁' % (name, age))
    time.sleep(2)
    print('%s说:介绍完毕' % name)
 
 
# def talk(*args, **kwargs):
#     print('%s说:今年我%s岁' % (args[0], kwargs['age']))
#     time.sleep(2)
#     print('%s说:介绍完毕' % args[0])
 
 
t1 = threading.Thread(target=talk, args=('大师兄',), kwargs={'age': 19}, name='线程1', group=None)
t2 = threading.Thread(target=talk, args=('牛教授',), kwargs={'age': 45}, name='线程2')
t1.start()
t2.start()

在启动多线程时,target代表你要启动的多线程运行的函数(例:比如我有一个talk的函数,两人希望同时说话,把函数名赋值给target就可以了),args代表多线程执行的函数所需要的参数(例:talk的函数谁来说话,name就可以通过args传递给talk的函数。注:这里args如果传递多个参数时,它接收的是元组,如果是一个参数时,我们需要写逗号。),kwargs同样是多线程执行时函数所需要的参数(接受的是一个字典),name代表这个线程的名字,group代表线程组(Python还没有实现,所以默认必须穿None或不写)

threading与实例对象提供了几个方法可以让我们更直观的学习线程。

threading.active_count()  # 返回当前运行的线程个数
 
threading.enumerate()  # 返回当前运行中的线程list
 
threading.current_thread()  # 返回当前的线程变量
 
t1.start()  # 启动线程
 
t1.is_alive()  # 判断线程是否在运行 运行指启动后、终止前。
 
t1.getName()  # 获取线程名
 
t1.setName('填写更改后的名称')  # 对线程进行命名
 
t1.setDaemon(True)  # 设置守护线程
 
t1.isDaemon()  # 判断是否是守护线程
 
t1.join(timeout=20)  # 阻塞当前上下文环境的线程,直到调用此方法的线程终止或到达指定的timeout(可选参数)

在Python中还有一种通过继承来完成的多继承(了解就好)

import threading
import time
 
 
class MyThread(threading.Thread):
    def __init__(self, name):
        super(MyThread, self).__init__()
        self.name = name
 
    def run(self):
        print(self.name, '说开始')
        time.sleep(2)
        print(self.name, '说结束')
 
 
t1 = MyThread('大师兄')
t2 = MyThread('牛教授')
t1.start()
t2.start()

通过继承threading.Thread并覆盖run方法完成多线程,比我们通过def实现的多继承有局限性,我们在类中定义的run方法,不是随意定义的,函数名必须为run,否则不会重写Thread的run方法,程序不会执行多线程。

启动多线程时,需要等待这个线程执行完成,主线程在继续执行时我们就要用到join(阻塞主线程,挡住,无法执行join以后的语句,专,注执行多线程。)

import threading
import time
 
 
def talk(name):
    print(name, '说开始')
    time.sleep(2)
    print(name, '说结束')
 
 
t_list = []
for i in range(1, 4):
    t = threading.Thread(target=talk, args=('大师兄%s' % i,))
    t.start()
    t_list.append(t)
start = time.time()
for t in t_list:
    t.join()
end = time.time() - start
print(end)
print(threading.active_count())

join分为有参数和无参数两种

多线程多join的情况下,依次执行各线程的join方法,前头一个结束了才能执行后面一个。

无参数时,需要等待线程执行结束后,才可以继续执行主进程

设置参数后,则等待该线程这么长时间就不管它了(而该线程并没有结束)。不管的意思就是可以执行后面的主进程了。

import threading
import time
 
 
def talk(name):
    print(name, '说开始')
    time.sleep(2)
    print(name, '说结束')
 
 
t_list = []
for i in range(1, 4):
    t = threading.Thread(target=talk, args=('大师兄%s' % i,))
    t.start()
    t_list.append(t)
start = time.time()
for t in t_list:
    t.join(0.1)
end = time.time() - start
print(end)
print(threading.active_count())

在设置了join的timeout参数后,每一个线程在等待0.1秒后,继续执行主线程。这时的end值为0.4左右。也就证明了,当设置了join的参数后,等待线程0.1秒后继续执行了主线程。

守护线程(setDaemo)

守护线程,代表当主线程执行完成后,所有守护线程立即结束执行。(例:我们可以把主线程比喻为古代的皇帝,而守护线程则比喻为古代皇帝的妃子,如果皇帝(主线程)死亡,妃子(子线程)需要陪葬。)

import threading
import time
 
 
def talk(name):
    print(name, '说开始')
    time.sleep(2)
    print(name, '说结束')
 
 
for i in range(1, 4):
    t = threading.Thread(target=talk, args=('大师兄%s' % i,))
    t.setDaemon(True)
    t.start()
 
print(threading.active_count())

上面这段代码虽然我们启动了talk函数的说的方法,但是我们在方法中sleep了2秒钟,同时我们又对每一个启动的子线程设置了守护线程,这时当主线程执行完4个子线程的启动后,打印当前执行的线程数后,主线程结束执行,同时子线程跟着也就结束执行,所以没有打印到最后的xxx说结束,因为主线程已经带着子线程一起死了。

在多线程中,如果启动多个线程操作同一份数据,可能会导致数据错乱。这时我们就要加上锁,来控制保证这份数据每次只允许一个线程操作。

import threading
 
lock = threading.Lock()
num = 0
 
 
def write():
    global num
    lock.acquire()
    num += 1
    lock.release()
 
 
t_list = []
for i in range(1, 100):
    t = threading.Thread(target=write, )
    t.start()
    t_list.append(t)
for t in t_list:
    t.join()
print(num)

我们通过acquire(timeout=2)和release来控制上锁和解锁(注:这里在上锁后一定要记得将锁解开,否则后面的线程无法操作同一份资源。这里在Python3中不会出现操作数据异常的情况,Python3默认加锁了,在Python2中会出现数算不准的情况)

RLock(递归锁)

import threading, time
 
lock = threading.RLock()
 
 
def talk():
    print('准备获得锁')
    if lock.acquire():
        print('获得第一个锁')
        if lock.acquire():
            print('获得第二个锁')
            time.sleep(1)
            # lock.release()
            # print('解除第二个锁')
        time.sleep(1)
        lock.release()
        print('解除第一个锁')
    print('没有锁')
 
 
t = threading.Thread(target=talk, )
t1 = threading.Thread(target=talk)
t.start()
t1.start()

实验

通过多线程来体现爬虫的效率

import threading
import requests, time
 
urls = {
    "blog": 'http://www.imdsx.cn',
    "besttest": 'http://www.besttest.cn',
    "taobao": "http://www.taobao.com",
    "jd": "http://www.jd.com",
}
 
 
def run(name, url):
    res = requests.get(url)
    with open(name + '.html', 'w', encoding=res.encoding) as fw:
        fw.write(res.text)
 
 
start_time = time.time()
lis = []
for url in urls:
    t = threading.Thread(target=run, args=(url, urls[url]))
    t.start()
    lis.append(t)
for t in lis:
    t.join()
end_time = time.time()
print('run time is %s' % (end_time - start_time))

多进程

Python中多进程与多线程的代码格式相同,Python的进程需要使用multiprocessing模块。多进程要比多线程耗费的资源大,进程的数据是独立的,所以每启动一个进程都会生成一份内存来存储进程的数据。多进程实质就是启动每个进程中的默认线程去完成任务。

from multiprocessing import Process
import time
 
 
def talk(name):
    print("%s说:进程和线程的代码方式相同。" % name)
    time.sleep(1)
 
if __name__ == '__main__':
    names = ['dsx', 'nn', 'ads']
    pro = []
    for item in names:
        p = Process(target=talk, args=(item,))
        p.start()
        pro.append(p)
    for item in pro:
        item.join()

猜你喜欢

转载自blog.csdn.net/c123_sensing/article/details/81563411