C中进程与线程的基本使用(结合代码讲解)

Posted 我要出家当道士

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C中进程与线程的基本使用(结合代码讲解)相关的知识,希望对你有一定的参考价值。

目录

零、前言

一、进程

 1、基本介绍与基本使用 

2、僵尸进程的产生与解决

(1)原因

(2)解决办法

 二、线程

1、进程的不足

2、线程的基本介绍

3、线程的五个基本操作

(1)创建线程

(2)关闭线程

(3)获取自身线程的 ID 

(4)阻塞等待线程的退出并释放资源

(5)子线程脱离

(6)example(pthread_join主线程阻塞等待后释放资源)

(7)example(pthread_detach子线程结束后自动释放资源)

4、多线程的数据共享

(1)互斥锁的基本使用

(2)example(注意两种初始化互斥锁的方法)


零、前言

        一个简单的总结,内容也比较简单,不是很完整,其他的待日后补充,比如进程间的数据通讯方式、多线程的其他锁的实现和条件变量等等。文章中可能出现内容有异议的,欢迎评论区讨论。下面的内容每部分都结合了demo进行介绍,希望你能从中受益。文章中的内容主要来源于 《unix网络编程卷一》。

一、进程

 1、基本介绍与基本使用 

        使用fork函数创建了一个新的进程,新进程(子进程)与原有的进程(父进程)一模一样。子进程和父进程使用相同的代码段;子进程拷贝了父进程的堆栈段和数据段。子进程一旦开始运行,它复制了父进程的一切数据,各自运行,互不影响。而且由于子进程复制了父进程的堆栈段,所以子进程也复制了父进程的代码执行进度。

#include <unistd.h>
pid_t fork(void);

         fork 函数返回两个值,对于调用进程(父进程)返回新派生进程的进程 ID 号;对于子进程返回 0。使用 getpid 函数可以查看当前进程的 ID 号。

        子进程可以通过 getppid 函数来获取父进程的进程 ID 号;相反创建子进程后父进程无法再获得子进程的 ID号,唯一的方法就是在调用 fork 函数的时候保存返回值。父进程调用 fork 函数之前打开的所有描述符在 fork 函数返回后由子进程分享。

        进程间相互独立,互不影响。每个进程都有自己的地址空间,所以可能会出现,不同进程的变量虚拟地址相同的现象,其实他们指向的物理地址不同。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
 
int ii=10;
int main()

  int jj=20;
  if (fork()>0)
  
    ii=11;jj=21; sleep(1);  printf("父进程:ii=%d,jj=%d\\n",ii,jj);
  
  else
  
    ii=12;jj=22; sleep(1);  printf("子进程:ii=%d,jj=%d\\n",ii,jj);
  
  /*
      ffy@ffy:~/桌面/work/code$ ./a.out 
      父进程:ii=11,jj=21
      子进程:ii=12,jj=22
  */

2、僵尸进程的产生与解决

(1)原因

        在linux系统中可以通过 top 这个工具查看僵尸进程,如下图右上角的 zombie 即为僵尸进程的数量。

        a. 在子进程退出前父进程先退出,则系统会让init进程接管子进程;

        b. 当子进程先于父进程终止,而父进程又没有调用wait函数等待子进程结束,子进程进入僵尸状态,并且会一直保持下去除非主进程退出。子进程处于僵尸状态时,内核只保存该进程的一些必要信息以备父进程所需。此时子进程始终占用着资源,同时也减少了系统可以创建的最大进程数;

        c. 如果子进程先于父进程终止,且父进程调用了 wait 或 waitpid 函数,则父进程会等待子进程结束(阻塞等待)。

(2)解决办法

        a.  在Linux下,可以简单地将 SIGCHLD 信号的操作设为 SIG_IGN,这样当子进程结束时就不会称为僵尸进程。

signal(SIGCHLD,SIG_IGN);  // 忽略子进程退出的信号,避免产生僵尸进程
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char const *argv[])

    pid_t pid = fork();
    signal(SIGCHLD,SIG_IGN);
    if (pid == 0) 
        sleep(1);
     else 
        sleep(20)
    
    return 0;

        b. 主进程可以通过调用 pid_t wait(int *statloc) 来阻塞的等待子进程的退出。wait 函数将子进程终止时传递的参数值保存到 statloc 所指向的内存空间。然后通过宏 WIFEXITED(status) 确定子进程是否正常终止,通过宏 WEXITSTATUS(status) 获得子进程的返回值。

