# 进程和线程

进程(Process)是操作系统进行资源分配和调度的基本单位,是程序执行时的一个实例。它拥有自己独立的地址空间,包含程序计数器、寄存器、堆栈段、数据段等,以及一个或多个线程。

特点:

  • 独立性:每个进程都拥有自己独立的地址空间和系统资源,如内存、文件句柄等。

  • 并发性:多个进程可以在操作系统中并发执行,但进程间的通信和同步较为复杂。

  • 动态性:进程是程序执行时的一个实例,具有生命周期,从创建、运行到消亡。

线程(Thread)是进程内的一个执行单元,也是操作系统调度的基本单位。它共享进程的资源(如地址空间、文件句柄等),但拥有自己的程序计数器、寄存器、堆栈等。

特点:

  • 共享性:线程共享其所属进程的地址空间和资源,这使得线程间的通信和同步相对简单。

  • 并发性:多个线程可以在一个进程内并发执行,提高程序的执行效率。

  • 轻量级:线程的创建、销毁和切换相对于进程来说开销较小。

# 进程与线程的区别与联系

区别:

  • 资源分配:进程是资源分配的基本单位,每个进程都有独立的地址空间和系统资源;而线程共享其所属进程的地址空间和资源。

  • 独立性:进程具有独立性,进程间的通信和同步较为复杂;而线程间的通信和同步相对简单。

  • 开销:进程的创建、销毁和切换开销较大;而线程的创建、销毁和切换开销较小。

联系:

  • 包含关系:一个进程可以包含多个线程,这些线程共享进程的资源。

  • 调度关系:进程是操作系统调度的基本单位,但线程也是操作系统调度的基本单位。操作系统通常以线程为单位进行调度和执行。

  • 并发执行:进程和线程都可以实现并发执行,但线程间的并发执行通常更为高效。

# 多进程

举个例子,比如下载两个文件:

from random import randint
from time import time,sleep

def download_file(filename):
    print(f'start download {filename} ...')
    time_to_download = randint(5,10)
    sleep(time_to_download)
    print(f'{filename} download done, consume {time_to_download} seconds')


def main():
    start_at = time()
    download_file('file_a')
    download_file('file_b')
    end_at = time()

    print(f'total need {end_at - start_at}')


if __name__ == '__main__':
    main()

可以看出,代码是从上至下执行,并且需要等下载好一个文件,另外一个文件才会开始下载,这样做没有效率,下面使用多进程进行优化。

from multiprocessing import Process
from os import getpid
from random import randint
from time import time,sleep


def download_file(filename):
    pid = getpid()
    print(f'process id is {pid}')
    print(f'start download {filename} ...')
    time_to_download = randint(5,10)
    sleep(time_to_download)
    print(f'{filename} download done, consume {time_to_download} seconds')



def main():
    start_at = time()
    p1 = Process(target=download_file,args=('filename_a',))
    p1.start()

    p2 = Process(target=download_file,args=('filename_b',))
    p2.start()
    
    p1.join()
    p2.join()

    end_at = time()

    print(f'total need {end_at - start_at}')


if __name__ == '__main__':
    main()

猜猜下面的列子会输出什么?为什么?

from multiprocessing import Process
from time import time,sleep

counter = 0

def sub_task(string):
    global counter
    while counter < 10:
        print(string,end='',flush=True)
        counter += 1
        sleep(0.01)

def main():
    Process(target=sub_task,args=('Ping',)).start()
    Process(target=sub_task,args=('Pong',)).start()

if __name__ == '__main__':
    main()

最后的结果是Ping和Pong各输出了10个,当我们在程序中创建进程的时候,子进程复制了父进程及其所有的数据结构,每个子进程有自己独立的内存空间,这也就意味着两个子进程中各有一个counter变量,所以结果也就可想而知了,要想解决这个问题就需要进程间通信。

# 多线程

尝试使用多线程进行文件下载

from random import randint
from threading import Thread
from time import time,sleep

def download_file(filename):
    print(f'start download {filename} ...')
    time_to_download = randint(5,10)
    sleep(time_to_download)
    print(f'{filename} download done, consume {time_to_download} seconds')

def main():
    start_at = time()
    t1 = Thread(target=download_file,args=('filenameA',))
    t1.start()

    t2 = Thread(target=download_file,args=('filenameB',))
    t2.start()

    t1.join()
    t2.join()

    end_at = time()
    print(f'total need {end_at - start_at}')


if __name__ == '__main__':
    main()

有个有趣的列子,搞100个线程,分别往一个账户里充1块钱,看看最后账户余额是多少。

from time import sleep
from threading import Thread


class Account(object):

    def __init__(self):
        self._balance = 0

    def deposit(self, money):
        # 计算存款后的余额
        new_balance = self._balance + money
        # 模拟受理存款业务需要0.01秒的时间
        sleep(0.01)
        # 修改账户余额
        self._balance = new_balance

    @property
    def balance(self):
        return self._balance


class AddMoneyThread(Thread):

    def __init__(self, account, money):
        super().__init__()
        self._account = account
        self._money = money

    def run(self):
        self._account.deposit(self._money)


def main():
    account = Account()
    threads = []
    # 创建100个存款的线程向同一个账户中存钱
    for _ in range(100):
        t = AddMoneyThread(account, 1)
        threads.append(t)
        t.start()
    # 等所有存款的线程都执行完毕
    for t in threads:
        t.join()
    print('账户余额为: ¥%d元' % account.balance)


if __name__ == '__main__':
    main()

运行了之后竟然不是100块,之所以出现这种情况是没有对银行账户这个“临界资源”加以保护,多个线程同时向账户中存钱时,多个线程得到的账户余额都是初始状态下的0,所以都是0上面做了+1的操作,因此得到了错误的结果。在这种情况下,“锁”就可以派上用场了。我们可以通过“锁”来保护“临界资源”,只有获得“锁”的线程才能访问“临界资源”,而其他没有得到“锁”的线程只能被阻塞起来,直到获得“锁”的线程释放了“锁”,其他线程才有机会获得“锁”,进而访问被保护的“临界资源”。

from time import sleep
from threading import Thread, Lock


class Account(object):

    def __init__(self):
        self._balance = 0
        self._lock = Lock()

    def deposit(self, money):
        # 先获取锁才能执行后续的代码
        self._lock.acquire()
        try:
            new_balance = self._balance + money
            sleep(0.01)
            self._balance = new_balance
        finally:
            # 在finally中执行释放锁的操作保证正常异常锁都能释放
            self._lock.release()

    @property
    def balance(self):
        return self._balance


class AddMoneyThread(Thread):

    def __init__(self, account, money):
        super().__init__()
        self._account = account
        self._money = money

    def run(self):
        self._account.deposit(self._money)


def main():
    account = Account()
    threads = []
    for _ in range(100):
        t = AddMoneyThread(account, 1)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()
    print('账户余额为: ¥%d元' % account.balance)


if __name__ == '__main__':
    main()