线程控制、如何给面试官描述线程不安全的情况?模拟黄牛抢票展现不安全的情况及解决方式、互斥锁加锁解锁
多线程1
- 线程概念
- 线程控制
- 线程创建
- 线程终止
- 线程等待
- 线程分离
- 线程安全
- 如何给面试官描述线程不安全的现象?
- 线程不安全的情况(模拟黄牛抢票)
- 互斥
- 互斥锁的原理
- 互斥锁的接口
- 静态初始化
- 动态初始化
- 加锁
- 阻塞加锁接口
- 非阻塞加锁接口
- 带有超时时间的加锁接口
- 解锁
- 销毁接口
- 互斥锁的使用(结合黄牛抢票)
- 什么时候要初始化互斥锁?
- 什么时候进行加锁
- 什么时候解锁?
- 什么时候释放互斥锁资源?
线程概念
一个进程当中一定存在一个主线程,执行main函数的线程就称为主线程其他线程都被称之为工作线程进程本质上是线程组,换句话说,线程组被称之为进程,线程也可以被称为轻量级进程(LWP),因为在操作系统内核当中不存在线程的概念pid:轻量级进程id,也被称之为线程idtgid:轻量级进程组id,也被称之为进程id在一个进程中,不管这个进程有多少线程,在所有线程的PCB中,tgid都是相同的主线程(执行main函数的LWP)的pid和tgid相等除了主线程,工作线程的pid都是不一样的,可以用pid去区分到底是哪一个线程
线程的共享与独有
独有:在共享去当中有自己的调用堆栈、寄存器、线程ID、errmo、信号屏蔽字、调度优先级(PR)共享:文件描述表(fd_array[xxx])、当前进程工作目录、用户id和用户组id、信号处理方式
线程的优缺点:
前提:并行:每一个执行六在同一时间都拥有一个CPU,同时进行运算并发:多个执行流在同一时刻只能由一个执行流拥有CPU进行运算优点:1、一个进程当中多个执行流可以并行的执行代码,就可以提高程序的运行效率2、进程切换要比线程切换操作系统付出的代价大3、线程占用的资源要比进程少很多4、可并行的运行缺点:1、当一个进程当中的线程数量远远超过CPU数量的时候,有可能线程切换的开销会影响程序运行效率总结:程序当中的线程数量不是越多越好2、健壮性,也就是代码的鲁棒性,多线程的状态下代码并没有单线程那么健壮,原因在于一个线程的崩溃就会影响其他线程3、缺乏访问控制4、编程的难度高
线程是操作系统的调度的基本单位
进程是操作系统资源分配的基本单位
进程与线程的对比:
1、进程的健壮性比线程好
2、多线程要比多进程耗费资源小,而且切换快,程序运行效率高
线程控制
线程创建
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void *), void *arg)
pthread_t:线程的标识符,本质上是线程在共享区独有空间的首地址thread:是一个出参,该值是由pthread_create函数赋值的pthread_attr_t:创建线程的属性,一般情况都指定为NULL,采用默认属性void *(*start_routine)(void *):函数指针,接收一个返回值为void*,参数为void*的函数地址,本质上就是线程入口函数,即线程创建出来后执行的第一个函数void *arg:给线程入口函数传递的参数,由于参数的类型是void*,所以给了程序无限的传递参数方式返回值:失败 < 0成功:==0
使用此函数必须包含一个名为 “ pthread.h ” 的头文件
创建一个Makefile文件
在编译多线程程序时一定要链接 libpthread.so线程库,去头(lib)去尾(.so)后用 -l 链接
即 -lpthread
↑ 线程标识符
↑ LWP代表轻量级进程,后边的数字是轻量级进程id,也叫线程id
怎么区分哪个是主线程?
看哪个调用了main函数
这里看到Thread 1 调用了main函数,所以Thread 1 是主线程,同理Thread 2 便是工作线程主线程的线程id和进程id是一样的,而工作线程不是看堆栈的原则:从下往上去看
top -H -p [pid]
查看线程占用情况
线程终止
void pthread_exut(void *retval);
作用:谁调用,谁退出
retval:线程结束时传递给等待线程的参数等待线程相当于进程中的父进程
1 #include <stdio.h>2 #include <unistd.h>3 #include <pthread.h>4 5 void* MyThreadStart(void* arg)6 {7 int* i = (int*)arg;8 i++;9 //while(1)10 {11 printf("i am MyThreadStart, i = %d\n", *i);12 sleep(1);13 }14 15 pthread_exit(NULL);16 printf("pthread_exit fail\n");17 18 }19 20 int main()//主线程21 {22 pthread_t tid;23 int i = 1;24 int ret = pthread_create(&tid, NULL, MyThreadStart, (void*)&i);25 if (ret<0)26 {27 perror("pthread_create");28 return 0;29 }30 while(1)31 {32 printf("i am main thread\n");33 sleep(1);34 }35 return 0;36 }
线程的入口函数代码执行完毕之后,线程就退出了
int pthread_cancel(pthread_t thread);
参数:thread:被终止的线程的标识符
1 #include <stdio.h>2 #include <unistd.h>3 #include <pthread.h>4 5 #define THREAD_NUM 46 7 struct ThreadId8 {9 int thread_id_;10 };11 12 void* MyThreadStart(void* arg)13 {14 struct ThreadId* ti = (struct ThreadId*)arg;15 while(1)16 {17 printf("i am MyThreadStart, i = %d\n", ti ->thread_id_);18 sleep(1);19 }20 21 delete ti;22 }23 24 int main()//主线程25 {26 pthread_t tid[THREAD_NUM];27 28 for(int i = 0; i < THREAD_NUM; i++)29 {30 struct ThreadId* ti = new ThreadId();31 ti->thread_id_= i;32 33 int ret = pthread_create(&tid[i], NULL, MyThreadStart, (void*)ti);34 if (ret<0)35 {36 perror("pthread_create");37 return 0;38 }39 }40 41 sleep(10);42 pthread_cancel(tid[2]);//退出3号线程,执行到这行时应该只剩下4个线程43 44 while(1)45 {46 printf("i am main thread\n");47 sleep(1);48 }49 return 0;50 }
获取当前自己线程的标识符:
pthread_t pthread_self(void);
可以退出别人,也可以退出自己
注意:如果主线程的代码当中调用pthread_cancel(pthread_self());,则主线程的状态变成僵尸状态,工作线程正常。整个线程并没有退出。
线程等待
原因:由于线程的默认属性为joinable属性,当线程退出的时候,其资源不远被操作系统回收,需要其他线程来进行线程等待,继续回收,否则就会造成内存泄漏
接口
int pthread_join(pthread_t thread,void **retval);
thread:需要等待的线程标识符
retval:线程退出的时候的返回值①线程入口函数退出的时候,retval就是线程入口函数的返回值②pthread_exit(void* retval):retval就pthread_exit函数的参数值③pthread_cancel:retval的值是一个常数 PTHREAD_CANCELED
1 #include <stdio.h>2 #include <unistd.h>3 #include <pthread.h>4 5 #define THREAD_NUM 46 7 struct ThreadId8 {9 int thread_id_;10 };11 12 void* MyThreadStart(void* arg)13 {14 struct ThreadId* ti = (struct ThreadId*)arg;15 //while(1)16 {17 printf("i am MyThreadStart, i = %d\n", ti ->thread_id_);18 sleep(1);19 }20 21 delete ti;22 return NULL;23 }24 25 int main()//主线程26 {27 pthread_t tid[THREAD_NUM];28 29 for(int i = 0; i < THREAD_NUM; i++)30 {31 struct ThreadId* ti = new ThreadId();32 ti->thread_id_= i;33 34 int ret = pthread_create(&tid[i], NULL, MyThreadStart, (void*)ti);35 if (ret<0)36 {37 perror("pthread_create");38 return 0;39 }40 }41 42 for(int i = 0; i < THREAD_NUM; i++)43 {44 pthread_join(tid[i], NULL);45 }46 47 while(1)48 {49 printf("i am main thread\n");50 sleep(1);51 }52 return 0;53 }
此时会打印“i am main thread”是因为线程等待到了,若是无法等待到呢
15 while(1)
调用 pthread_join 进行等待的执行流如果害没有等待到退出 线程,则当前调用 pthread_jon 函数的执行流就会阻塞
线程分离
一个线程的属性如果从joinable属性变成detach属性,则当前这个线程在退出的时候,不需要其他线程回收资源,操作系统会自己回收资源
接口
int pthread_detach(pthread_t thread);
参数:thread:待要分离的线程的标识符
线程安全
多个执行流访问临界资源,不会导致程序产生二义性执行流:理解为线程访问:指的是对临界资源进行操作临界资源:指的是多个线程都可以访问到的资源例如:全局变量、某个结构体变量,某个类的实例化指针临界区:代码操作临界资源的代码区域称之为临界区二义性:结果会有多个
如何给面试官描述线程不安全的现象?
原理(对正常变量进行操作的原理)如果想对一个变量i进行++操作,首先需要线程将数据传递到寄存器中,随后再由寄存器传入到CPU中进行++操作,在CPU++操作完后再回写到寄存器中,再由寄存器回写到内存当中1、假设场景,有几个线程,每个线程都想做什么事情假设有两个线程AB,线程AB都想对全局变量i进行++2、分线程去描述,体现出来:线程切换(上下文信息,程序计数器)线程A从内存当中把全局变量i读到CPU的寄存器当中,i 原始的值为10,此时线程A的时间片到了,线程A被切换出来,线程A中的上下文信息保存的是寄存器当中的值,程序计数器中保存的是线程A下一步即将进行的+指令,此时线程B如果获取CPU资源或时间片,线程B也想对当前的全局变量i进行++,此时B从内存当中把i的值10读到了寄存器当中,顺利的进行了++,并且回写到了寄存器当中再回写到了内存当中,此时内存中全局变量 i的值就从10变成了11,此时线程B的时间片到了,让出了CPU资源,当线程A再次切换回来时,A想对全局变量i继续进行++,但是A此时i的值是从上下文信息当中获取,而上下文信息当中的值是10,进行++后回写到内存当中,内存当中的值也是113、总结线程A对全局变量i加了一次,线程B也对全局变量i加了一次,但最终i的值变成了11,并不是12,由此就产生了线程不安全的情况,即产生了多个线程使用临界资源时有可能产生的二义性
线程不安全的情况(模拟黄牛抢票)
1 #include <stdio.h>3 #include <unistd.h>4 5 #define THREAD_NUM 4 //设定黄牛的数量为46 7 int g_tickets = 10000;//设定总票数为10000张8
W> 9 void* MyThreadStart(void* arg)10 {11 while(1)//循环抢票12 {13 if(g_tickets > 0)14 {15 //票的数量大于0代表能抢
W> 16 printf("i have %d, i am %p\n", g_tickets, pthread_self());17 //打印线程标识符(黄牛的代号)以及黄牛拿到了第几张票18 g_tickets--;19 }20 else//没抢到票21 {22 pthread_exit(NULL);//直接退出23 }24 }25 return NULL;26 }27 28 int main()29 {30 pthread_t tid[THREAD_NUM];31 for(int i = 0; i < THREAD_NUM; i++)32 {33 int ret = pthread_create(&tid[i], NULL, MyThreadStart, NULL);34 if(ret < 0)35 {36 perror("pthread_create fail\n");37 return 0;38 }39 }40 41 for(int i = 0; i < THREAD_NUM; i++)42 {43 pthread_join(tid[i], NULL);44 //主线程在创建完成工作线程之后调用pthread_join进行等待45 //等待两个工作线程将票抢完,工作线程退出后主线程等待到了则pthread_join结束46 }47 48 printf("pthread_join end ...\n");49 return 0;50 }
运行一下
好家伙,这直接出现了三个不同的黄牛抢到了同样的9789号票的情况,而且很有可能第四号黄牛也抢到了同样的票,不过记录被顶掉了,所以看不到
互斥
互斥锁的原理
互斥锁的底层是一个互斥量,而互斥量的本质是一个计数器
计数器的取值只有两种,一种是1,一种是01:表示当前临界资源可以被访问0:表示当前临界资源不可以被访问获取/释放互斥锁的逻辑:1、调用加锁接口,加锁接口内部判断计数器的值是否为1如果为1,则能访问,当加锁成功偶,会将计数器的值从1变成0如果为0,则不能访问2、调用解锁逻辑,计数器的值从0变成1,表示资源可用
互斥锁的接口
初始化互斥锁变量
互斥锁的类型:pthread_mutex_t
静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
PTHREAD_MUTEX_INITIALIZER 是一个宏定义,包含了多个值
动态初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
mutex:该参数为出参,由调用者传递一个互斥锁变量的地址,由 pthread_mutex_init 函数进行初始化
attr:互斥锁的属性信息,一般置为NULL,采用默认属性
注意:动态初始化互斥锁变量的情况需要动态销毁互斥锁,否则就会造成内存泄漏
加锁
阻塞加锁接口
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数为传递一个互斥锁变量的地址
注意:如果互斥锁变量当中的计数器的值为1,调用该接口,加锁成功,该接口调用完毕,函数返回如果互斥锁变量当中的计数器的值为0,调用该接口,调用该接口的执行流阻塞
非阻塞加锁接口
int pthread_mutex_trylock(pthread_mutex_t *mutex);
注意:不管有没有加锁成功,都会返回,所以需要对加锁进行判断是否加锁成功,如果成功则操作临界资源,失败则需要循环获取互斥锁直到拿到
带有超时时间的加锁接口
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);struct timespec:typedef long time_t#ifndef _TIMESPEC#define _TIMESPECstruct timespec {time_t tv_sec;//secondslong tv_nsec;//nanoseconds};#endif
struct timespec 有两个成员,一个是秒,一个是纳秒,即最高精确度是纳秒
超时时间内,如果还没有获取到互斥锁,则返回;
超时时间内,如果获取了互斥锁直接返回
解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
注意:不管是阻塞加锁/非阻塞加锁/timelock加锁成功互斥锁,都可以用该接口进行解锁
销毁接口
int pthread_mutex_destory(pthread_mutex_t *mutex);
释放动态开辟的互斥锁的资源
互斥锁的使用(结合黄牛抢票)
1 #include <stdio.h>2 #include <pthread.h>3 #include <unistd.h>4 5 #define THREAD_NUM 26 7 pthread_mutex_t my_lock;//定义一个全局变量8 9 int g_tickets = 100;10
W> 11 void* MyThreadStart(void* arg)12 {13 while(1)14 {15 16 pthread_mutex_lock(&my_lock);//加锁17 18 if(g_tickets > 0)19 {W> 20 printf("i have %d, i am %p\n", g_tickets, pthread_self());21 g_tickets--;22 }23 else24 {25 pthread_mutex_unlock(&my_lock);//解锁126 pthread_exit(NULL);27 }28 pthread_mutex_unlock(&my_lock);//解锁229 }30 return NULL;31 }32 33 int main()34 {35 36 pthread_mutex_init(&my_lock, NULL);//初始化37 38 pthread_t tid[THREAD_NUM];39 for(int i = 0; i < THREAD_NUM; i++)40 {41 int ret = pthread_create(&tid[i], NULL, MyThreadStart, NULL);42 if(ret < 0)43 {44 perror("pthread_create fail\n");45 return 0;46 }47 }48 49 for(int i = 0; i < THREAD_NUM; i++)50 {51 pthread_join(tid[i], NULL);52 }53 54 pthread_mutex_destroy(&my_lock);//释放互斥锁资源55 56 printf("pthread_join end ...\n");57 return 0;58 }
什么时候要初始化互斥锁?
在创建工作线程之前,进行初始化互斥锁
什么时候进行加锁
在执行流访问临界资源前必须加锁操作注意:如果一个执行流加锁成功后再去获取互斥锁,该执行流也会阻塞
如果只加锁运行程序会发生什么?
哦豁,卡死了
使用 gdb attach [pid] 来将gdb附加到进程上
查看当前线程所有线程调用堆栈 back trace ==> bt
thread apply all bt
所有线程的信息就都显示出来了再回头找刚才抢到了票的黄牛
看来是线程3拿到了票
跳转到某个线程的堆栈t [线程编号]
其他的都是库里边的玩意,唯一一个能被我们掌握的也就只有3号了
__owner代表当前互斥锁被线程编号27373拿着,也就是线程3说明:线程3第一次加锁成功打印后第二次再去加锁时卡在pthread_lock接口中,此时我们再打印互斥锁变量时发现它被自己拿着加锁之后一定要记得解锁,否则就会导致死锁
什么时候解锁?
在执行流所有可能退出的地方进行解锁
什么时候释放互斥锁资源?
在所有使用该互斥锁的线程退出之后就可以释放该互斥锁了
线程控制、如何给面试官描述线程不安全的情况?模拟黄牛抢票展现不安全的情况及解决方式、互斥锁加锁解锁相关推荐
- 面试官系统精讲Java源码及大厂真题 - 40 打动面试官:线程池流程编排中的运用实战
40 打动面试官:线程池流程编排中的运用实战 没有智慧的头脑,就像没有蜡烛的灯笼. --托尔斯泰 引导语 在线程池的面试中,面试官除了喜欢问 ThreadPoolExecutor 的底层源码外,还喜欢 ...
- 面试官系统精讲Java源码及大厂真题 - 34 只求问倒:连环相扣系列锁面试题
34 只求问倒:连环相扣系列锁面试题 自信和希望是青年的特权. 引导语 面试中,问锁主要是两方面:锁的日常使用场景 + 锁原理,锁的日常使用场景主要考察对锁 API 的使用熟练度,看看你是否真的使用过 ...
- 面试官一个线程池问题把我问懵逼了。
你好呀,我是why哥. 前几天,有个朋友在微信上找我.他问:why哥,在吗? 我说:发生肾么事了? 他啪的一下就提了一个问题啊,很快. 我大意了,随意瞅了一眼,这题不是很简单吗? 结果没想到里面还隐藏 ...
- adguard没有核心 core no_面试官:线程池如何按照core、max、queue的执行顺序去执行?...
前言 这是一个真实的面试题. 前几天一个朋友在群里分享了他刚刚面试候选者时问的问题:"线程池如何按照core.max.queue的执行循序去执行?". 我们都知道线程池中代码执行顺 ...
- 面试官:线程顺序执行,这么多答案你都答不上来?
前言:最近在面试过程中,发现一些面高程的朋友连怎么实现线程顺序执行都答不上来,特分享相关文章,以作科普,有收获帮忙点个在看,感谢,助中秋快乐! 一.实现 本文使用了8种方法实现在多线程中让线程按顺序运 ...
- main线程 子线程 顺序_面试官:线程池如何按照core、max、queue的执行顺序去执行?详解...
前言 这是一个真实的面试题. 前几天一个朋友在群里分享了他刚刚面试候选者时问的问题:"线程池如何按照core.max.queue的执行循序去执行?". 我们都知道线程池中代码执行顺 ...
- python 等待其他线程执行完_面试官:如何让线程顺序执行,join,还有其他办法吗?...
面试官:如让线程顺序执行? 我:使用Thread的join方法. 面试官:除了join还有别的办法吗? 我:目前只用过join. 面试官:哦,那你了解CountDownLatch吗? 我:不了解,没使 ...
- 【建议珍藏系列】如果你这样回答「什么是线程安全」,面试官都会对你刮目相看!...
戳蓝字"CSDN云计算"关注我们哦! 作者 | 陈树义 责编 | 阿秃 不是线程的安全 面试官问:"什么是线程安全",如果你不能很好的回答,那就请往下看吧. 论 ...
- jmeter 线程执行顺序_面试官让我说出8种线程顺序执行的方法!我懵了
https://www.cnblogs.com/wenjunwei/p/10573289.html 一.前言 本文使用了8种方法实现在多线程中让线程按顺序运行的方法,涉及到多线程中许多常用的方法,不止 ...
最新文章
- 购买阿里云遇到Permission denied的问题
- Hibernate之检索策略
- ABBYY FineReader 12可以内置自动化任务吗
- PHP list的赋值
- LeetCode 1784. 检查二进制字符串字段
- 分区表理论解析(下):SQL Server 2k52k8系列(二)
- 年轻人不要上来就说我要创业
- CentOS 系统sudo命令配置
- SQL Server 2000中数据库质疑的恢复方法
- linux scp 非22端口,[ssh scp sftp] 连接远程ssh非22端口的服务器方法
- (4) IFC属性及属性集 (Industry Foundation Class)
- UltraISO 9.7.0.3476中文完美破解安装版
- HttpUtil工具示例(GET、POST请求)IP工具根据token获取用户信息工具
- iterate java_ibatis中iterate的用法(conjunction=or ,)
- 诺奖背后的一位女性:伯莎·冯·苏特娜
- cscd期刊是c刊吗_武工商C刊和北大核心期刊论文发表数量位列全省同类高校前三甲...
- 嵌入式开发(三):海思Hi3559a交叉编译live555
- 拉斯维加斯国际黑客大会 本周开战
- Unit Test Harness(用具)应该具备什么功能?
- 如何选择语音短信通知?语音短信通知接入教程
热门文章
- Liunx上训练模型的常见情况(不定期更新)
- kafka Linux 下启动服务 测试,Linux下安装部署Kafka分布式集群与测试
- php.ini 没有pdo,php.ini 没有pdo怎么办
- wordpress php 链接,WordPress中获取页面链接和标题的相关PHP函数用法解析
- android超级管理员权限作用,Android获取超级管理员权限的实现
- wrieshark 指令
- 从数学到计算机 从莱布尼兹到冯诺依曼 从数理逻辑到算法分析
- git完全cli指南之详细思维导图整理分享
- python之websocket
- Java 学习笔记(121208)