pid_t wait(int *statloc)
// statloc = &status
WIFEXITED(status)
WEXITSTATUS(status)
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char const *argv[])

    pid_t pid = fork();
    if (pid == 0) 
        sleep(15);
        return 3;
     else 
        int status;
        pid = wait(&status);
        printf("child process exited\\n");
        if (WIFEXITED(status)) 
            printf("child process return : %d\\n", WEXITSTATUS(status));
        
    
    return 0;

        c.  使用 waitpid,例如:waitpid(-1, &status, WNOHANG) ; -1 表示可以等待任意子进程终止,WNOHANG 表示子进程没有终止也不会进入阻塞状态,而是返回0并退出函数。

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char const *argv[])

    pid_t pid = fork();
    if (pid == 0) 
        sleep(15);
        return 3;
     else 
        int status;
        while (!waitpid(-1, &status, WNOHANG)) 
            printf("child process in running\\n");
            printf("do someting\\n");
        
        printf("child process exited\\n");
        if (WIFEXITED(status)) 
            printf("child process return : %d\\n", WEXITSTATUS(status));
        
    
    return 0;

 二、线程

1、进程的不足

        (1)、不断的生成子进程占用大量资源,因为每个子进程都要把父进程的内存映像复制到子进程中去,并在子进程中复制所有的描述符等。也可以说子进程和父进程完全一样,连程序的执行进度都完全一样,拥有独立的内存空间。

        (2)、子进程生成后会复制父进程的所有数据,包括调用fork之前的数据。所以父进程向尚未创建的子进程传递信息很容易,但子进程向父进程传递返回消息十分的吃力,需要借助进程间通讯机制。

2、线程的基本介绍

        线程是轻量级的进程,创建速度更快。同一个进程内的线程共享相同的全局内存,这使得线程间共享信息十分方便,但要面临同步的问题。

        下面是线程共享与独有的资源。

共享

独有

进程指令

线程ID

描述符

信号与信号处理函数

寄存器集合(程序计数器和栈)

用户ID与组ID

errno

大多数数据

信号掩码

优先级

3、线程的五个基本操作

(1)创建线程

#include <pthread.h>
int pthread_create(pthread_t *tid, const pthread_attr_t *attr,
                    void *(*func)(void *), void *arg);

        程序正常启动后会自动创建主线程,其余线程才由 pthread_create 创建。每个线程由线程ID(thread ID)标识,其数据类型是 pthread_t(unsigned int)。

        每个线程都有很多属性:优先级、初始栈大小、是否是守护进程等,可以通过 attr 这个参数传递,默认情况下传递空指针。

        最后指导线程的执行函数 func 和该函数的唯一调用参数 arg,如果需要传递多个参数可以打包成结构体传值。func 传入参数和返回值都是通用指针(void *)。

        pthread_create 成功创建线程返回 0,不成功返回 非0(正数)。

(2)关闭线程

        线程退出的两种方式,一种是子线程执行函数的退出(隐式、显示);另一种是主线程的退出(程序进程结束,其附属的线程自然结束)。

        子线程的退出分为显示和隐式退出,隐式退出即线程的执行函数执行 return 返回;显示的退出即调用 pthread_exit。

void pthread_exit(void *status);

(3)获取自身线程的 ID 

pthread_t pthread_self(void)

(4)阻塞等待线程的退出并释放资源

        通过给定线程 ID,阻塞的等待指定线程的终止,并释放该线程的资源。例如主线程阻塞的等待子线程的关闭,待子线程退出后释放其资源。

#include <pthread.h>
int pthread_join(pthread_t *tid, void **status);

