更多优质内容
请关注公众号

python 多线程 + DBUtils连接池操作数据库(附python之GIL)-张柏沛IT博客

正文内容

python 多线程 + DBUtils连接池操作数据库(附python之GIL)

栏目:Python 系列: 发布时间:2020-03-06 14:08 浏览量:5700

import os,threading
from DBUtils.PooledDB import PooledDB
import pymysql,random,time
from queue import Queue
# from twisted.enterprise import adbapi
# from twisted.internet import reactor

# 创建一个有10个连接的mysql连接池.创建并维持10个线程并发写入1000000条随机数据到test.stu表中

class Test(threading.Thread):
    def __init__(self,n):
        super(Test,self).__init__()
        mysql_conf = {
            "host":"127.0.0.1",
            "user":"root",
            "passwd":"*****",
            "charset":"utf8",
            "db":"test",
            "cursorclass":pymysql.cursors.DictCursor
        }

        # 创建一个连接池,连接池初始最多容纳和创建25个连接,当连接池没有可用连接则阻塞
        # 使用连接池可以进行长连接,无需每次操作mysql时都建立连接,节省了建立连接的时间
        self.n=n
        self.pool = PooledDB(pymysql,maxconnections=25,blocking=True,**mysql_conf)
        self.alpha = list("qwertyuiopasdfghjklzxcvbnm")
        self.sex=["m","s"]

    def run(self):
        print("%s号线程开始任务" % self.n)
        sql = "insert into stu values (%s,%s,%s,%s,%s)"

        # 获取连接
        conn = self.pool.connection()
        cursor = conn.cursor()

        data_set=[]
        try:
            for i in range(100):
                name = "".join(random.sample(self.alpha,5))
                sex = random.choice(self.sex)
                age = random.randint(10,60)
                classid = random.randint(1000,9999)
                data_set.append((None, name, sex, age, classid))

            cursor.executemany(sql,data_set)    # 批量操作,提高效率
            conn.commit()
            print("%s号线程完成任务" % self.n)
        except:
            # 如果出现错误,要回滚
            conn.rollback()
            print("%s号线程任务失败" % self.n)
        finally:
            # 无论插入成功还是失败,记得将连接放回连接池供其他线程使用,否则该线程会一直被占用
            cursor.close()
            conn.close()  # 执行完sql操作后,将连接放回连接池,而不是真的关闭连接.如果不放回连接池,则该连接一直处于占用状态,其他线程就无法使用该连接


# 最多创建10个线程并发执行
start_time = time.time()
thread_list=[]  # 创建线程池
for i in range(1000):
    thread_list.append(Test(i+1))
    if len(thread_list)>=10:  # 当列表中的线程有10个,就开始执行10个线程
        print(len(thread_list))
        print(i)
        for thread in thread_list:
            thread.start()

        for thread in thread_list:
            thread.join()   # 10个线程都等待执行完,也就是说,10个线程有一个线程没运行完就不能往下执行代码; 这里会阻塞后面的thread_list=[]和print。但是多个线程间的join和join不会阻塞,也就是说执行完一个join还可以马上执行下一个join,但是执行完最后一个join不能马上执行 thread_list=[]

        thread_list=[]  # 当所有线程运行完清空线程池 
       
print("总共用时:"+str(time.time()-start_time))

 

上面的例子要注意:

1. 必须等所有的线程都执行完start()后才能执行join(),而不能是一个线程执行一次start()和join(),下一个进程在执行一次start()和join(),这样的话就是多个线程顺序执行而不是并发执行,就和单线程没有区别了。

2. 在上面的代码中,执行1000次循环,每循环一次开启一个线程,但是并发的线程只有10个,等10个线程完成执行完才能再开新的10个线程。这意味着,10个并发的线程中,有的线程先执行完,有的还没有,必须等最慢的那个线程执行完才能开启新的一批10个线程。这也意味着不是每时每刻都有10个线程在并发执行。而且,10个线程只要有一个线程阻塞住了,就不能生成新的线程执行任务,一直卡着不能工作。但是最理想的状态是,10个线程如果有先执行完的就会新创建一个线程补上,时时刻刻保持有10个线程在并发运行。这点是我没有做到的。

3. 每次使用完连接都要执行 conn.close() ,执行这一句不是真正的关闭连接,而是将连接放回连接池等待其他线程使用。如果不执行conn.close() 会导致线程不断创建连接,超过了连接池能容纳的最大连接数而报错: pymysql.err.OperationalError,1040, u'Too many connections'

如果不调用join()等待线程执行完,也会导致这个问题,原因是线程没有执行完就又开始产生新的线程,还没来得及执行conn.close就又不断产生的线程从而产生过多的mysql连接数。

4. 使用连接池可以复用长连接发送mysql请求,无需每次执行语句都连接一次。另外,使用批量操作也可以增加mysql操作的效率

 

 

关于Python中的多线程和多进程

首先

