返回顶部
首页 > 资讯 > 后端开发 > Python >Python3 多线程
  • 248
分享到

Python3 多线程

多线程 2023-01-31 08:01:15 248人浏览 八月长安

Python 官方文档:入门教程 => 点击学习

摘要

两个概念: 并发:假同时,一段时间内同时处理多个任务,单核都可以; 并行:真同时,同时处理多个任务,必须多核。 主流操作系统上完成并发的手段有进程和线程,主流的编程语言提供了用户空间的调度:协程。python 也不例外。 (想自学习编

两个概念:

  • 并发:假同时,一段时间内同时处理多个任务,单核都可以;
  • 并行:真同时,同时处理多个任务,必须多核。

主流操作系统上完成并发的手段有进程和线程,主流的编程语言提供了用户空间的调度:协程。python 也不例外。

(想自学习编程的小伙伴请搜索圈T社区,更多行业相关资讯更有行业相关免费视频教程。完全免费哦!)

由于现在的操作系统上的进程越来越轻量,导致进程和线程之间的区别越来越少。事实上,linux 并没有原生的线程,线程是通过进程实现的。

Python 中每一个进程会启动一个解释器,而线程会共享一个解释器。

Python 中的线程是通过标准库 threading 实现的。而启动一个线程就是让这个线程执行一些逻辑,这些逻辑就对应一个函数。

>>> import threading
>>> def worker(): # 让多个线程来执行它
...     print('work')
...
>>> thread = threading.Thread(target=worker) # 创建了一个线程对象,target 参数是一个函数,即线程要执行的逻辑
>>> thread.start() # start 启动一个线程,执行完毕后,自动退出,Python 没有提供主动退出线程的方法
work

由于 python 没有提供退出线程的方法,因此我们一定不能在逻辑中定义死循环,不然线程无法退出。当然直接 kill -9 和刻意为之的另说。而像那种监听某个端口提供服务的进程,为了保证不退出,通常都会有一个 while True 的死循环。

上面只是启动了一个线程,很显然没什么屌用。启动多个线程的方式非常简单,就是在它的外面套一个 for 循环就可以了:

import time
import threading

def worker(num):
    time.sleep(1)
    print('work-{}'.fORMat(num))

for i in range(5):
    t = threading.Thread(taret=worker, args=(i, )) # 启动了五个线程,要启动几个就循环几次
    t.start()

通过 args 给函数传递参数,也可以使用 kwargs 通过字典传递。结果是在等待一秒之后,所有线程同时输出了,并且在一个线程的换行符还没有打印出来的时候,下一个线程就输出了,这就涉及到线程安全的问题了。很显然,print 并不是线程安全的。

线程相比于进程更轻量,上下文切换的代价没有进程那么大,但即使如此,线程数量也不宜过多。

标识一个线程

threading.current_thread() 可以返回当前的线程对象。

>>> threading.Thread(target=lambda: print(threading.current_thread())).start()
<Thread(Thread-13, started 140007299499776)>

返回的线程对象我们可以通过一个变量进行接收:

thread = threading.current_thread()

它有很多属性和方法:

  • name:返回线程的名字;
  • ident:返回该线程的唯一标识符;
  • is_alive:告知该线程是否存活;
  • enumerate:可以通过循环它打印出所有的线程;

我们创建线程对象的时候是可以给它取名字的:

t = threading.Thread(target=worker, name='thread1')

这个 name 可以通过 logging 的 threadName 获得。

logging

前面提到过,print 并不是线程安全的,而 logging 模块线程安全。

>>> import logging
>>> logging.warning('hehe')
WARNING:root:hehe
>>> logging.info('hehe') # 默认只输出 warning 以上级别

我们可以对其进行一些基础的配置,让其记录 DEBUG 以上的级别,以及记录线程名:

>>> logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(threadName)s %(message)s')
>>> logging.info('hehe')
2017-09-23 15:41:36,868 INFO MainThread hehe

知道了它的简单用法之后,我们就可以使用多线程了:

import logging
import threading

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(threadName)s %(lineno)d %(message)s')

def worker():
    logging.info(logging.info('work'))

for i in range(5):
    t = threading.Thread(target=worker)
    t.start()

使用 logging 就没有问题了,因此我们通常使用它来替代 print。

logging 还可以将异常的栈追踪信息记录下来,这在排查错误的时候非常方便:

import logging

try:
    config['DE']['xxx']
except Exception as e:
    logging.exception(e)
print('xxx')

daemon 与 non-daemon

daemon 在 linux 上是守护进程的意思,它始终在后台运行。而在 Python 中的 daemon 线程会在主线程退出之后退出。也就是说,如果不是 daemon 线程,主线程退出之后,非 daemon 线程还会继续执行,直到结束退出。

线程默认不是 daemon,如果想要设置为 daemon,那就在创建线程对象的时候,给它传递 daemon=True 即可。

>>> t = threading.Thread(target=worker, daemon=True)
>>> t.daemon
Out[20]: True

通过下面的例子证明之前的说法:

import time
import logging
import threading

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(threadName)s %(message)s')

def worker():
    logging.info('start')
    time.sleep(2)
    logging.info('end')

if __name__ == '__main__':
    logging.info('start')
    t1 = threading.Thread(target=worker, name='non-daemon')
    t1.start()
    t2 = threading.Thread(target=worker, name='daemon', daemon=True)
    t2.start()
    logging.info('end')

# 执行结果
2017-09-24 04:08:49,027 INFO MainThread start
2017-09-24 04:08:49,028 INFO non-daemon start
2017-09-24 04:08:49,028 INFO daemon start
2017-09-24 04:08:49,028 INFO MainThread end
2017-09-24 04:08:51,031 INFO non-daemon end

执行上面的代码你会发现有的时候主线程退出了,但是 daemon 线程还会执行完成。这是因为虽然从日志中看到主线程退出,但是事实上主线程是没有退出的,它会等待非 daemon 线程执行完毕后才会退出,这样就给了 daemon 线程的执行时间了。当我们将 t1 给注释掉之后,就不可能出现主线程退出后,daemon 线程仍然执行的情况了。

如果我们在 t2.start() 之后增加一行 t2.join(),那即使它是 daemon 线程,主线程依然会等待它执行完毕后再退出。因为 join 会阻塞直到线程执行完毕。join 支持一个参数,那就是阻塞的秒数。t2.join(1) 表示只阻塞一秒,这个时候即使 t2 没有执行完成,主线程依然会退出。join 用的比较多,它并不占用 CPU 时间。

创建线程的另一种方法

上面创建线程的方法是通过实例化 Thread,我们还可以通过下面这种方式:

import logging
import threading

class Mythread(threading.Thread):
    def run(self):
        logging.warning('worker')

t = Mythread()
t.start()

通过继承 + 重写 run 方法来到达启动多线程的效果,run 等同于之前 target 指定的函数。但是 Python 中这种方法使用的很少。

当我们创建一个线程对象的时候,除了可以使用 start 启动它之外,还可以通过 run 来启动。如果不是以继承的方式创建线程,一个线程对象的 run 和 start 只能执行其中一个。

thread local

定义一个 thread local 对象。

ctx = threading.local()

这时的 ctx 没有任何属性,我们可以给它增加属性:

>>> ctx.data = 5
>>> ctx.data
Out[25]: 5

继续:

>>> data = 'abc' # 定义一个变量
>>> def worker():
...     logging.warning(data)
...     logging.warning(ctx.data)
...
>>> worker() # 执行没什么问题
WARNING:root:abc
WARNING:root:5
>>> threading.Thread(target=worker).start() # 但是通过线程执行就不行了
WARNING:root:abc # data 可以直接打出来
Exception in thread Thread-9:
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/usr/local/python3/lib/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "<ipython-input-32-2e99199c517b>", line 3, in worker
    logging.warning(ctx.data)
AttributeError: '_thread._local' object has no attribute 'data' # 但是 ctx.data 提示没有

这是因为 ctx.data 是一个 thread local 的变量,我们可以给它赋值任意属性,但是只对当前线程可见。线程独享!

使用 run 方法,它会将 target 放在主线程中;start 则会将其放到子线程中,二者只能执行一个。

定时器

也可以称为延时执行。Python 中存在一种特殊的线程,可用于延迟执行。它继承自 Thread 类,因此它也是 Thread 对象。

>>> def worker():
...     logging.warning('worker')
...
>>> t = threading.Timer(interval=5, function=worker)
>>> t.start()

  • interval:延时多少秒执行,默认为 30;
  • function:等同于 target。

可以看到执行 start 方法后,五秒后才有输出。在等待的过程中,它可以通过 cancel() 终止。

它也可以设置线程名,只不过要这样:

>>> t = threading.Timer(interval=5, function=worker)
>>> t.name = 'Timer'
>>> t.deamon = True # 设置是否为 daemon

当 function 指定的函数开始执行的时候,无法通过 cancel() 终止。

Timer 的定时执行功能很弱,如果真的有这方面的需要,可以使用 APSchedule。

event

第一种线程同步的方式。同步意味着阻塞,如果线程之间没有联系,完全没有必要使用同步。有这么一种需求:worker 线程做一些事情,当它完成之后,通知 boss 线程,由 boss 完成处理后续工作。这可能并不难实现,但是 boss 线程要统计 worker 线程的执行时间呢?

这就要用到线程间通信的机制了,最简单的是 event:

>>> event = threading.Event()
>>> event.set()
>>> event.wait()
Out[8]: True

它是一个 threading.Event 的对象,有 set 和 wait 这两个方法。wait 会阻塞线程直到 set 方法被调用。
有了这两种方法之后,我们就可以完成上面的需求了:

import time
import random
import logging
import datetime
import threading

def worker(event: threading.Event):
    time.sleep(random.randint(1, 5))
    event.set()

def boss(event: threading.Event):
    start = datetime.datetime.now()
    event.wait()
    logging.warning('worker exit {}'.format(datetime.datetime.now() - start))

def start():
    event = threading.Event()
    b = threading.Thread(target=boss, args=(event,), name='boss')
    b.start()
    for x in range(5):
        threading.Thread(target=worker, args=(event,), name='worker').start()

start()

五个 worker 线程,谁先执行完成就谁执行 event.set(),一旦 event.set 被执行,boss 线程也就会继续执行并输出日志了。但是会有一个问题,由于是随机 sleep 时间,也就是说最快 boss 线程可以一秒就退出,但是还有四个 worker 线程还在执行,这四个线程拉长了整个脚本的执行时间。

再做修改:

import time
import random
import logging
import datetime
import threading

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(threadName)s %(message)s')

def worker(event: threading.Event):
    s = random.randint(1, 5)
    event.wait(s) # 先阻塞
    event.set() # 一下全放开了
    logging.info('sleep {}'.format(s))

def boss(event: threading.Event):
    start = datetime.datetime.now()
    event.wait()
    logging.info('worker exit {}'.format(datetime.datetime.now() - start))

def start():
    event = threading.Event()
    threading.Thread(target=boss, args=(event,), name='boss').start()
    for x in range(5):
        threading.Thread(target=worker, args=(event,), name='worker-{}'.format(x)).start()

start()

# 执行结果
2017-09-25 06:15:42,114 INFO worker-0 sleep 2
2017-09-25 06:15:42,115 INFO boss worker exit 0:00:02.004014
2017-09-25 06:15:42,116 INFO worker-1 sleep 5
2017-09-25 06:15:42,116 INFO worker-2 sleep 4
2017-09-25 06:15:42,116 INFO worker-3 sleep 3
2017-09-25 06:15:42,117 INFO worker-4 sleep 2

可以看到都在同一秒退出了,这是因为 wait 可以指定超时时间,时间一到它就不再阻塞。这样阻塞时间最短的那个线程就会执行 set,这样一来所有阻塞的线程同时放开了,于是同一时间都执行完成了。因此,wait 会阻塞线程直到 set 方法被调用,或者超时时间到。

event 可以被多个线程所持有,多个线程可以同时被阻塞,一旦其中一个线程执行了 set,那么所有的线程都不再阻塞。event 可以在线程之间发送信号,通常用于某个线程需要其他线程处理某些动作之后才能启动。

event 还有一个特性,如果先 set 然后 wait,不管有没有指定超时,它都瞬间返回 True(因为阻塞被放开,所以无法再阻塞);而如果直接 wait,且给它一个超时时间,那么超时完成之后,它会返回 False。我们可以根据这个特点来完成定时的操作。

import logging
import threading

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(threadName)s %(message)s')

def worker(event: threading.Event):
    while not event.wait(3):
        logging.info('running')

event = threading.Event()
threading.Thread(target=worker, args=(event,)).start()

每三秒会输出一次日志,会无限输出下去。但是如果执行 event.set() 就会终止死循环。

event 还有一些方法:

  • is_set:用来判断有没有 set 过;
  • clean:清除 set 标志,通常用来做线程退出的条件。

    def worker(event):

    while not event.is_set():
        pass
    

wait 会主动让出 CPU 时间片,time.sleep 却不会。假如它们分到了 10ms 的 CPU 时间,都使用了 5ms,那么剩余的 5ms wait 会让给别人,而 sleep 会自己用完。因此我们会使用 wait 而不是 sleep。

实现定时器

延时执行。

import logging
import threading

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(threadName)s %(message)s')

class Timer:
    def __init__(self, interval, function, *args, **kwargs):
        self.interval = interval
        self.function = function
        self.args = args
        self.kwargs = kwargs
        self.event = threading.Event()
        self.thread = threading.Thread(target=self.__thread)

    def __thread(self):
        if not self.event.wait(self.interval):
            self.function(*self.args, **self.kwargs)

    def start(self):
        self.thread.start()

    def cancel(self):
        self.event.set()

def worker():
    logging.info('running')

t = Timer(interval=2, function=worker)
t.start()

Lock

第二种线程同步的方式。lock 用来保护共享资源,其余几种线程同步的方式都是用了它。

import random
import logging
import threading

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(threadName)s %(message)s')

class Counter:
    def __init__(self):
        self.__val = 0

    @property
    def value(self):
        return self.__val

    def inc(self):
        self.__val += 1

    def dec(self):
        self.__val -= 1

counter = Counter()

def fn():
    if random.choice([-1, 1]) > 0:
        logging.info('inc')
        counter.inc()
    else:
        logging.info('dec')
        counter.dec()

for x in range(10):
    threading.Thread(target=fn).start()

print(counter.value)

上面的代码即使你知道它加了多少次减了多少次,但你不能肯定它的结果,这是因为资源的争用。Lock 对象可用于解决这种问题:

>>> lock = threading.Lock()
>>> lock.acquire()
Out[4]: True

对于 lock 实例,只能调用一次 acquire 方法,再次调用会被阻塞,直到 release 方法被调用。根据它的这种特性,可用来改造之前的 Counter。

class Counter:
    def __init__(self):
        self.__val = 0
        self._lock = threading.Lock()

    @property
    def value(self):
        return self.__val

    def inc(self):
        self._lock.acquire()
        self.__val += 1
        self._lock.release()

    def dec(self):
        self._lock.acquire()
        self.__val -= 1
        self._lock.release()

这样一来,不管有多少个线程,同一时间只会有一个线程能够修改 __val。但是这样会有一个问题,如果执行加减的时候发生了异常(虽然这里不会),那么 release 永远就不会执行,那么就会形成死,因此我们要使用 try finally。

  def inc(self):
        try:
            self._lock.acquire()
            self.__val += 1
        finally:
            self._lock.release()

从上面这种结构中我们可以联想到 with,事实上它是支持 with 的,因此我们可以定义的更为简单:

def inc(self):

with self._lock:
    self.__val += 1

凡是用锁的地方,一定要在 finally 中使用 release,否则就会有锁死的可能性。

而对于读来说,如果不加锁,就会存在脏读的可能性,就看能不能忍受了。通过加锁之后,Counter 类就变成线程安全了,我们可以放心的使用。

锁是并发的难点,它会将并发变为串行,掌握了锁,并发就没有丝毫问题了。那么何时需要加锁?凡是有共享资源的地方都要加锁。

lock 对象可以接收两个参数:

    • blocking:当再次加锁时,如果它为 False,那么不会阻塞,而是返回 False;
    • timeout:如果 blocking 为 True,timeout 大于等于 0 会阻塞到超时,并返回 False。

    预先启动 10 个线程处理一些任务,当其中一个线程在处理其中一个任务时,其他线程可以处理其他任务,这时候就可以用到非阻塞锁。第一个线程对该任务加非阻塞锁,由于之前没有加过锁,因此可以加上。第二个线程再加的时候就加不上了,并且返回 False,这时就可以让它跳过这个任务去执行下一个任务了。

    import logging
    import threading
    
    logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(threadName)s %(message)s')
    
    def worker(tasks):
        for task in tasks:
            # 第一个执行加锁的线程可以锁,它的值为 True。由于锁住了,剩下的九个线程执行的时候它的值都为 False
            # 因此 loggi.info 语句只会执行 10 次
            if task.lock.acquire(False):
                logging.info(task.name)
    
    class Task:
        def __init__(self, name):
            self.name = name
            self.lock = threading.Lock()
    
    tasks = [Task(x) for x in range(10)]
    
    for i in range(5):
        threading.Thread(target=worker, args=(tasks,), name='work-{}'.format(i)).start()
    

    如果任务有先后顺序的话,就只能串行了。

    RLock

    可重入锁在同一个线程内可多次加锁,但是只能有一个线程成功,并且 acquire 几次,就需要 release 几次。

    >>> rlock = threading.RLock()
    >>> rlock.acquire()
    Out[13]: True
    >>> rlock.acquire()
    Out[14]: True
    >>> rlock.release()
    >>> rlock.release()
    

    condition

    第三种线程同步的方式。通常用于生产者消费者模式,生产者生产消息之后,使用 notify 和 notify_all 通知消费者进行消费。而消费者使用 wait 方法阻塞等待生产者的通知。

    import random
    import logging
    import threading
    
    logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(threadName)s %(message)s')
    
    class Dispatcher:
        def __init__(self):
            self.data = None
            self.event = threading.Event()
            self.cond = threading.Condition()
    
        def consumer(self):
            while not self.event.wait(1):
                with self.cond:
                    self.cond.wait() # 会阻塞,直到 notifyAll 被执行
                    logging.info(self.data)
    
        def producer(self):
            for _ in range(10):
                data = random.randint(0, 100)
                logging.info(data)
                self.data = data
                with self.cond:
                    self.cond.notify_all()
                self.event.wait(1)
            self.event.set()
    
    d = Dispatcher()
    p = threading.Thread(target=d.producer, name='producer')
    c = threading.Thread(target=d.consumer, name='consumer')
    p.start()
    c.start()
    

    有生产者修改共享资源,然后通知消费者进行消费。

    • wait:会阻塞,直到被 notify 唤醒;
    • notifyAll:老版的驼峰写法,现已改为下面的,但为了兼容仍然存在;
    • notify_all:用于通知所有 wait 的线程,可以理解为广播;
    • notify:接收一个数字,表示唤醒多少个 wait 线程,默认为 1。可以理解为单播。

    比如下面的示例中,虽然启动了四个消费者进程,但是只允许两个同时消费,至于是哪两个就不得而知了。

    import random
    import logging
    import threading
    
    logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(threadName)s %(message)s')
    
    class Dispatcher:
        def __init__(self):
            self.data = None
            self.event = threading.Event()
            self.cond = threading.Condition()
    
        def consumer(self):
            while not self.event.is_set():
                with self.cond:
                    self.cond.wait()
                    logging.info(self.data)
    
        def producer(self):
            for _ in range(10):
                data = random.randint(0, 100)
                logging.info(data)
                self.data = data
                with self.cond:
                    self.cond.notify(2)
                self.event.wait(1)
            self.event.set()
    
    d = Dispatcher()
    p = threading.Thread(target=d.producer, name='producer')
    for i in range(4):
        threading.Thread(target=d.consumer, name='consumer-{}'.format(i)).start()
    p.start()
    

    按理来说,因为有锁的存在,所以只有在消费者的 with 代码块执行完毕,锁释放之后,生产者才能进入自己的 with 代码块。这样就能够保证,消费者只有在消费完毕之后生产者才能继续生产。但是我在运行过程中生产者根本不会等待消费者消费,它自己一个劲的跑。

    无论 notify、notify_all 还是 wait,都必须先 acquire,完成之后必须确保 release,因此通常使用 with 语法。

    barrier

    第四种线程同步的方式,栅栏的意思,只有凑齐一拨人之后才往下走。从下面这段代码中就能理解它的作用:

    import logging
    import threading
    
    logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(threadName)s %(message)s')
    
    def worker(barrier: threading.Barrier):
        logging.info('waiting for {} threads'.format(barrier.n_waiting))
        try:
            # 上面的代码各个线程什么时候执行,怎么执行都无所谓
            # 但是所有线程都会在这里同时等待,只有所有线程都执行到这了,才同时执行下面的代码
            worker_id = barrier.wait()
        except threading.BrokenBarrierError:
            logging.warning('aborting')
        else:
            logging.info('after barrier {}'.format(worker_id))
    
    # 实例化的时候指定拦多少个线程,如果启动了四个线程,只要三个到齐了就可以同时往下走了
    barrier = threading.Barrier(3)
    for i in range(3):
        threading.Thread(target=worker, args=(barrier,), name='worker-{}'.format(i)).start()
    logging.info('start')
    

    barrier 对象的一些属性和方法:

    • wait:阻塞线程,它可以指定超时时间,超时时间一到抛出 BrokenBarrierError 异常。如果执行过 abort 方法,那么再执行 wait 也会抛出 BrokenBarrierError 异常;
    • reset:清除对象执行 abort 的痕迹。执行 abort 后执行 rest,接着执行 wait 就不会抛异常了;
    • n_waiting:当前有多少个线程在等待;
    • abort:通知已经在等待的线程不必再等了,不能因为它一个而让其他线程在那傻等。而一旦执行了这个方法, wait 就会抛出 BrokenBarrierError 异常,因此不处于 wait 状态的线程是不会抛出这个异常的。

    适用场景:比如有十种工作,每个线程负责一种,只有这十个线程都初始化完成后才能工作。

    semaphore

    最后五种线程同步的方式。信号量和锁很像,锁是为 1 的信号量。

    # 创建一个为 3 的信号量
    >>> s = threading.Semaphore(3)
    >>> s.acquire()
    Out[84]: True
    >>> s.acquire(False)
    Out[85]: True
    >>> s.acquire(False)
    Out[86]: True
    >>> s.acquire(False)
    Out[87]: False
    

    它可以锁多次,上面锁了三次都没有问题,等到第四次的时候就不行了。由于锁只能锁一次,所以它是为 1 的信号量。RLock 也能锁多次,它是它只能用在同一个线程上,信号量却可以在多个线程中使用。

    创建一个连接池的时候可以用到它:

    import time
    import random
    import logging
    import threading
    
    logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(threadName)s %(message)s')
    
    class Pool:
        def __init__(self, num):
            self.num = num # 指定池子的连接数
            self.conns = [self._make_connect(x) for x in range(num)]
            self.s = threading.Semaphore(num)
    
        # 这个函数是拿到连接之后做的操作
        def _make_connect(self, name):
            return name
    
        # 从池子中取出一个连接
        def get(self):
            self.s.acquire()
            return self.conns.pop()
    
        def return_resource(self, conn):
            # 执行完毕后,将连接放回池子中
            self.conns.insert(0, conn)
            self.s.release()
    
    def worker(pool):
        logging.info('started')
        name = pool.get()
        logging.info('get connect {}'.format(name))
        time.sleep(random.randint(1, 3))
        pool.return_resource(name)
        logging.info('return resource {}'.format(name))
    
    pool = Pool(3)
    for i in range(5):
    threading.Thread(target=worker, args=(pool,), name='worker-{}'.format(i)).start()

    如果不使用信号量的话,我们还需要对池子是否为空进行判断。为什么将连接放回连接池中的 insert 操作不需要加锁呢?这是因为 GIL 的影响。

    信号量也是对资源的保护,但是和锁不一样的地方在于,锁限制只有一个线程可以访问共享资源,而信号量限制指定个线程可以访问共享资源。事实上我们只需要使用信号量就可以了,因为锁本身就是信号量的一种。

    queue

    队列,它是进程间通信的一种方式,队列有三种:

    • FIFO:Queue.Queue(maxsize=0),先进先出,线程安全;
    • LIFO:Queue.LifoQueue(maxsize=0),后进先出;
    • Priority:Queue.PriorityQueue(maxsize=0),优先队列。

    创建一个先进先出队列:

    >>> import queue
    >>> q = queue.Queue() # 队列长度无限
    

    对象的属性和方法:

    • empty():判断队列是否为空(不可靠)。因为等你获取队列的长度时,可能已经有人往里面放入了数据;
    • full():队列是否满了(不可靠);
    • maxsize:查看队列的最大长度;
    • qsize():看到队列当前长度(不可靠);
    • clear():清空队列;
    • join():等到队列为空的时候,才进行操作;
    • put():往队列里面添加内容,可以为任意数据结构。put(self, item, block=True, timeout=None),block 表示是否为队列是否为阻塞状态。队列满了,再往里面加内容,队列会阻塞。如果不阻塞会返回一个异常,默认为阻塞状态;timeout 是阻塞的时间,如果队列满了,再往队列里面添加数据时,timeout 时间后会抛出异常。如果为 None(默认),它会一直阻塞,直到有线程从队列中取出数据;
    • get():从队列中取内容。如果是先进先出队列,它会取出最先存进去的数据。get(self, block=True, timeout=None),如果队列是空的,并且 timeout 为 None,它会一直阻塞,直到有线程往队列里面存入数据;
    • put_nowait(item):等效于 put(item, block=False);
    • get_nowait():等效 get(item, block=False)。

    我们可以通过它来重写生产者消费者模型:

    #!/usr/local/python3/bin/python3
    
    import queue
    import random
    import logging
    import threading
    
    logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(threadName)s %(message)s')
    
    def producer(queue: queue.Queue, event: threading.Event):
        while not event.wait(3):
            data = random.randint(0, 100)
            logging.info(data)
            queue.put(data)
    
    def consumer(queue: queue.Queue, event: threading.Event):
        while not event.is_set():
            logging.info(queue.get())
    
    q = queue.Queue()
    e = threading.Event()
    threading.Thread(target=consumer, args=(q, e), name='consumer').start()
    threading.Thread(target=producer, args=(q, e), name='producer').start()
    

    通过 e.set() 就能停止它。相对 condition 实现的生产者消费者模型,它的优势在于可以暂存数据,这在生产者和消费者速率不一致的时候很好用;而它的缺陷在于无法广播,无法通知多个线程同时消费一条消息。因为我们通常可以将它们结合起来使用。

    取出队列中所有数据:

    while not q.enpty():
        q.get()
    

    GIL

    全局解释器锁,这是 Python 争议很大的一个点。正是由于它的存在,在操作内置容器时,解释器会在解释器级别增加一个锁,因此 Python 所有内置容器(字典、列表等)都是线程安全的,多线程环境下使用没有丝毫问题。而导致的后果就是 Python 的并发性能很差。

    Python 中 collection, logging 等标准库都是线程安全的。

    concurrent.futures

    官网地址,Python3.2 引入的异步模块。
    创建一个线程池

    from concurrent import futures
    pool = futures.ThreadPoolExecutor(max_workers=5)
    

    pool 对象有三个方法。submit 用于执行一个函数:

    >>> fut = pool.submit(lambda: 1+1) # 执行一段逻辑,也就是一个函数
    >>> fut.result() # 获取执行结果
    Out[116]: 2
    >>> fut.done() # 查看函数是否执行完成
    Out[117]: True
    >>> fut.running() # 是否处于运行状态
    Out[118]: False
    >>> fut.cancel() # 一个已经开始运行的线程是无法结束的,没开始的(比如 pool 满了在阻塞)可以
    Out[119]: False
    >>> fut.exception() # 如果函数中产生了异常,可以通过它来获取异常的实例
    

    传递参数:

    pool.submit(self.create_vm, vm_attributes, extra_attributes, conns)

    通过这种方式使用线程,不需要将数据发送到队列中。

    进程池由 ProcessPoolExecutor 实现,它们简化了进程和线程的操作,并且对返回值和异常进行了处理。

    建议使用 futures,虽然它无法设置线程名(3.6 之后可以)、daemon 等属性,但是问题不大。

    --结束END--

    本文标题: Python3 多线程

    本文链接: https://lsjlt.com/news/193282.html(转载时请注明来源链接)

    有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

    猜你喜欢
    • Python3 多线程
      两个概念: 并发:假同时,一段时间内同时处理多个任务,单核都可以; 并行:真同时,同时处理多个任务,必须多核。 主流操作系统上完成并发的手段有进程和线程,主流的编程语言提供了用户空间的调度:协程。Python 也不例外。 (想自学习编...
      99+
      2023-01-31
      多线程
    • python3 - 多线程 - thre
      目录 一、threading 模块:对线程的处理 二、Thread组件 2-1 开启线程的方式一(函数) 2-2 开启线程的方式二 (类) 2-3 Thread 相关内置子方法  2-4 主进程等待新建线程实例 2-5 线程共用创造进程内...
      99+
      2023-01-31
      多线程 thre
    • python3 多线程编程
      0.什么是线程 多线程模块 创建线程的方法 join()方法 4.isAlive()方法 name属性和daemon属性 6.线程的同步---锁 7.线程的同步---Event对象 8.线程的同步---Condition条件...
      99+
      2023-01-31
      多线程
    • Python3多线程详解
      目录为什么要使用多线程?如何创建和执行一个线程方法一正式介绍threading模块守护线程 Daemon方法一方法二让主线程等待子线程结束 join线程共享资源可能引起什么问题?竞态...
      99+
      2023-05-19
      Python3多线程 Python3线程
    • Python3 多线程讲解
      前言 Python3 线程中常用的两个模块为•_thread•threading(推荐使用) thread 模块已被废弃。用户可以使用 threading 模块代替。所以,在 Python3 中不能再使用”thread” 模块。为了兼容性,...
      99+
      2023-01-31
      多线程
    • python3 queue多线程通信
      目录queue分类例子一、生产消费模式例子二、task_done和join例子三、多线程里用queuequeue分类 python3 queue分三类: 先进先出队列后进先出的栈优先...
      99+
      2024-04-02
    • python3-001-多线程简单聊天室
              这次代码为python3编写,并且使用网络调试助手、与python3聊天室程序进行通信测试。1、先放干货代码,如下: 1 from socket import * 2 from threading import Th...
      99+
      2023-01-31
      多线程 聊天室 简单
    • python3 queue多线程通信怎么实现
      这篇文章主要介绍了python3 queue多线程通信怎么实现的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇python3 queue多线程通信怎么实现文章都会有所收获,下面我们一起来看看吧...
      99+
      2023-07-02
    • python3实现多线程ping一个网段
      ####################################################""" 实现多线程ping一个网段的地址,测试联通性 copyright  2016/9/30 lighter_py"""#######...
      99+
      2023-01-31
      网段 多线程 ping
    • Python3多线程处理爬虫的实战
      多线程 到底什么是多线程?说起多线程我们首先从单线程来说。例如,我在这里看书,等这件事情干完,我就再去听音乐。对于这两件事情来说都是属于单线程,是一个完成了再接着完成下一个。但是我一...
      99+
      2023-03-02
      Python3多线程爬虫 Python 多线程爬虫
    • python3进程和线程
      进程基本概念 进程是执行中的程序, 也就是说进程是动态的, 程序是静态的 进程是操作系统分配资源的最小单位,有一个进程控制块(PCB), 有自己唯一的一个进程标识符(PID) 进程之间相互独立, 内存不共享 每个进程都是一个实体, ...
      99+
      2023-01-31
      线程 进程
    • python3爬虫中多线程的使用示例
      这篇文章主要介绍了python3爬虫中多线程的使用示例,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。本文教程操作环境:windows7系统、Python 3.9.1,DELL...
      99+
      2023-06-14
    • 怎么使用Python3多线程处理爬虫
      本文小编为大家详细介绍“怎么使用Python3多线程处理爬虫”,内容详细,步骤清晰,细节处理妥当,希望这篇“怎么使用Python3多线程处理爬虫”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。多线程到底什么是多线程...
      99+
      2023-07-05
    • python3--threading模块(线程)
      上章节内容总结:队列    队列:先进先出,数据进程安全    管道+锁 可实现队列    生产者消费者模型:解决数据供需不平衡管道    双向通信 数据进程不...
      99+
      2023-01-30
      线程 模块 threading
    • Python3 多线程(连接池)操作MySQL插入数据
      目录多线程(连接池)操作MySQL插入数据 1.主要模块 2.创建连接池 3.数据预处理 4.线程任务 5.启动多线程 6.完整示例 7.思考/总结 多线程(连接池)操作MySQL插...
      99+
      2024-04-02
    • python3多线程批量去除电视剧的片头
      简介 下载的电视剧经常遇到片头和片尾,有的片头还有广告,比较厌烦,所以笔者就写了个脚本,使用python3,ffmpeg批量多线程去除了片头和片尾,这里针对的是单部多集电视剧的片头、片尾,片头片尾的时间比较相似 针对多部多集的电视剧,可以...
      99+
      2023-01-31
      片头 多线程 批量
    • python3多进程实现
      python多进程的主要方式有两种,一种是使用os模块的fork方法,另一种方法是使用multiprocessing模块,这两种方法的就别在于前者仅适用于Unix/Linux操作系统,后者是跨平台的实现方式 1.使用fork方法实...
      99+
      2023-01-31
      进程
    • Python3中多线程操作MySQL插入数据的方法
      这篇文章主要介绍Python3中多线程操作MySQL插入数据的方法,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!多线程(连接池)操作MySQL插入数据针对于此篇博客的收获心得:首先是可以构建连接数据库的连接池,这样可...
      99+
      2023-06-15
    • python多线程————3、多线程间通
      1、共享变量 #通过共享变量 import time import threading url_list = [] def get_detail_html(): global url_list while True: ...
      99+
      2023-01-31
      多线程 python
    • python3--进程,线程,协程效率对比
      需求:写一个脚本,判断192.168.11.0/24网络里,当前在线ip有哪些?知识点:1 使用subprocess模块,来调用系统命令,执行ping 192.168.11.xxx 命令2 调用系统命令执行ping命令的时候,会有返回值(p...
      99+
      2023-01-30
      线程 进程 效率
    软考高级职称资格查询
    编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
    • 官方手机版

    • 微信公众号

    • 商务合作