(5)子线程脱离

        pthread_detach 函数,unix网络编程中解释是变为脱离状态(detach),脱离后该线程执行完毕会自动释放资源。如果是可汇合状态(joinable),该线程结束后需要另一个线程执行 pthread_join 来释放其资源。

        线程有两个状态:joinable 状态和 detach 状态。这个状态可以在 pthread_create 创建线程时设置,默认是 joinable。也就是默认需要你执行 pthread_join() 函数来阻塞的等待子线程的退出,以释放其资源。或者你可以在子线程中使用 pthread_detach(pthread_self()) 函数来改变自身线程的状态为 detach,这样当子线程执行结束后会自动的释放资源。

(6)example(pthread_join主线程阻塞等待后释放资源)

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

void * print(void *arg)

    pthread_t id = pthread_self();
    printf("pthread id : %lu\\n", id);
    int start = *(int *)arg;
    for (int i = start; i < start + 10; i++)
    
        sleep(1);
        printf("%d \\n", i);
        // 主动终止
        if ( 0 == i % 7) 
            pthread_exit(NULL);
    
    return NULL;


int main(void)

    pthread_t tid1, tid2;
    // 定义线程 ID,创建线程,并指定执行函数与参数。
    int start1 = 1;
    pthread_create(&tid1, NULL, print, &start1);
    int start2 = 10;
    pthread_create(&tid2, NULL, print, &start2);
    // 主线程阻塞的等待线程的终止, 即两个线程都执行完毕后主线程执行之后的步骤。
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;

(7)example(pthread_detach子线程结束后自动释放资源)

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

void * print(void *arg)

    pthread_detach(pthread_self());
    int start = *(int *)arg;
    for (int i = start; i < start + 10; i++)
    
        sleep(1);
        printf("%d \\n", i);
        if ( 0 == i % 7) 
            pthread_exit(NULL);
    
    return NULL;


int main(void)

    pthread_t tid1, tid2;
    int start1 = 1;
    pthread_create(&tid1, NULL, print, &start1);
    int start2 = 10;
    pthread_create(&tid2, NULL, print, &start2);
    sleep(10);
    printf("over\\n");
    return 0;

4、多线程的数据共享

        为了保护多线程共享的数据,可以使用互斥锁。任何线程使用共享的数据时必须持有该锁(上锁),其他线程再试图上锁时会被阻塞,直到该数据被持有线程使用完后解锁。

        互斥锁的类型是 pthread_metux_t,互斥锁变量存在两种初始化方式:静态分配和动态分配。

        静态分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZer;

        动态分配

pthread_mutex_init(pthread_mutex_t *mutex, 
                    const pthread_mutex_attr_t *mutexattr);
// mutexattr用于指定锁的属性,默认是普通锁

(1)互斥锁的基本使用

#include <pthread.h>
// 阻塞时加锁
int pthread_mutex_lock(pthread_mutex_t *mptr);
// 非阻塞式加锁
int pthread_mutex_trylock(pthread_mutex_t *mptr);
// 释放锁
int pthread_mutex_unlock(pthread_mutex_t *mptr);
// 销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mptr);

// 成功均返回 0
// 非阻塞式加锁在锁被其他线程占用时返回,不阻塞。
// 销毁锁时需确保锁处于 unlock 状态。

(2)example(注意两种初始化互斥锁的方法)

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
// 1 static
//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 2 dynamic
pthread_mutex_t mutex;
// the data to be protected
int count = 0;

void * print(void *arg)

    //pthread_detach(pthread_self());
    for (int i = 0; i < 10; i++)
    
        pthread_mutex_lock(&mutex);
        count++;
        printf("thread %lu -- count is : %d\\n", pthread_self(), count);
        pthread_mutex_unlock(&mutex);
    
    return NULL;


int main(void)

    // 2 dynamic
    pthread_mutex_init(&mutex, NULL);
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, print, NULL);
    pthread_create(&tid2, NULL, print, NULL);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_mutex_destroy(&mutex);
    printf("over\\n");
    return 0;

以上是关于C中进程与线程的基本使用(结合代码讲解)的主要内容,如果未能解决你的问题,请参考以下文章

多线程控制讲解与代码实现

C语言多线程教程

C语言多线程教程

啥是指针?指针怎么用?来看C指针快速入门(结合代码和图例讲解,通俗易懂)

C++11多线程第一篇:并发基本概念及实现,进程线程基本概念

C++11多线程第一篇:并发基本概念及实现,进程线程基本概念