155-Linux的多线程(上)

线程的概念

进程是一条正在运行的程序
线程是进程内部的一条执行序列或执行路径,一个进程可以包含多条线程。
在这里插入图片描述
在操作系统中,线程的实现有以下三种方式
内核级线程
用户级线程
组合级线程
在这里插入图片描述
用户级线程:在内核中并不提供创建线程的机制,在内核中只知道创建1个进程,也就是说只能创建1条执行路径,然后在用户空间提供线程库模拟出多条执行路径,内核并不能感知到用户空间的多条执行路径的存在,是在用户空间创建的,开销小,可以创建更多的数目,是用户管理的,缺点是无法使用多处理器的资源。比如说,现在有2个处理器,都是空闲的,如果是内核级线程,因为内核参与了线程的创建,它可以感知到该线程的存在,而且该线程是由内核直接进行管理的,所以可以把2个不同的线程放在2个不同的处理器上,把这2个处理器都使用起来,那像用户级线程,因为操作系统只知道自己只创建了1条线程,只对这1条路径管理,而用户空间模拟出的多条路径,虽然开销小,但是无法把这多条路径放在多个的处理器上,因为在内核空间眼里只有1条路径。内核级线程创建开销大。
组合级线程:内核空间允许创建多条线程,目的是为了用多处理器资源,比如说有4个处理器,内核空间可以创建4个线程,就可以利用到这4个处理器,接下来在用户空间可以创建更多的用户线程,可以映射到这4个内核线程中,在用户空间可以创建更多线程,当线程数超出了处理器的数目,还是要时间片轮转的,此时就没必要创建内核级线程,我们只要保证内核级的线程的数目和处理器的数目相当就可以,其余就是用户空间创建多个线程,多对多的过程,用户空间线程数大于内核空间线程,能够使用到多处理器资源,而且创建线程的开销小一些。

Linux 中线程的实现
Linux 实现线程的机制非常独特。从内核的角度来说,它并没有线程这个概念。Linux 把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的 task_struct,所以在内核中,它看起来就像是一个普的进程(只是线程和其他一些进程共享某些资源,如地址空间)。
内核级的线程,内核管理的。

进程与线程的区别

1、进程是资源分配的最小单位,线程是 CPU 调度的最小单位
2、进程有自己的独立地址空间,线程共享进程中的地址空间
3、进程的创建消耗资源大,线程的创建相对较小
4、进程的切换开销大,线程的切换开销相对较小

在这里插入图片描述
目前层面,C语言没有提供多线程,C++现在新标准提了创建线程的方法

Linux线程库中的接口介绍

#include <pthread.h>

/*
pthread_create()用于创建线程
thread: 接收创建的线程的 ID
attr: 指定线程的属性  可以给空 默认情况给
start_routine: 指定线程函数
arg: 给线程函数传递的参数
成功返回 0, 失败返回错误码
*/
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
				   void *(*start_routine) (void *), void *arg);

/*
pthread_exit()退出线程
retval:指定退出信息
*/
int pthread_exit(void *retval);


/*
pthread_join()等待 thread 指定的线程退出,线程未退出时,该方法阻塞
retval:接收 thread 线程退出时,指定的退出信息
*/
int pthread_join(pthread_t thread, void **retval);

Linux多线程示例

1、
在这里插入图片描述
在这里插入图片描述

执行
在这里插入图片描述
再执行一次
在这里插入图片描述这种情况是主线程执行完了,直接退出主函数了,执行exit退出整个进程,连同整个线程直接退出了,有的线程还没执行完就 退出了,所以我们一般让主线程main最后再退出。

修改代码
在这里插入图片描述
执行
在这里插入图片描述
线程是并发运行的,fun和main谁先打印是系统决定的
再调整一下
在这里插入图片描述
执行
在这里插入图片描述
子线程的结束是不影响主线程的运行
再调整一下
在这里插入图片描述
执行
在这里插入图片描述
为什么?因为主线程只有2次,没事干了,系统会帮助调动exit退出进程,整个线程就异常结束

调整一下
让主线程退出!不是让进程退出哦!
在这里插入图片描述
执行
在这里插入图片描述
调整一下
等待fun结束
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

使用字符串常量是因为线程结束临时变量会消失。试图获得临时变量的地址应用字符串常量因为要注意生存期!
执行
在这里插入图片描述
下面这个操作也可以
在这里插入图片描述

在这里插入图片描述
注意:create语句执行了,fun线程才启动

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <pthread.h>

void * pthread_fun(void *arg)
{
	int i = 0;
	for(; i < 5; ++i)
	{
		sleep(1);
		printf("fun thread running\n");
	}

	pthread_exit("fun over");
}


