菜鸟笔记
提升您的技术认知

线程进程以及多线程多进程

前言

线程和多进程是实现并发执行的两种常见方式,它们在操作系统和编程领域中被广泛使用。

一、什么是进程和线程

进程 (Process)

  • 定义:进程是程序执行时的一个实例。在计算机中,每个进程都有自己独立的内存空间,包括代码、数据和堆栈等,同时拥有一组系统资源,如文件描述符、信号、处理器状态等。
  • 特点:
    • 独立性:每个进程在执行过程中都是独立的,一个进程的崩溃不会影响其他进程。
    • 资源分配:操作系统为每个进程分配独立的内存空间和资源,进程间相互隔离。
    • 可调度性:操作系统可以对进程进行调度,分配处理器时间片,以实现多任务并发执行。

线程 (Thread)

  • 定义:线程是进程内的一个执行单元,是操作系统能够进行运算调度的最小单位。一个进程可以包含多个线程,它们共享进程的资源,如内存、文件等。
  • 特点:
    • 共享资源:同一进程内的线程共享进程的地址空间和系统资源,可以直接访问共享数据。
    • 轻量级:相对于进程来说,线程的创建和切换开销较小,可以更高效地实现并发。
    • 并发性:多个线程可以在同一进程内并发执行,提高程序的响应速度和资源利用率。

多线程 (Multithreading)

  • 定义:多线程是指在单个进程内同时执行多个线程的机制。一个进程可以包含多个线程,每个线程独立执行不同的任务,共享进程的资源。
  • 特点:
    • 轻量级:线程是进程内的一个执行单元,相对于进程来说创建和切换开销较小。
    • 共享资源:同一进程内的线程共享地址空间、文件描述符等进程资源,可以直接访问共享数据。
    • 通信简便:线程之间的通信和同步相对较为简单,可以使用共享内存、信号量、互斥锁等机制。

多进程 (Multiprocessing)

  • 定义:多进程是指在操作系统中同时执行多个独立的进程,每个进程有自己独立的地址空间和资源。
  • 特点:
    • 独立性:每个进程拥有独立的内存空间和资源,一个进程的崩溃不会影响其他进程。
    • 进程间通信:进程间通信需要借助操作系统提供的机制,如管道、消息队列、共享内存等。
    • 开销较大:与线程相比,创建和切换进程的开销较大,因为进程需要分配独立的内存空间。

相互组合关系

  • 单进程单线程:一个进程中只有一个线程,所有任务由这个单线程执行。
+-------------------+
|    进程 A         |
|  +-------------+  |
|  |   线程 A1   |  |  <- 单线程
|  +-------------+  |
+-------------------+
  • 单进程多线程:一个进程中包含多个线程,这些线程可以并发执行,但共享进程的资源。
+-------------------+
|    进程 B         |
|  +-------------+  |
|  |   线程 B1   |  |  
|  +-------------+  |
|  +-------------+  |
|  |   线程 B2   |  |  
|  +-------------+  |
|  +-------------+  |
|  |   线程 B3   |  |  <- 多线程
|  +-------------+  |
+-------------------+
  • 多进程单线程:每个进程都有自己独立的资源和内存空间,每个进程中只有一个线程。
+-------------------+
|    进程 C         |
|  +-------------+  |
|  |   线程 C1   |  |  <- 单线程
|  +-------------+  |
+-------------------+

+-------------------+
|    进程 D         |
|  +-------------+  |
|  |   线程 D1   |  |  <- 单线程
|  +-------------+  |
+-------------------+
  • 多进程多线程:每个进程都有自己独立的资源和内存空间,每个进程内部包含多个线程,这些线程可以并发执行并共享进程资源。
+-------------------+
|    进程 E         |
|  +-------------+  |
|  |   线程 E1   |  |  
|  +-------------+  |
|  +-------------+  |
|  |   线程 E2   |  |  <- 多线程
|  +-------------+  |
+-------------------+

+-------------------+
|    进程 F         |
|  +-------------+  |
|  |   线程 F1   |  |  
|  +-------------+  |
|  +-------------+  |
|  |   线程 F2   |  |  <- 多线程
|  +-------------+  |
|  +-------------+  |
|  |   线程 F3   |  |  
|  +-------------+  |
+-------------------+

