Linux下的并发编程:多进程与多线程编程

重要概念

进程(Process)

进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位。每个进程都有自己独立的地址空间,包括文本区域(程序代码)、数据区域(变量和进程运行中使用的动态分配的内存)和堆栈(用于存放函数参数、局部变量等)。进程间相互独立,一个进程崩溃不会直接影响到其他进程。但是进程间通信(IPC)需要特定的OS支持。

进程有几个关键特性:

  • 独立性:每个进程在操作系统中相互独立,有自己的地址空间。
  • 动态性:进程是动态产生、变化和消亡的。
  • 并发性:多个进程可以并发执行。

线程(Thread)

线程,有时被称为轻量级进程,是进程的执行单元。一个进程中可以包含多个线程,它们共享进程的地址空间和资源,但每个线程有自己的执行序列(即线程执行的代码)、程序计数器、寄存器集合和栈。线程之间的信息共享和通信更为容易,但需要注意同步和互斥问题,以避免竞态条件。

线程的关键特性包括:

  • 轻量级:线程的创建和上下文切换比进程更快、更高效。
  • 共享性:线程共享所属进程的资源,如内存和文件。
  • 独立性:每个线程有自己的执行路径和状态。

IPC(Inter-Process Communication)

IPC,即进程间通信,是指在不同进程之间传递数据或信号的机制。由于进程间相互独立,它们不能直接访问对方的地址空间,IPC提供了一种安全的方法来交换数据,包括管道(pipe)、信号(signal)、共享内存(shared memory)、消息队列(message queue)、信号量(semaphore)等机制。

IPC的主要目的是:

  • 数据共享:不同进程之间共享信息。
  • 任务协作:多个进程协同完成一项任务。
  • 资源共享:不同进程共享系统资源,如打印机等。
  • 进程控制:一个进程可以启动和控制另一个进程的执行。

关键函数

fork() 函数

fork() 是 Unix 和 Linux 操作系统中用于创建新进程的系统调用。调用 fork() 会创建一个新的进程,这个新进程被称为子进程,它是调用进程(父进程)的副本。子进程和父进程会继续从 fork() 调用之后的位置开始执行。fork() 在父进程中返回新创建的子进程的进程ID,在子进程中返回0。如果出错,fork() 会在父进程中返回一个负值。

fork() 创建的新进程拥有父进程数据段、堆和栈的副本,但是这两个进程的这些部分在物理内存中是独立的。父子进程只共享代码段。

用法示例:

c 复制代码
pid_t pid = fork();

if (pid == -1) {
    // 错误处理
} else if (pid > 0) {
    // 父进程代码
} else {
    // 子进程代码
}

pthread_create() 函数

pthread_create() 是 POSIX 线程库中用于创建新线程的函数。POSIX 线程,或 "pthread",是一个可移植的线程标准,定义了线程的创建、控制和终止等操作。pthread_create() 调用会创建一个新的线程并将其加入到当前进程中。新线程从指定的函数地址开始执行。

pthread_create() 函数需要4个参数:一个指向线程标识符的指针、一个指定线程属性的指针(通常设置为NULL,表示默认属性)、线程函数的起始地址以及传递给线程函数的参数。线程函数通常有一个指向 void 的指针作为参数,返回一个指向 void 的指针。

用法示例:

c 复制代码
void *thread_function(void *arg) {
    // 线程执行的代码
}

pthread_t thread_id;
int result;

result = pthread_create(&thread_id, NULL, thread_function, (void*) arg);

if (result != 0) {
    // 错误处理
}

总结

  • fork() 用于创建一个与当前进程几乎完全相同的新进程。它们之间的主要区别在于PID和一些资源统计数据。
  • pthread_create() 用于在同一个进程内创建一个新线程,这些线程共享进程的地址空间和资源,但拥有独立的执行路径和堆栈。
  • 在使用这些函数时,特别要注意对返回值的检查,以便正确处理错误情况。在多线程编程中还需要考虑线程之间的同步和数据一致性问题。

用多进程实现累加和计算

要使用多进程实现累加和计算,我们可以使用 fork() 来创建子进程,每个子进程负责计算数列中一部分的和。主进程(父进程)等待所有子进程完成计算,并收集它们的计算结果来得到最终的累加和。

在这个示例中,我们将通过管道(pipe)来实现父子进程间的通信,以便子进程可以将其计算结果发送回父进程。

以下是一个简单的实现:

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/time.h>

#define NUM_PROCESSES 10 // 定义子进程数量

// 累加函数,每个子进程执行的代码
long sumRange(int start, int end) {
    long sum = 0;
    for (int i = start; i <= end; ++i) {
        sum += i;
    }
    return sum;
}