Python的多线程(threading.Thread)是无法使用多核的,这意味着python的多线程只能并发而不能并行。

Python的多进程(multiprocessing.Process)是可以使用多核的,这意味着python的多进程是可以并行,能够充分利用CPU资源

 

为什么python的多线程不能利用多核CPU

因为在 python中有一个 GIL( Global Interpreter Lock),中文为:全局解释器锁

 

什么是GIL和为什么会用到GIL

GIL是一个互斥锁,它防止多个线程同时执行Python字节码(python代码).。这个锁是必要的,主要是因为CPython(Python解释器)的内存管理不是线程安全的。怎么个不安全呢?

Python内部对变量或数据对象使用了引用计数器,我们通过计算引用个数,当个数为0时,变量或者数据对象就被自动释放。

这个引用计数器需要保护,当多个线程同时修改这个值时,可能会导致内存泄漏;我们使用锁来解决这个问题,可有时会添加多个锁来解决,这就会导致另个问题,死锁;

为了避免内存泄漏和死锁问题,CPython使用了单锁,即全局解释器锁(GIL),即执行Python字节码都需要获取GIL,而其他线程如果想要操作和执行相同的代码需要等某个线程操作完了,释放了GIL后,这个线程才能拿到GIL锁,并且上锁,执行代码。

也就是说多线程共用一个GIL锁,如果某线程需要执行,要等持有GIL锁的线程释放了锁,才能获取这个锁才能执行。而多锁则是每个线程使用一把锁。

这可以防止死锁,导致多线程同一时刻只能有一个线程在执行,也就是多线程并行而不是并发.

 

在 python多线程下,每个线程的执行方式如下: 
1、获取GIL

2、执行代码直到sleep或者是 python虚拟机将其挂起(比如执行IO操作)。 
3、释放 GIL

一个进程中,同一时间只会有一个获得了 GIL 的线程在跑,其它的线程都处于等待状态等着 GIL 的释放。 

多线程中,如果某个占有GIL锁的线程在遇到 I/O 操作时会释放这把锁。如果是纯计算的程序,没有 I/O 操作,解释器会每隔 
100次操作就让线程释放这把锁,让别的线程有机会执行代码

 

一个进程会有自己的GIL锁,所以如果是多进程的话,能够使用到多核实现真正的并行。

 

关于python中多线程和多进程的适用场景

IO密集型操作

IO密集型操作,指的是频繁读写文件,或者进行网络请求的操作。IO操作无需CPU参与。

适合使用多线程

 

在操作系统中,我们知道IO操作的速度远比CPU操作要慢,所以在执行IO操作时,线程或者进程会让出CPU给其他线程或进程并进入阻塞状态,而在该线程或者进程阻塞过程中,IO操作由IO设备独立完成(主存和辅存的交互,如硬盘数据写入内存或者内存数据写入硬盘,这样的操作可以有IO设备独立完成,无需CPU参与)

在python多线程中,如果某个占有GIL锁的正在运行的线程在执行IO操作,由于IO操作无需CPU参与并且需要等待,所以该线程就会释放GIL锁让给其他线程执行。当其他线程在执行的时候,他们就会占用CPU资源。

 

多线程非常适合IO密集型操作,例如爬虫或者数据库的写操作。

因为:单线程在进行IO操作等待时不能做其他事情,而多线程在IO操作等待时其他线程可以做其他事情如发出更多的IO请求

例如:

使用单线程进行爬数据或者数据库操作,假设要爬100个网页,每爬一个页面都要等待响应,假如一个页面平均等0.1秒才能获取其内容。单线程要10s+才能执行完。

使用多线程,10个线程爬100个网页,10个线程一起发出请求并一起等,10个线程在0.1秒后就得到响应,爬取到10个页面。1s+就能执行完。因为等待的时间是平摊到每一个线程的。

当然单核执行多线程是并发交替执行,所以会进行线程间的切换,会损耗切换的时间。切换一次的时间短的可以忽略不计,但是如果线程数增加,切换的次数增加,切换的时间也会增加。在IO操作中,IO等待时间远比切换消耗的时间大,所以多线程比单线程有利。

如果是CPU密集型操作,由于都是计算性的工作,没有阻塞和等待的情况发生,此时多线程和单线程都是同一时刻只有一个线程在运算,但是多线程会消耗切换时间而单线程不会。结果是单线程反而比多线程快。

 

CPU密集型操作

CPU密集型操作指频繁使用CPU的操作,如大规模的数据运算。

适合使用多进程

 

原因是python多线程是并发而不是并行,所以多线程和单线程都是同一时刻只有一个线程在运行,多线程由于频繁切换反而比单线程慢。

python多线程会使用到多核,所以多线程间是并行的。在核数充足的情况下,一个主进程下有几个子进程在运行就相当于有几个人在同时工作。




更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > python 多线程 + DBUtils连接池操作数据库(附python之GIL)

热门推荐
推荐新闻