二、资源分配

进程

私有资源
  • 地址空间:每个进程都有自己独立的地址空间,这包括代码段、数据段、堆和堆栈。
  • 文件描述符表:每个进程有自己的文件描述符表,用于管理打开的文件和I/O设备。
  • 信号处理:每个进程有自己的信号处理机制,独立于其他进程。
  • 安全属性:每个进程有自己的用户ID和权限信息。
共享资源
  • 共享内存:尽管进程之间通常是独立的,但可以通过共享内存区段进行通信。
  • 信号量:用于进程间同步,可以在多个进程之间共享。
  • 消息队列:一种进程间通信机制,允许进程间发送和接收消息。

线程

私有资源
  • :每个线程有自己的栈,用于存储局部变量、函数调用信息等。
  • 寄存器:每个线程有自己独立的寄存器状态,包括程序计数器(PC)。
  • 线程控制块(TCB):每个线程有自己的控制信息,如线程ID、状态、优先级等。
共享资源
  • 地址空间:同一进程内的所有线程共享相同的地址空间,包括代码段、数据段和堆。
  • 全局变量:所有线程可以访问同一进程的全局变量。
  • 文件描述符表:每个线程可以访问和操作同一进程的文件描述符表。
  • 动态内存:线程共享同一进程的堆区,可以通过动态内存分配(malloc/free)进行共享数据的读写。

多进程

私有资源
  • 独立的地址空间:每个进程有各自独立的地址空间和所有相关资源。
  • 文件描述符表:每个进程维护自己的文件描述符表。
共享资源
  • 共享内存:多进程可以通过共享内存区块进行通信。
  • IPC机制:包括管道、消息队列、信号量等。

多线程

私有资源
  • 线程栈:每个线程有自己的栈。
  • 线程寄存器:每个线程有自己的寄存器状态。
共享资源
  • 进程地址空间:所有线程共享进程的整个地址空间,包括代码段、数据段和堆。
  • 文件描述符表:所有线程共享同一进程的文件描述符表。
  • 全局变量:所有线程可以访问和修改进程的全局变量。

进程的共享和私有资源

// 进程 A
int main() {
    int a = 10; // 局部变量(私有)
    int *shared_mem = (int *)shmget(key, size, IPC_CREAT | 0666); // 共享内存(共享)
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        shared_mem[0] = 20; // 修改共享内存中的值
        exit(0);
    } else {
        wait(NULL); // 等待子进程结束
        printf("Shared memory value: %d\n", shared_mem[0]); // 应该输出20
    }
    return 0;
}

线程的共享和私有资源

#include <pthread.h>
#include <stdio.h>

int global_var = 0; // 全局变量(共享)

void *thread_func(void *arg) {
    int local_var = 5; // 局部变量(私有)
    global_var++; // 修改全局变量
    printf("Local variable: %d\n", local_var);
    printf("Global variable: %d\n", global_var);
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, thread_func, NULL);
    pthread_create(&tid2, NULL, thread_func, NULL);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    printf("Final global variable: %d\n", global_var); // 应该输出2
    return 0;
}

三、多进程和多线程间访问资源的保护

多进程中的资源保护

  1. 使用进程间通信(IPC)机制

    • 信号量(Semaphore):通过信号量实现进程对共享资源的互斥访问控制,确保同一时刻只有一个进程可以访问共享资源。
    • 互斥锁:在共享内存中使用互斥锁来保护临界区域,只有成功获取锁的进程才能进入临界区域执行操作。
    • 条件变量:结合互斥锁使用,实现进程间的同步,确保在特定条件下才能访问或修改共享资源。
  2. 使用文件锁

    如果共享资源是文件或文件区域,可以使用文件锁(如 fcntl() 函数提供的锁机制)来确保同一时刻只有一个进程可以对文件进行读写操作。

  3. 共享内存管理

    通过分配和释放共享内存的方式,确保只有一个进程可以修改内存区域,或者使用特定的同步机制来协调多个进程对共享内存的访问。

