30、Python 并发编程:线程、进程与守护进程全解析

Python 并发编程:线程、进程与守护进程全解析

1. 线程编程基础

在 Python 中,线程是实现并发的一种重要方式。以下是一个简单的线程池示例代码:

worker.start()
#spawn pool of arping threads
for i in range(num_arp_threads):
    worker = Thread(target=arping, args=(i, out_queue))
    worker.setDaemon(True)
    worker.start()
print "Main Thread Waiting"
#ensures that program does not exit until both queues have been emptied
in_queue.join()
out_queue.join()
print "Done"

运行这段代码,输出如下:

python2.5 ping_thread_basic_2.py
Main Thread Waiting
Thread 0: Pinging 10.0.1.1
Thread 1: Pinging 10.0.1.3
Thread 2: Pinging 10.0.1.11
Thread 0: Pinging 10.0.1.51
IP Address: 10.0.1.1 | Mac Address: [00:00:00:00:00:01]
IP Address: 10.0.1.51 | Mac Address: [00:00:00:80:E8:02]
IP Address: 10.0.1.3 | Mac Address: [00:00:00:07:E4:03]
10.0.1.11: did not respond
Done

通过添加线程池和队列,我们对第一个示例的行为进行了扩展。使用队列模块可以让线程的使用更加简单和安全,这是一项非常重要的技术。

2. 线程的定时延迟

Python 的 threading.Timer 提供了一种方便的方式来实现线程内函数的定时执行。以下是示例代码:

#!/usr/bin/env python
from threading import Timer
import sys
import time
import copy
#simple error handling
if len(sys.argv) != 2:
    print "Must enter an interval"
    sys.exit(1)

#our function that we will run
def hello():
    print "Hello, I just got called after a %s sec delay" % call_time

#we spawn our time delayed thread here
delay = sys.argv[1]
call_time = copy.copy(delay)    #we copy the delay to use later
t = Timer(int(delay), hello)
t.start()

#we validate that we are not blocked, and that the main program continues
print "waiting %s seconds to run function" % delay
for x in range(int(delay)):
    print "Main program is still running for %s more sec" % delay
    delay = int(delay) - 1
    time.sleep(1)

运行此代码,我们可以看到主线程会继续运行,而函数会在指定的延迟后执行:

[ngift@Macintosh-6][H:10468][J:0]# python thread_timer.py 5
waiting 5 seconds to run function
Main program is still running for 5 more sec
Main program is still running for 4 more sec
Main program is still running for 3 more sec
Main program is still running for 2 more sec
Main program is still running for 1 more sec
Hello, I just got called after a 5 sec delay
3. 线程事件处理程序

我们可以将延迟线程技术应用到实际场景中,例如监控两个目录的文件名变化。以下是一个线程化的目录同步工具示例:

#!/usr/bin/env python
from threading import Timer
import sys
import time
import copy
import os
from subprocess import call

class EventLoopDelaySpawn(object):
    """An Event Loop Class That Spawns a Method in a Delayed Thread"""
    def __init__(self, poll=10,
                        wait=1,
                        verbose=True,
                        dir1="/tmp/dir1",
                        dir2="/tmp/dir2"):
        self.poll = int(poll)
        self.wait = int(wait)
        self.verbose = verbose
        self.dir1 = dir1
        self.dir2 = dir2

    def poller(self):
        """Creates Poll Interval"""
        time.sleep(self.poll)
        if self.verbose:
            print "Polling at %s sec interval" % self.poll

    def action(self):
        if self.verbose:
            print "waiting %s seconds to run Action" % self.wait
        ret = call("rsync -av --delete %s/ %s" % (self.dir1, self.dir2), shell=True)

    def eventHandler(self):
        #if two directories contain same file names
        if os.listdir(self.dir1) != os.listdir(self.dir2):
            print os.listdir(self.dir1)
            t = Timer((self.wait), self.action)
            t.start()
            if self.verbose:
                print "Event Registered"
        else:
            if self.verbose:
                print "No Event Registered"

    def run(self):
        """Runs an event loop with a delayed action method"""
        try:
            while True:
                self.eventHandler()
                self.poller()
        except Exception, err:
            print "Error: %s " % err
        finally:
            sys.exit(0)

