一个例子让你看清线程调度的随机性

  1. 粉丝提问|c语言:如何定义一个和库函数名一样的函数,并在函数中调用该库函数
  2. 一个端口号可以同时被两个进程绑定吗?
  3. 两个线程,两个互斥锁,怎么形成一个死循环?
  4. 一个例子让你看清线程调度的随机性

线程调度的几个基本知识点

多线程并发执行时有很多同学捋不清楚调度的随机性会导致哪些问题,要知道如果访问临界资源不加锁会导致一些突发情况发生甚至死锁。

关于线程调度,需要深刻了解以下几个基础知识点:

  1. 调度的最小单位是轻量级进程【比如我们编写的hello world最简单的C程序,执行时就是一个轻量级进程】或者线程;
  2. 每个线程都会分配一个时间片,时间片到了就会执行下一个线程;
  3. 线程的调度有一定的随机性,无法确定什么时候会调度;
  4. 在同一个进程内,创建的所有线程除了线程内部创建的局部资源,进程创建的其他资源所有线程共享;
    比如:主线程和子线程都可以访问全局变量,打开的文件描述符等。

实例

再多的理论不如一个形象的例子来的直接。

预期代码时序

假定我们要实现一个多线程的实例,预期程序执行时序如下:

期待时序

期待的功能时序:

  1. 主进程创建子线程,子线程函数function();
  2. 主线程count自加,并分别赋值给value1,value2;
  3. 时间片到了后切换到子线程,子线程判断value1、value2值是否相同,如果不同就打印信息value1,value2,count的值,但是因为主线程将count先后赋值给了value1,value2,所以value1,value2的值应该永远相同,所以不应该打印任何内容;
  4. 重复2、3步骤。

代码1

好了,现在我们按照这个时序编写代码如下:

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #include <pthread.h>
  5 #include <unistd.h>
  6 
  7 unsigned int value1,value2, count=0;
  8 void *function(void *arg);
  9 int main(int argc,  char *argv[])
 10 {
 11     pthread_t  a_thread;
 12 
 13     if (pthread_create(&a_thread, NULL, function, NULL) < 0)
 14     {
 15         perror("fail to pthread_create");
 16         exit(-1);
 17     }
 18     while ( 1 )
 19     {
 20         count++;
 21         value1 = count;
 22         value2 = count;
 23     }
 24     return 0;
 25 }
 26 
 27 void  *function(void *arg)
 28 {
 29     while ( 1 )
 30     {
 31         if (value1 != value2)
 32         {                                                                                                                                                                                         
 33             printf("count=%d , value1=%d, value2=%d\n",  count, value1, value2);
 34             usleep(100000);
 35         }     
 36     }
 37     return  NULL;
 38 }  

乍一看,该程序应该可以满足我们的需要,并且程序运行的时候不应该打印任何内容,但是实际运行结果出乎我们意料。

编译运行:

gcc test.c -o run -lpthread
./run

代码1执行结果

执行结果:
代码1执行结果
可以看到子程序会随机打印一些信息,为什么还有这个执行结果呢?
其实原因很简单,就是我们文章开头所说的,线程调度具有䘺随机性,我们无法规定让内核何时调度某个线程。
有打印信息,那么这说明此时value1和value2的值是不同的,那也说明了调度子线程的时候,是在主线程向value1和value2之间的位置调度的。

代码1执行的实际时序

实际上代码的执行时序如下所示:
代码1实际时序

如上图,在某一时刻,当程序走到**value2 = count;**这个位置的时候,内核对线程进行了调度,于是子进程在判断value1和value2的值的时候,发现这两个变量值不相同,就有了打印信息。

该程序在下面这两行代码之间调度的几率还是很大的。

value1 = count; 
value2 = count;

解决方法

如何来解决并发导致的程序没有按预期执行的问题呢?
对于线程来说,常用的方法有posix信号量、互斥锁,条件变量等,下面我们以互斥锁为例,讲解如何避免代码1的问题的出现。

互斥锁的定义和初始化:

pthread_mutex_t  mutex;
pthread_mutex_init(&mutex, NULL)

申请释放锁:

pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);

原理:
进入临界区之前先申请锁,如果能获得锁就继续往下执行,
如果申请不到,就休眠,直到其他线程释放该锁为止。

代码2

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #include <pthread.h>
  5 #include <unistd.h>
  6 #define _LOCK_
  7 unsigned int value1,value2, count=0;
  8 pthread_mutex_t  mutex;
  9 void *function(void *arg);
 10 
 11 int main(int argc,  char *argv[])
 12 {
 13     pthread_t  a_thread;
 14          
 15     if (pthread_mutex_init(&mutex, NULL) < 0)                                                                                                                                                          
 16     {
 17         perror("fail to mutex_init");
 18         exit(-1);
 19     }
 20 
 21     if (pthread_create(&a_thread, NULL, function, NULL) < 0)
 22     {
 23         perror("fail to pthread_create");
 24         exit(-1);
 25     }
 26     while ( 1 )
 27     {
 28         count++;
 29 #ifdef  _LOCK_
 30         pthread_mutex_lock(&mutex);
 31 #endif
 32         value1 = count;
 33         value2 = count;
 34 #ifdef  _LOCK_
 35         pthread_mutex_unlock(&mutex);
 36 #endif
 37     }
 38     return 0;
 39  }
