当前位置: 首页 > 科技观察

一个多线程的简单例子,让你看到线程调度的随机性

时间:2023-03-14 10:10:13 科技观察

线程调度的几个基本知识点很多同学不知道多线程并发执行时,调度的随机性会带来什么问题。解锁关键资源会导致一些紧急情况甚至死锁。关于线程调度,需要深入了解以下基础知识点:调度的最小单位是轻量级进程【比如我们写的最简单的C程序helloworld,执行时就是轻量级进程】或者线程;每个Threads都会被分配一个时间片,当时间片到达时会执行下一个线程;线程的调度具有一定的随机性,无法确定什么时候调度;在同一个进程中,所有创建的线程除线程内部创建的本地资源外,进程创建的其他资源为所有线程共享;例如:主线程和子线程都可以访问全局变量,打开文件描述符等。一个有很多例子的理论不如一个生动的例子直接。预期的代码时序假设我们要实现一个多线程实例。期望的程序执行时序如下:ExpectedtimingExpectedfunctionaltiming:主进程创建子线程,子线程函数function();主线程计数自增,分别赋值给value1,value2;时间片到后,切换到子线程。子线程判断value1和value2是否相同。如果它们不同,它会打印value1、value2和count的值。但是,因为主线程先后给value1和value2赋值,所以value1、value2的值永远不应该相同,所以什么都不打印;重复步骤2和3。代码1就OK了,现在我们按照这个顺序写代码如下:1#include2#include3#include4#include5#include67unsignedintvalue1,value2,count=0;8void*function(void*arg);9intmain(intargc,char*argv[])10{11pthread_ta_thread;1213if(pthread_create(&a_thread,NULL,function,NULL)<0)14{15perror("failtopthread_create");16exit(-1);17}18while(1)19{20count++;21value1=count;22value2=count;23}24return0;25}2627void*function(void*arg)28{29while(1)30{31if(value1!=value2)32{33printf("count=%d,value1=%d,value2=%d\n",count,value1,value2);34usleep(100000);35}36}37returnNULL;38}程序乍一看应该满足我们的需求,程序运行时应该不会打印任何东西,但实际运行结果却出乎我们的意料。编译运行:gcctest.c-orun-lpthread./runCode1Executionresult执行结果:可以看到子程序会随机打印一些信息,为什么会出现这样的执行结果呢?其实原因很简单,就是我们文章开头说的,线程调度是随机的,我们无法指定内核什么时候调度一个线程。如果有打印信息,说明此时value1和value2的值不同,也说明在调度子线程时,主线程调度到value1和value2之间的位置。代码1的实际执行时序代码的实际执行时序如下:如上图所示,在某一时刻,当程序到达**value2=count;**的位置时,内核调度线程,所以子进程在判断value1和value2的值时,发现这两个变量的值不一样,然后打印信息。程序还是很有可能会调度到下面两行代码之间。值1=计数;值2=计数;解决方案如何解决由于并发导致程序没有按预期执行的问题?对于线程,常用的方法有posix信号量、互斥量、条件变量等。接下来我们以相互锁排斥为例,讲解如何避免代码1的问题。互斥量的定义和初始化:pthread_mutex_tmutex;pthread_mutex_init(&mutex,NULL)申请释放锁:pthread_mutex_lock(&mutex);pthread_mutex_unlock(&mutex);原理:在进入临界区之前先申请锁,如果能拿到锁,继续执行,如果申请不到,就会休眠,直到其他线程释放锁。代码21#include2#include3#include4#include5#include6#define_LOCK_7unsignedintvalue1,value2,count=0;8pthread_mutex_tmutex;9void*function(void*arg);1011intmain(intargc,char*argv[])12{13pthread_ta_thread;1415if(pthread_mutex_init(&mutex,NULL)<0)16{17perror("failtomutex_init");18exit(-1);19}2021if(pthread_create(&a_thread,NULL,function,NULL)<0)22{23perror("failtopthread_create");24exit(-1);25}26while(1)27{28count++;29#ifdef_LOCK_30pthread_mutex_lock(&mutex);31#endif32value1=count;33value2=count;34#ifdef_LOCK_35pthread_mutex_unlock(&mutex);36#endif37}38return0;39}041void*function(void*arg)42{43while(1)44{45#ifdef_LOCK_46pthread_mutex_lock(&mutex);47#endif4849if(value1!=value2)50{51printf("count=%d,value1=%d,value2=%d\n",count,value1,value2);52usleep(100000);53}54#ifdef_LOCK_55pthread_mutex_unlock(&mutex);56#endif57}58returnNULL;59}如上代码所示:当主线程和子线程要访问临界资源value1和value2时,必须先申请锁,获得锁后才能访问临界资源.访问完成后,释放互斥锁。不会打印任何信息。如果程序在以下代码之间产生调度,我们来看一下程序的时序图。值1=计数;值2=计数;时序图如下:如上图所示:在时刻n,主线程获得互斥锁,进入临界区;n+1时刻,时间片到时,切换到子线程;在n+2时刻,子线程无法申请锁mutex,因此放弃CPU进入休眠;n+3次,主线程释放互斥量,离开临界区,唤醒阻塞在互斥量中的子线程,子线程申请互斥量,进入临界区;n+4次,子线程离开临界区,释放互斥量。可以看出加锁后,即使主线程在value2=count;之前产生了schedule,子线程也会因为获取不到mutex而进入休眠状态。只有当主线程退出临界区时,子线程才能获得互斥量并访问value1和value2,永远不会打印信息,实现了我们预期的代码时序。综上所述,在实际项目中,程序的并发可能会更加复杂。例如,运行在多个CPU上的任务之间、运行在CPU上的任务与中断之间、中断与中断之间可能存在并发。虽然有些调度的概率很小,但不代表不会发生,资源同步互斥带来的问题很难重现。查看linux内核代码,所有的critical资源都会对应上锁。多看Linux内核源码,向大神学习,结交大神。正所谓代码读百遍,方知其意!看了几万行代码,就算不会写也能抄下来!关于内核与应用程序同步互斥的知识点,可以查看一口君的其他文章。本文转载自微信公众号“一口Linux”,可通过以下二维码关注。转载本文请联系易口Linux公众号。