E = EventLoopDelaySpawn()
E.run()

延迟机制虽然不是严格必需的,但它可以带来一些好处,例如在发现其他事件时取消线程操作。

4. 进程编程

线程并不是 Python 中处理并发的唯一方式,进程在某些方面具有优势。由于全局解释器锁(GIL)的存在,Python 中的线程在同一时间只能有一个真正运行,并且只能使用一个处理器。因此,在需要大量使用 CPU 的情况下,使用进程是更好的选择。

以下是进程的优缺点对比:
| 对比项 | 线程 | 进程 |
| ---- | ---- | ---- |
| 可扩展性 | 受 GIL 限制,难以扩展到多个处理器 | 可以扩展到多个处理器 |
| 资源共享 | 共享全局状态 | 完全独立,通信需要更多努力 |
| 适用场景 | 适合 I/O 密集型任务 | 适合 CPU 密集型任务 |

5. 处理模块

处理模块(processing module)是一个用于 Python 的包,它支持使用标准库的线程模块的 API 来生成进程。以下是一个处理模块的入门示例:

#!/usr/bin/env python
from processing import Process, Queue
import time

def f(q):
    x = q.get()
    print "Process number %s, sleeps for %s seconds" % (x,x)
    time.sleep(x)
    print "Process number %s finished" % x

q = Queue()
for i in range(10):
    q.put(i)
    i = Process(target=f, args=[q])
    i.start()

print "main process joins on queue"
i.join()
print "Main Program finished"

运行此代码的输出如下:

[ngift@Macintosh-7][H:11199][J:0]# python processing1.py
Process number 0, sleeps for 0 seconds
Process number 0 finished
Process number 1, sleeps for 1 seconds
Process number 2, sleeps for 2 seconds
Process number 3, sleeps for 3 seconds
Process number 4, sleeps for 4 seconds
main process joins on queue
Process number 5, sleeps for 5 seconds
Process number 6, sleeps for 6 seconds
Process number 8, sleeps for 8 seconds
Process number 7, sleeps for 7 seconds
Process number 9, sleeps for 9 seconds
Process number 1 finished
Process number 2 finished
Process number 3 finished
Process number 4 finished
Process number 5 finished
Process number 6 finished
Process number 7 finished
Process number 8 finished
Process number 9 finished
Main Program finished

以下是该程序的执行流程图:

graph TD;
    A[初始化队列] --> B[将元素放入队列];
    B --> C[创建并启动进程];
    C --> D[主进程等待队列];
    D --> E[进程从队列获取元素];
    E --> F[进程执行任务];
    F --> G[进程完成任务];
    G --> H[主程序结束];

我们还可以使用处理模块实现基于进程的 ping 扫描,示例代码如下:

#!/usr/bin/env python
from processing import Process, Queue, Pool
import time
import subprocess
from IPy import IP
import sys

q = Queue()
ips = IP("10.0.1.0/24")

def f(i,q):
    while True:
        if q.empty():
            sys.exit()
        print "Process Number: %s" % i
        ip = q.get()
        ret = subprocess.call("ping -c 1 %s" % ip,
                        shell=True,
                        stdout=open('/dev/null', 'w'),
                        stderr=subprocess.STDOUT)
        if ret == 0:
            print "%s: is alive" % ip
        else:
            print "Process Number: %s didn’t find a response for %s " % (i, ip)

for ip in ips:
    q.put(ip)
#q.put("192.168.1.1")
for i in range(50):
    p = Process(target=f, args=[i,q])
    p.start()

print "main process joins on queue"
p.join()
print "Main Program finished"