多线程中的资源保护

  1. 互斥锁(Mutex)

    最常用的方法是使用互斥锁来保护临界区域,确保同一时刻只有一个线程可以进入临界区域执行操作。线程在进入临界区前必须先获取锁,并在退出时释放锁。

  2. 条件变量

    用于在线程间进行通信和同步,例如等待某个条件为真时才继续执行,可以防止竞态条件的发生。

  3. 读写锁(Read-Write Lock)

    如果多个线程需要读取共享资源,但很少修改它,可以使用读写锁来允许多个线程同时读取,但只有一个线程可以写入。

  4. 原子操作

    使用原子操作可以保证某些简单操作的原子性,如递增操作等,这样可以避免竞态条件的发生。

多线程示例

在多线程场景下,可以使用标准库提供的 std::mutexstd::thread 来实现资源的保护。确保多个线程不会同时修改这个共享变量

#include <iostream>
#include <thread>
#include <mutex>

const int NUM_THREADS = 2;
const int MAX_COUNT = 100000;

struct SharedData {
    int shared_resource;
    std::mutex mtx;
};

void worker(SharedData& data) {
    for (int i = 0; i < MAX_COUNT; ++i) {
        // 每个线程在执行 worker 函数时,会自动对 data.mtx 进行加锁和解锁,从而保证线程安全。
        std::lock_guard<std::mutex> lock(data.mtx); // 使用互斥量保护临界区域
        data.shared_resource++;
    }
}

int main() {
    SharedData shared_data;
    shared_data.shared_resource = 0;

    std::thread threads[NUM_THREADS];
    for (int i = 0; i < NUM_THREADS; ++i) {
        threads[i] = std::thread(worker, std::ref(shared_data));
    }

    for (int i = 0; i < NUM_THREADS; ++i) {
        threads[i].join();
    }

    std::cout << "Final value of shared_resource: " << shared_data.shared_resource << std::endl;

    return 0;
}

多进程示例

在Linux下,可以使用命名信号量(named semaphore)来实现多进程间的资源保护。

#include <iostream>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <semaphore.h>

const char* SEM_NAME = "/my_semaphore";
const int NUM_PROCESSES = 2;
const int MAX_COUNT = 100000;

struct SharedMemory {
    int shared_resource;
    sem_t mutex;
};

