C语言基本并发操作简介

引言

在处理一些数据集比较繁杂且解题思路比较单一的问题时,可以通过同一个算法计算多次循环操作解决,但当对效率要求比较高的时候,如果电脑的配置比较好,通常倾向于采用并发编程,其中,线程之间由于切换比较复杂,上下文切换时间较长,并且进程间数据共享比较麻烦,这里我们通常倾向于利用多线程的方法来解决问题。这里我们简要介绍一下多进程、线程思想的一些基本操作。

实验环境

本次讨论我们采用对C/C++编成比较方便的Ubuntu20.04.02(linux内核为5.8.0)操作系统。

利用fork函数创建进程

在UNIX系统中给出的创建新进程的API为fork函数,这个函数可以从当前进程中创建一个几乎一模一样的进程。

每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。

fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。子进程使用相同的pc(程序计数器),相同的CPU寄存器,在父进程中使用的相同打开文件。

它不需要参数并返回一个整数值。下面是fork()返回的不同值。

负值:创建子进程失败。

:返回到新创建的子进程。

正值:返回父进程或调用者。该值包含新创建的子进程的进程ID 。

创建实例

接下来是一段简单的fork函数的运用C代码:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
  pid_t cpid, mypid;
  int status=0;
  pid_t pid = getpid();            /* get current processes PID */
  printf("Parent pid: %d\n", pid);
  cpid = fork();
  if (cpid > 0) {            /* Parent Process */
    mypid = getpid();
    printf("[%d] parent of [%d]\n", mypid, cpid);
    printf("Parent exit\n");
  }  else if (cpid == 0) {       /* Child Process */
    mypid = getpid();
    printf("[%d] child\n", mypid);
    printf("Child exit\n");
  } else {
    perror("Fork failed");
    exit(1);
  }
  exit(0);
}

fork函数会返回两次,在父进程中,返回的是子进程的PID,在子进程中,返回值为0。如果fork失败(一般是由于内存空间等资源不足无法创建新进程),则会返回负值。

./ProcessBasicAPI-fork
Parent pid: 4007
[4007] parent of [4008]
Parent exit
[4008] child
Child exit

上面为在bash中的运行结果,由于操作系统内部的进程调换操作,每次的结果各个输出顺序可能不同。

进程之间的竞争

由于操作系统对进程的调度问题,当进程之间没有信号操作(等待等操作),通常两个进程的运行顺序会变得不可控,每次程序的运行都会出现不同的输出结果。

竞争实例

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    int i;
    pid_t mypid;
    pid_t cpid = fork();
    if (cpid > 0) {
        mypid = getpid();
        printf("[%d] parent of [%d]\n", mypid, cpid);
        for (i=0; i<10; i++) {
          printf("[%d] parent: %d\n", mypid, i);
          usleep(1000);
        }
    }  else if (cpid == 0) {
        mypid = getpid();
        printf("[%d] child\n", mypid);
        for (i=0; i>-10; i--) {
          printf("[%d] child: %d\n", mypid, i);
          usleep(1000);
        }
    } 

    return 0;
}

在上述的代码中,运行时会产生不同的主进程和子进程之间的切换顺序,有时会主进程和子进程穿插进行:

./ProcessBasicAPI-races
[4434] parent of [4435]
[4434] parent: 0
[4435] child
[4435] child: 0
[4434] parent: 1
[4435] child: -1
[4434] parent: 2
[4435] child: -2
[4434] parent: 3
[4435] child: -3
[4434] parent: 4
[4435] child: -4
[4434] parent: 5
[4435] child: -5
[4435] child: -6
[4434] parent: 6
[4434] parent: 7
[4435] child: -7
[4434] parent: 8
[4435] child: -8
[4434] parent: 9
[4435] child: -9

同时应该注意到的是,进程之间的数据是不共享的,这里主进程和子进程的i值是不同的。

创建线程

线程和进程最大的区别在于,线程是存在于进程中的,一个进程中可能会有多个不同的线程,每个线程都有着独立的栈和堆,每个线程的动态开辟内存和栈内的变量内存是独立的。互相不干扰。

在C语言中,对线程的操作函数被定义在头文件<pthread.h>中。在C中定义了一个pthread_t数据类型,用来存储线程的ID。

创建线程可以用pthread_create函数,在创建线程之前我们要先定义一个pthread_t变量。

pthread_create函数的声明如下:

int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,
void *(*start_rtn)(void*),void *arg);

为了方便查看这里分为了两行,实际上这只是一行的函数原型,这四个函数参数分别是:

第一个参数为指向线程标识符的指针(pthread_t)。

第二个参数用来设置线程属性。

第三个参数是线程运行函数的起始地址(函数指针)。

