本篇文章,继续来和大家分享与Linux相关的知识。本次的内容主要会涉及到多线程的创建,线程独立栈的验证,线程之间没有秘密,线程局部存储,分离线程以及线程互斥等相关知识。
Linux中的线程 = 用户级线程 + 内核的LWP,用户级线程和内核的LWP的关系是1 :1的关系。
在一些书籍中,会有用户级线程和内核级线程的概念。用户级线程是指在用户层实现的线程,比如Liunx;而内核级线程是指在OS内部实现的线程,比如Windowns。
一个线程相当于一个执行流,执行流的本质是调用链。每个线程都在执行自己的任务,有不同的调用链,需要独立的栈进行隔离,保证线程之间互不干扰。
创建多线程
下面,我们写一个程序演示如何创建多线程:
打开监控脚本
while :; do ps -aL | head -1 && ps -aL | grep mythread; sleep 1; done;
编译运行,我们就能看到线程数量逐渐增加,又逐渐减少。
验证每个线程都独立的栈
我们对刚刚的程序做一些修改:
在threadRoutine函数中,定义一个test_i变量,然后,打印这个变量的值和它的地址,并且每次循环自增一。
编译运行,我们就能看到每个线程的test_i变量的值都是从0开始递增的,并且它们的地址也不相同。这不就说明了,每个线程都会有自己独立的栈结构吗?
虽然每个线程都有自己独立的栈结构,但它们都在同一个共享区内。线程和线程之间没有秘密,一个线程可以拿到其他线程的内容。只不过我们要求线程有独立的栈。
验证一个线程,可以访问其他线程的内容
我们增加一个全局变量p,当线程为thread-2的时候,我们就在threadRoutine函数中,将变量test_i的地址拷贝到p中,并且让主线程进行访问。
编译运行,我们就能看到主线程,确实拿到了thread-2线程的test_i变量。
线程局部存储
如果我们定义一个全局变量g_val,让所有线程访问它,能访问到它吗?
编译运行,我们就能会看到多个线程访问的g_val的地址相同,这说明它们访问的是同一个g_val。
从刚刚的运行结果看,我们可以知道全局变量为所有的线程共享。
那如果我就想创建一个私有的全局变量怎么办?有的人会说,我们可以创建一个数组,一个线程给它分配一个变量,不就行了吗?不行,这样变量的名称变了。那该怎么办呢?我们只需要使用__thread修饰相应的变量即可。
编译运行,我们就能看到,每个线程的g_val的值是从0开始的,并且每个g_val的地址都不相同。这种技术,我们称之为线程的局部存储。__thread并不是C++的东西,而是编译器提供的一种编译选项。编译器在编译的时候,就默认将g_val变量,给每个线程开辟一份空间。
下面,我们使用__thread来修饰td,看看有什么有效果
编译直接报错了,这是为什么?这是因为__thread只能用来修饰内置类型,不能用来修饰自定义类型。
那这个线程的局部存储有什么用呢?
我们可以给线程创建一个私有的全局变量number和pid,用于保存线程的tid和pid,这样我们就不用频繁的调用系统接口进行获取。
编译运行,就能正常看到线程的tid和pid了。
分离线程
在讲进程等待的时候,我们可以采用阻塞等待,也可以采用非阻塞轮询,还可以采用信号回收的方式。
那线程等待这里,我们有没有什么别的方式,可以让主线程在等待线程的时候,不用阻塞在那里,可以去做别的事吗?
当然有,如果你不关心线程的返回值,可以告诉系统,当线程退出时,自动释放线程资源。
那你怎么告诉呢?通过调用函数pthread_detach。这个函数的参数就是线程的tid。detach是分离的意思,调用pthread_detach函数可以将相应的线程与整个进程进行分开。
哪由谁来调用pthread_detach函数呢?可以是主线程,也可以是自己。
下面,我们来用用pthread_detach接口:
主线程让其他线程进行分离:
编译运行,我们就能看到pthread_join的返回值是22,返回值为0才算等待成功。失败的原因,是参数无效。这说明,我们的线程分离成功了,并且分离的线程无法进行等待。
线程自己进行分离:
编译运行,结果是一样的,pthread_join无法对分离线程进行等待。
有一个细节,不知道大家注意到没:按照我们的程序设定,线程实际上并没有执行完,就退出了,这是为什么?
这是因为主线程走到pthread_join的时候,并没有阻塞等待,而是直接返回了,然后,结束了。主线程结束了,也就是进程退出了,进程退出,系统就会把进程的全部资源释放。线程赖以生存的资源被释放了,它没法执行了,只能跟着一起退出。
我们进行线程分离,只是不关心它的返回值,但我们还是希望它把自己的任务完成。所以,我们进行线程分离的时候,要保证主线程是最后退出的。
怎么保证?比较常见的方式是让主线程一直不退。
线程的分离状态,本质是线程的一种属性状态。一个线程是否是分离状态,取决于它的分离标志位,是0还是1。
线程互斥
我们以一个模拟的抢票程序,来引入互斥:
假设票数总共1000,让三个线程进行抢票。
编译运行,我们会发现票数抢到了负数。这是为什么呢?
变量tickets,是共享数据,在多线程并发读写时,并没有按照我们的预期减到0,而是直接干到了负数。
我们称为数据在无保护的情况下,被多线程并发访问,造成了数据不一致问题。
思考一个问题:对一个全局变量进行多线程并发--/++操作是否是安全的?答案是不安全。
为什么不安全?我们一起探索一下。
进行tickets--操作,实际会有三步操作。第一步,先将内存中ticktets值读入CPU的寄存器中。第二步,在CPU内部进行--操作。第三步,将计算结果写回内存。刚刚这三步对应着三条汇编操作,依次是mov [xxx] eax
--
mov eax [xxx]
假设有一个线程叫thread-1,它进行tickets--操作,刚刚执行完第一步,读取数据,很不巧,就被切换了。
线程切换,是需要保存上下文(线程的上下文是指线程在CPU中的状态和数据)。于是,thread-1就把寄存器中的1000保存了下来。
切换线程,切换到了thread-2。thread-2很幸运,一直没有没有被打断,它一直tickets--,减到了10。这时候,被切换了。
切换到了thread-1。调度thread-1,首先要做的是恢复thread-1的上下文。也就是将1000恢复到寄存器中,然后,执行第二步,减减操作得到999。第三步,将999写回内存中。咦?票数好不容易减到了10,怎么又变回了999。这种情况,就导致了数据不一致。
而我们的代码中,不仅有--操作,还有判断操作(tickets > 0)。判断操作,是逻辑运算,也是一种运算。
假设tickets减到了1,thread-1,进行if判断满足条件,进入if。这时候,又来了thread-2,thread-3,进行tickets > 0判断。tickets为1,它们两也满足条件,也进入了if。thread-1将tickets减为0,thread-2,thread-3再继续往下减,就出现了,我们看到的负数。
针对多线程并发访问无保护数据,导致的数据不一致问题,我们该怎么解决呢?
我们需要对共享数据的任何访问,保证任何时候只有一个执行流访问!这就需要用到互斥,而实现互斥,需要用到pthread库中提供的锁。
好了,到这里,我们本次的分享就到此结束了,不知道我有没有说明白,给予你一点点收获。关于更多和Linux相关的知识,会在后面的文章更新。如果你有所收获,别忘了给我点个赞,这是对我最好的回馈,当然你也可以在评论发表一下你的收获和心得,亦或者指出我的不足之处。如果喜欢我的分享,别忘了给我点关注噢。