python - OS相关

前言

我是偏后台开发的coder,学到python的这里时尤其的关注。操作系统的相关接口在python是不是比linux C中要简洁的多。OS的概念不说了,这次笔记集中关注python中多进程、多线程、高并发、加锁同步、进程间通信等实现。

Definition

进程(process),在我的理解中,就是一个任务,是一段运行的程序。后台的童鞋应该知道其本质就是一个task_struct结构体,里面记载着程序运行需要的所有资源和他自身的信息。当他获得运行所需的内存、CPU资源等,也就是成为了一个running状态的进程。

可以把进程理解为一个任务,那线程就是完成这个任务的执行流。线程是CPU调度的最小粒度。通常来说,现在的项目中,至少我接触的,一个进程中都包括着不止一个的线程。毕竟现在的OS都是SMP的,充分利用多核心提高程序效率应该是每个coder敲键盘时需要优先考虑的。

多进程

linux的内核向外提供了 fork() 这个系统调用来创建一个本进程的拷贝,当然往往fork()后都跟着 exec() 族系统调用,我们创建一个进程一般都是为了执行其他的代码程序。

python的 os 模块封装了很多常用的系统调用,可以说是python中最常用的一个库了。举个栗子:

import os

print('Process (%s) start...' % os.getpid())

pid = os.fork()
if pid == 0:
    print('Child process (%s).' % os.getpid())
else:
    print('Parent process (%s).' %  pid)

fork() 会返回两个结果,父进程返回一个大于0的无符号数,子进程返回0。

我们都知道socket()是有好几个步骤的,而对于web服务器,每天每时每分都有着成千上万的访问请求。如果是一个进程向外提供服务,那就是这个进程为第一个用户从创建socket到关闭,再为下一个用户提供服务。用户时排着队接受服务的,显然不符合逻辑。

拿Apache举个栗子,它是多进程架构服务器的代表。

  1. 运行主程序,只负责server端socket的listen()accept(),当然主进程是一个守护进程
  2. 每当一个用户请求服务,就会调用fork(),在子程序中接受数据,read()或者write(),然后提供服务直至关闭
  3. 主进程还是要负责回收结束的子进程资源的

伪代码如下:

import os

server_fd = socket()
bind(server_fd,ip,port)
listen(server_fd,MAX_PROCESS)
While Online:
    connfd = accpet(server_fd)
    for each connfd:
        os.fork()
        // TODO
close(server_fd)

上面这段程序只适用linux平台,windows平台创建进程的方式并不是 fork() 调用。python中提供了multiprocesssing模块来兼容windows,比起fork(),代码的语义更好理解一些

from multiprocessing import Process
import os

def run_proc(name):
    print('Child process %s (%s)...' % (name, os.getpid()))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    #创建Process实例
    p = Process(target=run_proc, args=('test',))
    print('Child process will start.')
    p.start()
    p.join()
    print('Child process end.')

这里的join语义和linux平台的多线程中的join语义很像,但效果其实是linux平台的wait

有时候需要进程池,multiprocessing 也直接提供了pool用于创建。

pool.apply(func,params) 是单进程阻塞模式
pool.apply_async(func,params,callback)  是多进程异步模式
pool.map(func,iter) 用于可迭代结构,阻塞式调用
pool.map_async(func,iter,callback)

一般情况下,还是把进程数控制成和CPU核数相同。pool结束调用pool.join()回收进程资源时,需要先pool.close()

上面提到过,创建一个新进程的原因往往是为了加载新的代码,去执行新的任务。所以python封装了fork()和之后的exec族,提供subprocess模块,直接操作新的子进程。这个包,一般是用来执行外部的命令或者程序如shell命令,和os.system()类似。

import subprocess

r = subprocess.call(['ls','-l'])    #阻塞
r = subprocess.call('ls -l',shell = True)
r = subprocess.check_call(['ls','-l'])  #returncode不为0则raise CalledProcessError异常
r = subprocess.check_output('ls -l',shell=True)
r = subprocess.Popen(['ls','-l'])   #非阻塞,需主动wait

r = subprocess.Popen(['ls','-l'],stdin=child1.stdout,stdout=subprocess.PIPE, stderr=subprocess.PIPE)    #设置标准输入输出出错的句柄
out,err = r.communicate()   #继续输入,或者用来获得返回的元组(stdoutdata,stderrdata)

手动继续输入的例子:

import subprocess

print('$ python')
p = subprocess.Popen(['python'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = p.communicate(b"print('Hello,world')")
print(output.decode('utf-8'))
print('Exit code:', p.returncode)

进程间通信

multiprocessingQueue或者Pipe来帮助实现,类似linux中的Pipe,打开一条管道,一个进程往里面扔数据,一个从另一头捡数据。python中的Pipe是全双工管道,既可以读也可以写。可以通过Pipe(duplex=False)创建半双工管道。

from multiprocessing import Pipe,Queue
#实例
q = Queue()
p = Pipe()
#写入数据
q.put(value)
p[0].send(value)
#读数据
q.get()
p[1].recv()

分别举个例子,用Queue

from multiprocessing import Process, Queue
import os, time, random

def write(q):
    print('Process to write: %s' % os.getpid())
    for value in ['A','B', 'C']:
        print('Put %s to queue...' % value)
        q.put(value)
        time.sleep(random.random())

def read(q):
    print('Process to read: %s' % os.getpid())
    while True:
        value = q.get(True)
        time.sleep(random.random())
        print('Get %s from queue.' % value)

if __name__=='__main__':
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))

    pw.start()
    pr.start()
    pw.join()
    pr.terminate()