最后一个参数是运行函数的参数。

等待线程

由于主线程结束后进程就会结束,所以一般要让主线程等待子线程,否则可能会子线程还没有运行完毕就会结束。

等待线程可以用pthread_join函数。

函数pthread_join用来等待一个线程的结束,线程间同步的操作。头文件 : #include <pthread.h>。

int pthread_join(pthread_t thread, void **retval);

描述 :pthread_join()函数,以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable的。

参数 :thread: 线程标识符,即线程ID,标识唯一线程。retval: 用户定义的指针,用来存储被等待线程的返回值。

返回值 : 0代表成功。 失败,返回的则是错误号。

注意事项

因为pthread并非Linux系统的默认库,而是POSIX线程库。在Linux中将其作为一个库来使用,因此加上 -lpthread(或-pthread)以显式链接该库。函数在执行错误时的错误信息将作为返回值返回,并不修改系统全局变量errno,当然也无法使用perror()打印错误信息。

线程实例

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <pthread.h>

void* myfunc(void* args) {
  
  pthread_t tid = pthread_self();          
  printf("[%ld] child. Exit\n", tid);
}

int main(int argc, char *argv[])
{

  pthread_t th;
  if(pthread_create(&th, NULL, myfunc, NULL)!=0)
  {
    perror("pthread_create failed");
    exit(1);
  }

  pthread_t mytid = pthread_self();

  printf("Parent tid: %ld\n", mytid);
  printf("[%ld] parent of [%ld]. Exit\n", mytid, th);

   pthread_join(th, NULL);

  exit(0);
}

在这里我们创建了子线程,打印了线程ID,之后在主线程里打印自身ID和子线程ID。

在bash中运行结果如下:

./ThreadBasicAPI-create
Parent tid: 140650212271936
[140650212271936] parent of [140650212267776]. Exit
[140650212267776] child. Exit

线程共享数据

线程之间虽然堆栈是不共享,互相独立的,但是线程都在同一个进程下,这就让全局的静态变量有了用武之地。

我们知道,可执行程序包括BSS段、数据段、代码段(也称文本段)。

BSS段通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0。

因此,我们如果想让一个问题由多个线程并发处理,就可以把公共的数据存在BSS段中。

共享数据实例

#include <stdio.h>
#include <pthread.h>
#include <time.h>
#include <stdlib.h>
#define MAX_ARRAY_SIZE 5000

int num[MAX_ARRAY_SIZE];
int total = 0;
void* myfunc1(void* args){
	if (args != NULL)
	{
		char* specialstr = args;
		printf("Pthread special string: %s\n",specialstr);
	}
	pthread_t pid = pthread_self();
	int i;
	int sum = 0;
	for (i = 0;i < 2500; ++i)
	{
		sum += num[i];
	}
	printf("Child[%ld]: the sum of num[%d] to num[%d] is: %d\n",pid,0,2499,sum);
	total+=sum;
}

void* myfunc2(void* args){
	if (args != NULL)
	{
		char* specialstr = args;
		printf("Pthread special string: %s\n",specialstr);
	}
	pthread_t pid = pthread_self();
	int i;
	int sum = 0;
	for (i = 2500;i < 5000; ++i)
	{
		sum += num[i];
	}
	printf("Child[%ld]: the sum of num[%d] to num[%d] is: %d\n",pid,2500,4999,sum);
	total+=sum;
}

int main(int argc, char const *argv[])
{
	int i;
	srand(time(0));
	for (i = 0;i < MAX_ARRAY_SIZE; ++i){
		num[i] = rand() % 1000;
	}
	int sum = 0;
	pthread_t th1,th2;
	pthread_create(&th1,NULL,myfunc1,"First");
	pthread_create(&th2,NULL,myfunc2,"Second");
	pthread_join(th1,NULL);
	pthread_join(th2,NULL);
	for(i = 0; i < 5000; ++i){
		sum += num[i];
	}
	pthread_t pid = pthread_self();
	printf("Main[%ld]: the sum of num[%d] to num[%d] is: %d\n",pid,0,4999,sum);
	printf("Total is %d\n",total );
	return 0;
}

这是一个简单的数列求和的函数,这里我们开辟了一个大小为5000的整形数组,我们分别创立了两个线程计算前2500个和以及后5000个和,在这里方便展示,主线程还是求和了一次,实际上可以把两个线程的求和结果都用一个存储在BSS中的静态变量加和记录,就可以两个线程并行处理问题了。

总结

本文讨论了一些很简单的并发编程的方法,读者也可以自己尝试用并发编程,编写一个用户可以自己指定创建多少个线程并行解决问题的程序。

  • 6
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值