int main()
{
	pthread_t tid;
	int res = pthread_create(&tid, NULL, pthread_fun, NULL);
	assert(res == 0);

	int i = 0;
	for(; i < 5; ++i)
	{
		sleep(1);
		printf("main thread running\n");
	}

	char *s = NULL;
	pthread_join(tid, (void **)&s);

	printf("s = %s\n", s);

	exit(0);
}

在这里插入图片描述

线程并发运行

在这里插入图片描述
在这里插入图片描述
打印结果
在这里插入图片描述
在这里插入图片描述
通过指针对i间接引用获取,执行第10行的那一刻,i的值是多少,index就是多少。create把i的地址传进去,地址是不会变的,但是通过i的地址获取i的值,fun函数什么时候走到获取i的值,才获取i的值,主线程i的值一直在循环++
比如说,当前启动第一个线程,当这个线程去获取i的值,原本把i的地址传进去,i为0,以为fun获取的值就为0,但是有可能fun线程走的慢一点,去获取i的值的时候,i的值已经变成1了,启动是有一个过程,主线程的++循环并没有停止,也有可能已经加到2了。
i的值在主线程周期性的改变
最后在27行的时候改为0,阻塞大概3秒钟,然后执行i++,
两个0,是一开始执行慢了。走到27行把i置为0,才获取到0

当i为0,只有一个线程被创建,i变为1后,才有第2个线程被创建,0是其中一个线程运行的比较晚,到27行被加到0的两个0有可能是27行时两个线程同时获取0,也有可能第一个线程运行特别快获取到0了。
i没有++,第二个线程是不会被创建的
i++,启动了多个线程,但是线程对i的获取时间不同,造成值不一样了。

多个线程是同时进行,create启动一次,就启动了一个线程。create 5次,就启动了5个线程,是并发运行的
在这里插入图片描述
在这里插入图片描述
执行
在这里插入图片描述
示例代码1

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

void* thread_fun(void *arg)
{
	int index = *((int*)arg);
	int i = 0;
	for( ;i < 5; i++ )
	{
		printf("index=%d\n",index);
		sleep(1);
	}
}

int main()
{
	pthread_t id[5];

	int i = 0;
	for( ; i < 5; i++ )
	{
		pthread_create(&id[i], NULL, thread_fun, (void*)&i);
	}

	for( i = 0;i < 5; i++ )
	{
		pthread_join(id[i],NULL);
	}

	exit(0);
}

在这里插入图片描述
示例代码2

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>

int g = 0;

void* thread_fun(void *arg)
{
	int i = 0;
	for( ;i < 1000; i++ )
	{
		printf("g=%d\n", ++g);
	}
}

int main()
{
	pthread_t id[5];

	int i = 0;
	for( ; i < 5; i++ )
	{
		pthread_create(&id[i], NULL, thread_fun, NULL);
	}

	for( i = 0;i < 5; i++ )
	{
		pthread_join(id[i], NULL);
	}

	exit(0);
}

在这里插入图片描述

看例子

创建5个线程 并发运行 对全局变量++1000次
正常情况下一共对val加加了5000次
在这里插入图片描述

运行结果如下
在这里插入图片描述
再运行一次
在这里插入图片描述
这是为什么呢?
我们对val++,不是原子操作,转换为指令,有多条指令构成,计算机执行的是二进制的指令
我们对变量的改变分了很多步骤

比如有两条线程对val++
但是++不是一下子可以完成,先将val读过来,再++,再读回去,这个操作还没结束,另外一个线程也把val读过来,++,再读回去。有可能两个线程对val=1;进行加加,最后值却为2
在这里插入图片描述
通过互斥锁,信号量进行保护
出现这种情况,其中还有一个问题。
对虚拟机的设置做一点改变
在这里插入图片描述
重新启动,对刚才的程序再执行
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
运行二十多次,出现1次小于5000的情况
大概率都是出现5000了

这是为什么呢?

刚才有4个处理器,至少有两个处理器有处理其他线程,存在一个线程放在2个处理器上,同时访问,出现小于5000的概率比较高,并行引起的

调成1个处理器,5个线程,1个线程执行,其余4个肯定没有执行,不出现同时执行两个线程的情况。出现小于5000的概率很小(这个原因是,把val值1读过来,还没来得及++回去,这个时候时间片到了,发生了切换,换到其余线程,读过来还是1,加加,现场恢复,还是1进行加加,这种场景出现的概率非常小)
1个处理器不能并行的

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

林林林ZEYU

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值