这个代码与之前的线程代码类似,但在处理模块中,每个进程会在一个无限循环中从队列中获取元素。当队列为空时,进程会退出。

6. Python 进程调度

在掌握了 Python 中处理进程的多种方式后,接下来探讨如何调度这些进程。使用传统的 cron 来运行 Python 进程是非常合适的。

许多 POSIX 系统中的 cron 有一个不错的新特性,即调度目录。现在我们通常使用这种方式,只需将 Python 脚本放入四个默认目录之一: /etc/cron.daily /etc/cron.hourly /etc/cron.monthly /etc/cron.weekly 即可。

以往,很多系统管理员会编写传统的磁盘使用情况邮件脚本。例如,将一个 Bash 脚本放在 /etc/cron.daily 目录下,内容如下:

df -h | mail -s "Nightly Disk Usage Report" staff@example.com

不过,使用 Python 脚本会是更好的选择。以下是一个基于 cron 的磁盘报告邮件 Python 脚本示例:

import smtplib
import subprocess
import string

p = subprocess.Popen("df -h", shell=True, stdout=subprocess.PIPE)
MSG = p.stdout.read()
FROM = "guru-python-sysadmin@example.com"
TO = "staff@example.com"
SUBJECT = "Nightly Disk Usage Report"
msg = string.join((
    "From: %s" % FROM,
    "To: %s" % TO,
    "Subject: %s" % SUBJECT,
    "",
    MSG), "\r\n")
server = smtplib.SMTP('localhost')
server.sendmail(FROM, TO, msg)
server.quit()

这个脚本的操作步骤如下:
1. 使用 subprocess.Popen 读取 df 命令的标准输出。
2. 创建 From To Subject 变量。
3. 将这些字符串连接起来创建邮件消息。
4. 设置发件 SMTP 服务器为本地主机,并将之前设置的变量传递给 server.sendmail()

通常的使用方式是将该脚本放在 /etc/cron.daily/nightly_disk_report.py 目录下。对于 Python 新手来说,可以将此脚本作为模板代码,快速实现一些有趣的功能。

7. 守护进程化

在 Unix 系统中,处理守护进程是一项常见任务。守护进程通常被认为是在后台运行且没有控制终端的任务。很多人可能认为在命令末尾加 & 或者使用 Ctrl - z bg 命令可以将进程变成守护进程,但实际上这些操作只是将进程放到后台,并没有使进程脱离 shell 进程,也没有与控制终端分离。守护进程有三个特征:在后台运行、与启动它的进程分离、没有控制终端。普通的 shell 作业控制只能实现第一个特征。

以下是一个定义 daemonize() 函数的代码,它可以使调用代码成为守护进程,该代码来自相关的 Python 代码示例:

import sys, os

def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
    # Perform first fork.
    try:
        pid = os.fork( )
        if pid > 0:
            sys.exit(0) # Exit first parent.
    except OSError, e:
        sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror))
        sys.exit(1)

    # Decouple from parent environment.
    os.chdir("/")
    os.umask(0)
    os.setsid( )

    # Perform second fork.
    try:
        pid = os.fork( )
        if pid > 0:
            sys.exit(0) # Exit second parent.
    except OSError, e:
        sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror))
        sys.exit(1)

    # The process is now daemonized, redirect standard file descriptors.
    for f in sys.stdout, sys.stderr: f.flush( )
    si = file(stdin, 'r')
    so = file(stdout, 'a+')
    se = file(stderr, 'a+', 0)
    os.dup2(si.fileno( ), sys.stdin.fileno( ))
    os.dup2(so.fileno( ), sys.stdout.fileno( ))
    os.dup2(se.fileno( ), sys.stderr.fileno( ))

