Linux多进程与多线程使用规则

        在Linux系统中,多线程和多进程的使用涉及多个重要的规则和准则,这些规则确保了线程与进程的安全、有效和可预测的行为。

多线程

        1.创建线程:使用pthread——create函数来创建新的线程。该函数需要线程标识符的地址、线程属性(通常为空,表示使用默认属性)、线程运行函数的地址以及该函数的参数。

      

#include <stdio.h>  
#include <stdlib.h>  
#include <pthread.h>  
  
// 线程函数,每个线程将执行此函数  
void *thread_function(void *arg) {  
    long thread_id = (long)arg;  
    printf("Thread %ld is running\n", thread_id);  
      
    // 在这里添加线程要执行的代码  
      
    return NULL;  
}  
  
int main() {  
    pthread_t threads[5]; // 线程ID数组  
    int rc;  
    long t;  
  
    // 创建5个线程  
    for (t = 0; t < 5; t++) {  
        printf("Creating thread %ld\n", t);  
        rc = pthread_create(&threads[t], NULL, thread_function, (void *)t);  
        if (rc) {  
            printf("Error: return code from pthread_create() is %d\n", rc);  
            exit(-1);  
        }  
    }  
  
    // 等待所有线程完成  
    for (t = 0; t < 5; t++) {  
        pthread_join(threads[t], NULL);  
    }  
  
    printf("All threads completed.\n");  
  
    return 0;  
}

       这段代码演示了如何创建5个线程,并让它们并行执行。每个线程打印其ID,并在执行完毕后退出。主线程使用pthread_create函数创建线程,并传递线程函数(在本例中为thread_function)和线程参数(线程ID)。然后,主线程使用pthread_join函数等待每个线程完成。

        2.线程属性:线程属性对象pthread_attr_t用于设置和获取线程的各种属性,如栈大小、栈地址、调度策略等。在创建线程之前,可以使用pthread_attr_set*系列的函数来设置这些属性。

        3.线程同步:多线程程序中,线程间的同步非常重要。可以使用互斥锁、条件变量、读写锁、和信号量等同步机制来确保线程之间的正确交互。

        互斥锁:互斥锁是一种特殊的二值型信号量,用于实现对资源的独占,只有一个线程可以持有互斥锁并访问临界资源,其它线程必须等待释放锁才能访问。例如有一个共享变量count,多个线程需要对其进行加1操作。为了避免多个线程同时修改该变量,可以使用互斥锁来同步线程。线程在修改count之前先尝试获取锁,若锁被其它线程持有,则等待释放锁,如果成功获取锁,则执行加1操作后释放锁,其它线程便可以获取锁继续执行。

        条件变量:条件变量是多线程编程中用于实现线程间同步的一种机制。它通常与互斥锁一起使用,以允许一个或多个线程在某条件为真之前一直等待,或在某条件变为假时唤醒一个或多个正在等待的线程。比如当某个线程需要等待某个条件成立时,它可以锁定一个互斥锁,然后调用条件变量的等待函数,将自己置于等待状态,并释放互斥锁,以便其它线程可以执行并可能改变条件。   当条件变量被另一个线程通过调用其通知函数(如pthread_cond_signalpthread_cond_broadcast)来唤醒时,等待的线程将被唤醒并重新获取互斥锁,然后检查条件是否确实成立。

      

#include <stdio.h>  
#include <stdlib.h>  
#include <pthread.h>  
  
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;  
int ready = 0; // 共享条件变量  
  
void *producer(void *arg) {  
    pthread_mutex_lock(&mutex);  
      
    // 生产数据或执行某些操作  
    printf("Producer is ready to sell the data.\n");  
    ready = 1; // 设置条件为真  
      
    // 通知等待的消费者线程  
    pthread_cond_signal(&cond);  
      
    pthread_mutex_unlock(&mutex);  
    return NULL;  
}  
  
void *consumer(void *arg) {  
    pthread_mutex_lock(&mutex);  
      
    // 等待条件成立  
    while (!ready) {  
        pthread_cond_wait(&cond, &mutex);  
    }  
      
    // 消费数据或执行某些操作  
    printf("Consumer bought the data.\n");  
      
    pthread_mutex_unlock(&mutex);  
    return NULL;  
}  
  