int main() {
    // 使用 shm_open 和 mmap 创建共享内存,使得多个进程可以访问同一块内存区域。
    int shm_fd = shm_open("/my_shared_memory", O_CREAT | O_RDWR, 0666);
    ftruncate(shm_fd, sizeof(SharedMemory));
    SharedMemory* shared_memory = (SharedMemory*)mmap(NULL, sizeof(SharedMemory), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
    //初始化一个命名信号量 mutex,用于保护 shared_resource 的访问。
    sem_init(&(shared_memory->mutex), 1, 1); // 初始化互斥信号量

    pid_t pid;
    for (int i = 0; i < NUM_PROCESSES; ++i) {
        pid = fork();
        if (pid == 0) { // 子进程
            for (int j = 0; j < MAX_COUNT; ++j) {
                // 每个子进程通过 sem_wait 和 sem_post 来对 mutex 进行加锁和解锁,
                // 确保每次只有一个进程可以访问共享资源。
                sem_wait(&(shared_memory->mutex)); // 等待信号量
                shared_memory->shared_resource++;
                sem_post(&(shared_memory->mutex)); // 发送信号量
            }
            return 0;
        }
    }

    // 等待所有子进程结束
    for (int i = 0; i < NUM_PROCESSES; ++i) {
        wait(NULL);
    }

    std::cout << "Final value of shared_resource: " << shared_memory->shared_resource << std::endl;

    // 清理资源
    munmap(shared_memory, sizeof(SharedMemory));
    shm_unlink("/my_shared_memory");
    sem_destroy(&(shared_memory->mutex));

    return 0;
}

四、通信方式

多进程通信方式

多进程间通信(Inter-Process Communication, IPC)通常涉及在不同的进程之间传递数据或同步操作。以下是几种常见的多进程通信方式:

  1. 管道(Pipe)

    管道是Unix/Linux系统中最简单的IPC形式之一,它可以在两个相关的进程之间传递数据。管道通常是单向的,即只能用于单向数据流,但可以通过创建两个管道实现双向通信。

  2. 命名管道(Named Pipe)

    类似于管道,但可以通过文件系统进行命名和访问,允许无亲缘关系的进程之间进行通信。

  3. 消息队列(Message Queue)

    消息队列是一种通过消息传递进行通信的机制,允许进程通过向队列发送和接收消息来进行通信。

  4. 共享内存(Shared Memory)

    共享内存是进程间通信的高效方式,它允许多个进程访问同一个逻辑内存区域,实现快速的数据共享。(速度快,但要同步)

  5. 信号量(Semaphore)

    信号量是一种计数器,用于多进程间的同步,可以用来解决多个进程竞争有限资源的问题。

  6. 套接字(Socket)

    套接字不仅用于网络编程,也可以在同一台机器上的不同进程间进行通信,提供了灵活和强大的通信能力。

多线程通信方式

多线程通信通常发生在同一进程内的不同线程之间,以下是常见的多线程通信方式:

  1. 共享内存

    在多线程中,共享内存仍然是一种有效的通信方式,因为所有线程都可以直接访问共享的内存数据结构。

  2. 互斥锁(Mutex)

    互斥锁是最基本的线程同步机制,它可以保护临界区,确保同时只有一个线程可以访问共享资源。

  3. 条件变量(Condition Variable)

    条件变量允许一个线程在满足特定条件前进入休眠状态,直到其他线程通知条件变量满足了条件。

  4. 信号量

    信号量不仅可以用于多进程,也可以在多线程中使用,用于控制对有限资源的访问。

  5. 屏障(Barrier)

    屏障允许一组线程在达到某个点前全部等待,然后同时继续执行,用于同步多个线程的执行步骤。

  6. 线程安全队列

    特殊设计的数据结构,例如线程安全的队列,可以用来在线程间安全地传递数据。

五、总结

    多进程和多线程各有优劣,选择合适的并发模型取决于应用的需求、性能要求和安全考虑。在实际开发中,通常会根据具体的场景来决定是采用多进程还是多线程,有时甚至两者结合使用,以充分利用系统资源和提升应用程序的效率。

1.定义和概念

    多进程指的是同时运行多个独立的进程,每个进程有自己的地址空间,是系统分配资源和调度的基本单位。每个进程都有自己的内存空间,数据独立,通信通过进程间通信(IPC)来实现。

多线程是在同一个进程中同时运行多个线程,每个线程共享进程的地址空间和资源,是CPU调度的基本单位。线程之间共享相同的上下文,可以方便地共享数据和通信。

2. 内存和资源管理

  • 每个进程有独立的内存空间,相互之间不受影响,但创建和销毁进程的开销比较大,因为需要分配和释放独立的内存空间。进程间的通信比较复杂,需要使用IPC机制来实现,例如管道、消息队列、共享内存等。

  • 线程共享进程的内存空间,因此数据共享和通信比较容易和高效,但需要确保线程安全,避免数据竞争和死锁等问题。创建和销毁线程的开销相对较小,因为不需要像进程那样分配和释放独立的内存空间。

3. 切换和调度

  • 进程切换的开销比较大,因为涉及到不同进程间的上下文切换,需要保存和恢复更多的状态信息。进程之间的调度由操作系统负责,进程间的切换是通过操作系统的调度算法来实现的。

  • 线程切换的开销相对较小,因为线程共享同一进程的地址空间,上下文切换主要涉及寄存器值的保存和恢复。线程的调度可以由操作系统调度器完成,也可以通过线程库中的用户级调度器来实现。

4. 并发性和效率

  • 由于每个进程都有独立的内存空间,可以更好地利用多核处理器,适合CPU密集型任务。但进程间的通信成本较高,因此在需要频繁通信和协作的场景下效率可能较低。

  • 线程之间共享数据和内存空间,适合IO密集型任务和需要频繁通信的场景。但需要注意线程安全问题,合理设计和使用锁和同步机制,避免数据竞争和死锁。

5. 编程模型和适用场景

  • 多进程适合需要在不同进程间分配任务或者需要独立处理的任务,例如服务器架构中的分布式处理。可以更好地利用多核处理器,提高整体系统的并行性能。

  • 多线程适合需要实时性和相互协作的任务,例如GUI应用程序、Web服务器处理请求、多媒体应用等。可以简化数据共享和通信,提升程序的响应速度和用户体验。