int main() {
    int numbersPerProcess = 1000000000 / NUM_PROCESSES;
    int pipefds[2 * NUM_PROCESSES]; // 为每个子进程创建一个管道
    pid_t pids[NUM_PROCESSES];
    struct timeval start, end;

    // 获取开始时间
    gettimeofday(&start, NULL);

    // 创建管道
    for (int i = 0; i < NUM_PROCESSES; ++i) {
        if (pipe(pipefds + i*2) == -1) {
            perror("pipe");
            exit(EXIT_FAILURE);
        }
    }

    // 创建子进程
    for (int i = 0; i < NUM_PROCESSES; ++i) {
        pids[i] = fork();
        if (pids[i] < 0) {
            perror("fork");
            exit(EXIT_FAILURE);
        }

        if (pids[i] == 0) { // 子进程
            close(pipefds[i*2]); // 关闭读端

            int start = i * numbersPerProcess + 1;
            int end = (i + 1) * numbersPerProcess;
            long partialSum = sumRange(start, end);

            write(pipefds[i*2 + 1], &partialSum, sizeof(partialSum)); // 写入计算结果
            close(pipefds[i*2 + 1]); // 关闭写端

            exit(EXIT_SUCCESS);
        }
    }

    // 父进程
    long totalSum = 0, readSum = 0;

    // 等待子进程并读取其计算结果
    for (int i = 0; i < NUM_PROCESSES; ++i) {
        close(pipefds[i*2 + 1]); // 关闭写端
        read(pipefds[i*2], &readSum, sizeof(readSum));
        totalSum += readSum;
        close(pipefds[i*2]); // 关闭读端

        wait(NULL); // 等待子进程结束
    }

    // 获取结束时间
    gettimeofday(&end, NULL);

    // 计算并打印执行时间
    long seconds = end.tv_sec - start.tv_sec;
    long micros = ((seconds * 1000000) + end.tv_usec) - (start.tv_usec);

    printf("NUM_PROCESSES = %d\n", NUM_PROCESSES);
    printf("Total Sum = %ld\n", totalSum);
    printf("Time taken: %ld microseconds (%.3f seconds)\n", micros, micros / 1000000.0);

    return 0;
}

在这个示例中:

  1. 我们首先为每个子进程创建了一个管道,以便它们可以将计算结果发送回父进程。
  2. 对于每个子进程,我们计算了一部分范围内的数字和,并将结果写入管道的写端。
  3. 在父进程中,我们关闭了管道的写端,从每个管道的读端读取子进程的计算结果,并将这些结果相加以得到最终的累加和。
  4. 父进程等待所有子进程完成,确保所有资源得到妥善处理。

单进程和多进程对比

单进程

多进程

用多线程实现累加和计算

使用多线程实现累加和计算,我们可以将要累加的数值范围分割给多个线程,每个线程计算自己那部分的和,最后将所有线程的结果累加起来得到最终结果。这里,我们将使用 POSIX 线程库(pthread)来实现这个多线程累加和计算。

以下是一个简单的实现方案:

  1. 定义一个结构体来传递给每个线程,这个结构体包含了线程需要知道的信息,比如计算的起始和结束值。
  2. 创建多个线程,每个线程负责计算一部分数值的和。
  3. 等待所有线程完成,然后汇总每个线程计算的结果。
  4. 输出最终的累加和

示例代码

c 复制代码
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>

#define NUM_THREADS 10 // 定义线程数量

typedef struct {
    int start;
    int end;
    long sum; // 用于存储这个线程计算的部分和
} ThreadData;

// 线程函数
void* sumRange(void* arg) {
    ThreadData* data = (ThreadData*)arg;
    data->sum = 0;
    for (int i = data->start; i <= data->end; ++i) {
        data->sum += i;
    }
    pthread_exit((void*) &(data->sum));
}

int main() {
    pthread_t threads[NUM_THREADS];
    ThreadData threadData[NUM_THREADS];
    int numbersPerThread = 1000000000 / NUM_THREADS;
    long totalSum = 0;

    struct timeval start, end;

    // 获取开始时间
    gettimeofday(&start, NULL);

    // 创建线程
    for (int i = 0; i < NUM_THREADS; ++i) {
        threadData[i].start = i * numbersPerThread + 1;
        threadData[i].end = (i + 1) * numbersPerThread;
        pthread_create(&threads[i], NULL, sumRange, (void*)&threadData[i]);
    }

    // 等待线程完成并汇总结果
    for (int i = 0; i < NUM_THREADS; ++i) {
        void* status;
        pthread_join(threads[i], &status);
        totalSum += *(long*)status;
    }

    // 获取结束时间
    gettimeofday(&end, NULL);

    // 计算并打印执行时间
    long seconds = end.tv_sec - start.tv_sec;
    long micros = ((seconds * 1000000) + end.tv_usec) - (start.tv_usec);

    printf("NUM_THREADS = %d\n", NUM_THREADS);
    printf("Total Sum = %ld\n", totalSum);
    printf("Time taken: %ld microseconds (%.3f seconds)\n", micros, micros / 1000000.0);

    return 0;
}

单线程和多线程结果对比

单线程

多线程

相关推荐
2201_7611990441 分钟前
nginx 负载均衡1
linux·运维·服务器·nginx·负载均衡
suri ..1 小时前
【Linux】进程第三弹(虚拟地址空间)
linux·运维·服务器
害羞的白菜1 小时前
Nginx基础详解5(nginx集群、四七层的负载均衡、Jmeter工具的使用、实验验证集群的性能与单节点的性能)
linux·运维·笔记·jmeter·nginx·centos·负载均衡
纪伊路上盛名在1 小时前
如何初步部署自己的服务器,达到生信分析的及格线
linux·运维·服务器·python·学习·r语言·github
Betty’s Sweet1 小时前
[Linux]:线程(三)
linux·线程·信号量·生产者消费者模型
程序员南飞3 小时前
ps aux | grep smart_webrtc这条指令代表什么意思
java·linux·ubuntu·webrtc
StrokeAce3 小时前
linux桌面软件(wps)内嵌到主窗口后的关闭问题
linux·c++·qt·wps·窗口内嵌
热爱嵌入式的小许7 小时前
Linux基础项目开发1:量产工具——显示系统
linux·运维·服务器·韦东山量产工具
韩楚风11 小时前
【linux 多进程并发】linux进程状态与生命周期各阶段转换,进程状态查看分析,助力高性能优化
linux·服务器·性能优化·架构·gnu
陈苏同学11 小时前
4. 将pycharm本地项目同步到(Linux)服务器上——深度学习·科研实践·从0到1
linux·服务器·ide·人工智能·python·深度学习·pycharm