int main() {  
    pthread_t producer_thread, consumer_thread;  
  
    // 创建生产者线程  
    if (pthread_create(&producer_thread, NULL, producer, NULL)) {  
        fprintf(stderr, "Error creating producer thread\n");  
        return 1;  
    }  
  
    // 创建消费者线程  
    if (pthread_create(&consumer_thread, NULL, consumer, NULL)) {  
        fprintf(stderr, "Error creating consumer thread\n");  
        return 1;  
    }  
  
    // 等待两个线程完成  
    pthread_join(producer_thread, NULL);  
    pthread_join(consumer_thread, NULL);  
  
    return 0;  
}

       

在这个示例中,生产者线程(producer)在准备好销售数据后,将ready变量设置为1,并通过pthread_cond_signal函数通知等待的消费者线程(consumer)。消费者线程在条件变量cond上等待,直到它被生产者线程唤醒。一旦条件成立(ready为1),消费者线程将执行消费数据的操作。

注意,在实际应用中,您应该始终确保在调用pthread_cond_wait之前锁定互斥锁,并在pthread_cond_wait返回后立即检查条件。这是因为pthread_cond_wait可能会由于虚假唤醒(spurious wakeup)而返回,即使没有线程调用pthread_cond_signalpthread_cond_broadcast。在上面的示例中,while循环用于处理这种情况,确保消费者线程在条件确实为真时才继续执行。

        自旋锁:自旋锁是一种特殊类型的锁,用于防止多处理器并发访问共享资源。与互斥锁不同,自旋锁在资源被占用时不会使线程进入睡眠状态,而是让线程在一个循环中不断检测锁是否被释放。如果锁已经被释放,线程立即获取锁并继续执行。例子:在一个多核处理器的系统中,有一个全局变量flag,多个线程需要对其进行检查并根据其值执行相应的操作。为了保证线程安全,可以使用自旋锁来保护flag的访问。线程在检查flag之前先尝试获取自旋锁,如果锁被其他线程持有,则当前线程在一个循环中不断检查锁的状态,直到获取到锁为止。一旦获取到锁,线程可以安全地检查flag并执行相应的操作,然后释放锁。

        读写锁:读写锁顾名思义,它分为共享锁(读锁)和排他锁(写锁),多个线程可以同时持有共享锁,但只有一个线程能持有排他锁。

        假设有一个共享数据结构data,多个线程需要对其进行读取和写入操作。为了保证线程安全,可以使用读写锁来控制对data的访问。当线程需要读取data时,它首先尝试获取共享锁。如果锁被其他线程持有(无论是共享锁还是排他锁),则当前线程等待;如果成功获取共享锁,则多个线程可以同时读取data。当线程需要写入data时,它尝试获取排他锁。如果锁被其他线程持有(无论是共享锁还是排他锁),则当前线程等待;如果成功获取排他锁,则只有一个线程可以写入data,并且其他线程无法获取共享锁或排他锁。

        4.避免数据竞争:数据竞争发生在两个或多个线程在没有同步的情况下访问同一数据。意思是每个线程都要尽量考虑它们的同步。

        5.线程安全函数:某些函数在多线程环境中可能不安全,因为它们可能修改全局变量或静态变量。在编写多线程程序时,应使用线程安全的函数或采取其它措施来避免这些问题。

        6.线程间通信:线程间可以通过共享内存、消息队列、管道、套接字等方式进行。由于线程共享相同的地址空间,因此可以直接访问和修改共享数据。

        7.避免使用信号和线程一起:不要将信号和线程一起使用,因为这可能导致程序动作的预测和调试变得困难。在信号处理函数中,只能执行那些被允许在信号处理函数中执行的函数,通常称为异步信号安全函数。

        8.线程撤销:在多线程程序中,不要尝试异步撤销线程,即不要试图从一个线程中立即终止另一个线程的执行。这可能导致未定义的行为和错误的结果。