用Pipe:

from multiprocessing import Process, Pipe
import os, time, random

def write(q):
    print('Process to write: %s' % os.getpid())
    for value in ['A','B', 'C']:
        print('Put %s to pipe...' % value)
        q.send(value)
        time.sleep(random.random())

def read(q):
    print('Process to read: %s' % os.getpid())
    while True:
        value = q.recv()
        time.sleep(random.random())
        print('Get %s from pipe.' % value)

if __name__=='__main__':
    p = Pipe()
    pw = Process(target=write, args=(p[0],))
    pr = Process(target=read, args=(p[1],))

    pw.start()
    pr.start()
    pw.join()
    time.sleep(2)
    pr.terminate()

多线程

有人会有疑问,问什么要在进程中开多个线程,多创建几个进程一起干活不就行了。其实这样是可以的,只不过进程这个单位有点大,比较占用资源,创建的时候开销比较大(尤其在windows系统下),进程多了CPU调度起来,在进程间切换也是非常耗时的。还有多任务协同合作时,需要数据交换,进程间通信也是开销,而一个进程中的线程是共享进程的内存空间的,可以直接交互。所以现在多线程的程序更加常见。

不过多线程也是有弊端的,协同合作的多线程,有一个挂了,会影响到所有的其他线程,也就代表这个任务是做不下去了。进程因为有着独立的地址空间,所以一个进程死了对其他进程的影响可以说很小。

python中提供了threading模块为多线程服务,threading.current_thread()返回当前线程,主线程名为MainThread

import threading

thread = threading.Thread(target=func,args=())
thread.start()
thread.join()

多线程编程,最重要的就是同步和互斥,也就是各种锁的用法。为什么要用锁,后台的童鞋应该都懂,现在的SMP操作系统都是抢占式内核,也就是即使你不同的核共同工作时,很幸运的没有改乱一个共享变量,当然这就不可能了。当你的CPU时间片到时间了,或者需要内存或者IO资源,你被踢出了CPU的工作队列,你必须得在走的时候给你的资源把锁加上,下次再来接着做。线程同步的重点的是对共享资源的判断,和选择合适的锁。也就是对什么资源加锁和用什么锁。

不过在python中很遗憾,多线程存在着天生的缺陷,因为有着GIL的存在,这是python解释器的设计缺陷。导致python程序在被解释时,只能有一个线程。不过,对于IO密集型的程序,多线程的设计还是很有帮助的。比如爬虫

  • 最常用的锁,类似 mutex
  • 条件变量,threading.Condition()会包含一个Lock对象,因为这两者一般都是配合使用的。
  • 信号量,threading.Semaphore()
import threading

lock = threading.Lock()
lock.acquire()
lock.realease()  #配合try...finally保证最后释放掉锁,防止死锁

cond = threading.Condition()
cond.wait()
cond.notify()   cond.notify_all()

sem = threading.Semaphore(NUM)
sem.acquire()
sem.realease()

event = threading.Event()   #相当于没有lock的cond
event.set(True)
event.clear()

假设以下的情况

thread_func(params):
    web_res = params
    def func1(web_res):
        http = web_res.http
        TODO
    def func2(web_res):
        data = web_res.data
        TODO
    def func3(web_res):
        user = web_res.user
        TODO

在一个线程中,又存在多个子线程或者函数时,需要把一个参数都传给它们时。可以通过唯一的id来区分出从全局变量自己的局部变量时。可以用ThreadLocal实现

import threading 

student = threading.local()

def func(name):
    person = student.name  #需要之前关联过

p1 = threading.Thread(target=func,argc='A')
p1 = threading.Thread(target=func,argc='B')

通过ThredLocal免去了我们亲自去字典中存取。通常用于web开发中的为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等。

分布式进程

分布式是为了在横向上提升整个系统的负载能力。python中multiprocessing模块中的manage子模块支持把多进程分布到不同的机器上。当然肯定存在一个master进程来负责任务的调度。依赖manage子模块,可以很轻松的写出分布式程序。

比如爬虫,想要爬下豆瓣或者知乎这样网站的全部数据,用单机估计得花费好几年。可以把需要爬的网站的所有URL放在一个Queue中,master进程负责Queue的管理,可以将很多设备与master进程所在的设备建立联系,爬虫开始获取URL时,都从主机器获取。这样就能保证协同不冲突的合作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • @(python)[笔记] 目录 一、什么是进程 1.1 进程的概念 进程的概念起源于操作系统,是操作系统最核心的...
    CaiGuangyin阅读 1,247评论 0 9
  • 进程与线程的区别 现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码...
    苏糊阅读 754评论 0 2
  • 引言&动机 考虑一下这个场景,我们有10000条数据需要处理,处理每条数据需要花费1秒,但读取数据只需要0.1秒,...
    chen_000阅读 500评论 0 0
  • 我读过的现代作品很少,但我不得不承认,路遥的《平凡的世界》无疑是深深打动我的。最令人痛心的是,晓霞的死。 我...
    锦官飞云阅读 233评论 0 1
  • 俗话说:人穷志短。 这话说的一点都没错,人穷到一定份儿上,志气梦想什么的就会抛到九霄云外,只要能混口饭吃,什么工作...
    口香糖啊原来阅读 7,580评论 15 7