该函数的执行步骤如下:
1. 第一次 fork :调用 os.fork() 后会有两个相同的进程运行。检查 pid ,如果 pid 为正,说明处于父进程,父进程退出。若出现异常,进程也会退出。
2. 与父环境解耦
- 使用 os.chdir("/") 将工作目录更改为根目录 / ,确保守护进程在一个始终存在的目录中运行,避免影响文件系统的卸载。
- 使用 os.umask(0) 将文件模式创建掩码设置为最宽松,防止继承的掩码对守护进程创建文件的权限产生不良影响。
- 使用 os.setsid() 创建一个新的会话,使进程成为新会话的领导者和新进程组的领导者,并且没有控制终端,避免受到终端的作业控制影响。
3. 第二次 fork :再次进行 fork 操作,使最终的进程不能成为会话领导者,进一步确保进程不会获取控制终端。
4. 重定向标准文件描述符 :刷新标准输出和标准错误,然后将标准输入、输出和错误重定向到指定的文件(默认是 /dev/null )。

以下是一个使用守护进程化函数的示例:

from daemonize import daemonize
import time
import sys

def mod_5_watcher():
    start_time = time.time()
    end_time = start_time + 20
    while time.time() < end_time:
        now = time.time()
        if int(now) % 5 == 0:
            sys.stderr.write('Mod 5 at %s\n' % now)
        else:
            sys.stdout.write('No mod 5 at %s\n' % now)
        time.sleep(1)

if __name__ == '__main__':
    daemonize(stdout='/tmp/stdout.log', stderr='/tmp/stderr.log')
    mod_5_watcher()

这个脚本首先将自身守护进程化,并指定使用 /tmp/stdout.log 作为标准输出, /tmp/stderr.log 作为标准错误。然后在接下来的 20 秒内监控时间,每秒检查一次。如果时间(以秒为单位)能被 5 整除,则写入标准错误;否则写入标准输出。运行该脚本后,我们可以在相应的日志文件中看到结果。

运行脚本后,会立即出现一个新的命令提示符:

jmjones@dinkgutsy:code$ python use_daemonize.py
jmjones@dinkgutsy:code$

查看结果文件:

jmjones@dinkgutsy:code$ cat /tmp/stdout.log
No mod 5 at 1207272453.18
No mod 5 at 1207272454.18

综上所述,Python 提供了丰富的工具和方法来处理并发编程,包括线程、进程、进程调度和守护进程化等。通过合理运用这些技术,我们可以编写出高效、稳定的 Python 程序。

【RIS 辅助的 THz 混合场波束斜视下的信道估计定位】在混合场波束斜视效应下,利用太赫兹超大可重构智能表面感知用户信道位置(Matlab代码实现)内容概要:本文围绕“IS 辅助的 THz 混合场波束斜视下的信道估计定位”展开,重点研究在太赫兹(THz)通信中,由于超大可重构智能表面(RIS)引起的混合近场-远场(混合场)波束斜视效应,对用户信道感知位置估计带来的挑战。文中提出利用RIS调控电磁波传播特性,结合先进的信号处理算法,在波束斜视影响下实现高精度的信道估计用户定位,并提供了基于Matlab的代码实现,支持科研复现进一步优化。研究对于提升未来6G超高速无线通信系统的感知定位能力具有重要意义。; 适合人群:具备通信工程、信号处理或电子信息等相关专业背景,熟悉Matlab编程,从事太赫兹通信、智能反射面(RIS)或无线定位方向研究的研究生、科研人员及工程师。; 使用场景及目标:① 理解并复现混合场波束斜视效应下的信道建模方法;② 掌握基于RIS的太赫兹系统中信道估计联合定位算法的设计实现;③ 为后续开展智能超表面辅助的ISAC(通感一体化)研究提供技术参考和代码基础。; 阅读建议:建议读者结合Matlab代码,深入理解文档中提出的系统模型算法流程,重点关注波束斜视的数学表征、信道估计算法设计及定位性能评估部分,可通过调整参数进行仿真验证,以加深对关键技术难点和解决方案的理解。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值