多进程

        1.进程创建:使用fork()或clone()系统调用来创建新进程。fork()创建一个与父进程几乎完全相同的子进程,而clone()提供了更多的灵活性,允许共享某些资源。

        2.进程编号(PID):每个进程都有一个唯一的进程ID,用于标识和跟踪进程。可以使用getpid()函数获取当前进程的PID,使用getppid()获取父进程的PID。

         3.进程终止:进程可以通过调用exit()或_exit()函数来终止。exit()会执行一些清理操作,如关闭文件描述符和释放内存,然后返回给父进程一个推出码。_exit()则立即终止进程,不进行任何清理。

        4.进程状态:进程可以有多种状态如运行、休眠、僵尸等。可以使用wait()或waitpid()函数等待子进程终止,确保子进程被父进程回收,避免成为僵尸进程。

        5.进程间通信(IPC):进程间可以通过多种方式进行通信,如管道、消息队列、共享内存、信号量等。这些机制允许进程间交换数据或同步操作。

        管道(pipe):管道是最早进程间通信的方式之一。它允许一个进程向另一个进程发送字节流数据。管道常用于父子进程间通信。命名管道是管道的一种扩展,它在文件系统中有一个与之关联的名字。这使得不相关的进程之间也可以通过命名管道进行通信。

        消息队列(Message Queues):消息队列是一种消息的链接列表,进程可以向队列中添加或从中删除消息。消息队列允许一个进程向另一个进程发送格式化的消息。

        共享内存(Shared Memory):共享内存允许多个进程访问同一块内存区域。通过映射共享内存到各自的进程地址空间,进程可以直接读写共享内存中的数据。

        信号量(Semaphores):信号量是一种用于同步进程和保护共享资源的机制。它通常用于实现进程之间的互斥访问或同步操作。

        套接字(Sockets):虽然套接字通常用于不同计算机之间的网络通信,但它们也可以在同一台机器的不同进程之间进行通信。

        6.进程优先级和调度:Linux使用复杂的调度算法来管理进程的执行顺序。可以使用nice()和renice()函数来调整进程的优先级,影响其在CPU的执行顺序。

       7. 进程组和会话:进程可以属于一个进程组或会话,这些概念用于控制进程的行为和交互。例如,可以使用setsid()函数创建一个新的会话,并成为该会话的领导者。

        8.资源限制和隔离:Linux提供了多种机制来限制和隔离进程使用的资源,如CPU时间、内存使用量、文件描述符数量等。这有助于防止单个进程消耗过多的系统资源。

以下是一个简单的C语言代码示例,展示了如何在Linux环境中创建子进程,并通过进程间通信(IPC)来传递数据。这个例子中,父进程创建了一个子进程,并通过管道(pipe)向子进程发送了一条消息。

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <string.h>  
  
#define BUFFER_SIZE 1024  
  
int main() {  
    int fd[2]; // 文件描述符数组,用于管道  
    pid_t pid; // 进程ID  
    char buffer[BUFFER_SIZE]; // 用于存储消息的缓冲区  
  
    // 创建管道  
    if (pipe(fd) == -1) {  
        perror("pipe");  
        exit(EXIT_FAILURE);  
    }  
  
    // 创建子进程  
    pid = fork();  
    if (pid == -1) {  
        perror("fork");  
        exit(EXIT_FAILURE);  
    }  
  
    // 子进程  
    if (pid == 0) {  
        // 关闭管道的写端  
        close(fd[1]);  
  
        // 从管道的读端读取消息  
        read(fd[0], buffer, BUFFER_SIZE);  
        printf("Child process received: %s\n", buffer);  
  
        // 关闭管道的读端  
        close(fd[0]);  
        exit(EXIT_SUCCESS);  
    }  
  
    // 父进程  
    else {  
        // 关闭管道的读端  
        close(fd[0]);  
  
        // 向管道的写端发送消息  
        strcpy(buffer, "Hello from parent process!");  
        write(fd[1], buffer, strlen(buffer));  
  
        // 关闭管道的写端  
        close(fd[1]);  
  
        // 等待子进程结束  
        wait(NULL);  
        printf("Child process exited.\n");  
    }  
  
    return 0;  
}

这段代码首先创建了一个管道,然后创建了一个子进程。在子进程中,它关闭了管道的写端,然后从读端读取了父进程发送的消息并打印出来。在父进程中,它关闭了管道的读端,向写端发送了一条消息,然后等待子进程结束。

  小结

        多线程主要特定主要是是资源共享,通信简便、同步和互斥、开销小但受限于硬件,由于CPU核心数量有限,过多线程可能会导致性能下降。多进程的特点主要是资源隔离(安全性高)、通信机制(通过操作系统提供的机制)、开销大(创建和销毁)、扩展较强(不受限硬件的CPU核心数,适用于多核或多机并行计算)、独立性(一个进程崩溃并不影响其它进程)

        在实际应用中,选择多线程还是多进程取决于具体的需求和场景。如果任务之间需要频繁通信和共享大量数据,且对资源安全性要求不高,可以考虑使用多线程。如果任务之间相互独立,需要隔离资源,或者需要利用多核多机进行并行计算,则可以考虑使用多进程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值