40 
 41 void  *function(void *arg)
 42 {
 43      while ( 1 )
 44      {
 45 #ifdef _LOCK_
 46         pthread_mutex_lock(&mutex);
 47 #endif           
 48 
 49         if (value1 != value2)  
 50         {
 51             printf("count=%d , value1=%d, value2=%d\n",  count, value1, value2);
 52             usleep(100000);
 53         }     
 54 #ifdef _LOCK_
 55         pthread_mutex_unlock(&mutex);
 56 #endif
 57      }
 58      return  NULL;
 59  }     

如上述代码所示:主线程和子线程要访问临界资源value1,value2时,都必须先申请锁,获得锁之后才可以访问临界资源,访问完毕再释放互斥锁。
该代码执行之后就不会打印任何信息。
我们来看下,如果程序在下述代码之间产生调度时,程序的时序图。

value1 = count; 
value2 = count;

时序图如下:
代码2加锁后时序图

如上图所示:

  1. 时刻n,主线程获得mutex,从而进入临界区;
  2. 时刻n+1,时间片到了,切换到子线程;
  3. n+2时刻子线程申请不到锁mutex,所以放弃cpu,进入休眠;
  4. n+3时刻,主线程释放mutex,离开临界区,并唤醒阻塞在mutex的子线程,子线程申请到mutex,进入临界区;
  5. n+4时刻,子线程离开临界区,释放mutex。

可以看到,加锁之后,即使主线程在value2 =count; 之前产生了调度,子线程由于获取不到mutex,会进入休眠,只有主线程出了临界区,子线程才能获得mutex,访问value1和value2,就永远不会打印信息,就实现了我们预期的代码时序。

总结

实际项目中,可能程序的并发的情况可能会更加复杂,比如多个cpu上运行的任务之间,cpu运行的任务和中断之间,中断和中断之间,都有可能并发。

有些调度的概率虽然很小,但是不代表不发生,而且由于资源同步互斥导致的问题,很难复现,纵观Linux内核代码,所有的临界资源都会对应锁。

多阅读Linux内核源码,学向大神学习,与大神神交。
正所谓代码读百遍,其义自见!
熟读代码千万行,不会编写也会抄!

关于内核和应用程序的同步互斥的知识点,可以查看一口君的其他文章。

更多Linux干货,请关注一口Linux

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 当然可以!这是一个使用 Python 的 `threading` 模块创建多线程的简单例子: ```python import threading def print_square(number): print(f"Square: {number * number}") def print_cube(number): print(f"Cube: {number * number * number}") if __name__ == "__main__": # 创建两个线程 t1 = threading.Thread(target=print_square, args=(10,)) t2 = threading.Thread(target=print_cube, args=(10,)) # 启动两个线程 t1.start() t2.start() # 等待所有线程结束 t1.join() t2.join() ``` 这段代码会同时运行 `print_square` 和 `print_cube` 两个函数,分别计算平方和立方。 如果您想了解更多关于 Python 多线程的知识,可以参考 Python 官方文档:https://docs.python.org/3/library/threading.html。 ### 回答2: 当然可以!以下是一个使用Python编写的简单多线程示例: ```python import threading # 定义一个线程任务函数 def task(): for i in range(5): print("线程执行:", i) # 创建多个线程 threads = [] for i in range(3): t = threading.Thread(target=task) threads.append(t) # 启动所有线程 for t in threads: t.start() # 等待所有线程完成 for t in threads: t.join() print("所有线程执行完毕") ``` 以上示例创建了三个线程,并分别执行同一个任务函数task。每个线程都会打印输出五次,输出内容为"线程执行:"加上相应的数字。经过多次运行,可以发现三个线程的执行次序是随机的,因为线程是并发执行的,其执行顺序不确定。 最后,等待所有线程执行完毕后,输出"所有线程执行完毕"。这里使用了join方法,意味着主线程会等待所有子线程都执行完毕才会继续执行。 ### 回答3: 当然可以!以下是一个用Python编写的多线程例子: ```python import threading # 自定义线程类 class MyThread(threading.Thread): def __init__(self, name): threading.Thread.__init__(self) self.name = name def run(self): print(f"线程 {self.name} 正在运行") def main(): threads = [] # 创建5个线程 for i in range(5): thread = MyThread(f"Thread-{i+1}") thread.start() threads.append(thread) # 等待所有线程完成 for thread in threads: thread.join() print("所有线程运行完成") if __name__ == "__main__": main() ``` 这个例子创建了5个自定义线程对象,并通过调用`start()`方法来启动它们。每个线程会执行`run()`方法中的代码。在这个例子中,`run()`方法只是简单地打印线程的名称。 主函数`main()`中,创建了一个线程列表`threads`,并将所有线程加入列表中。然后,通过遍历列表调用`join()`方法,等待所有线程运行结束。 最后,主函数打印出"所有线程运行完成"。 通过多线程的使用,我们可以同时执行多个任务,提高程序的并发性和效率。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一口Linux

众筹植发

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值