线程控制、如何给面试官描述线程不安全的情况?模拟黄牛抢票展现不安全的情况及解决方式、互斥锁加锁解锁

4 篇文章 0 订阅
3 篇文章 0 订阅

线程概念

一个进程当中一定存在一个主线程,执行main函数的线程就称为主线程

其他线程都被称之为工作线程

进程本质上是线程组,换句话说,线程组被称之为进程,线程也可以被称为轻量级进程(LWP),因为在操作系统内核当中不存在线程的概念

pid:轻量级进程id,也被称之为线程id

tgid:轻量级进程组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 4
  6 
  7 struct ThreadId
  8 {
  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 4
  6 
  7 struct ThreadId
  8 {
  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,进行++后回写到内存当中,内存当中的值也是11
	3、总结
	线程A对全局变量i加了一次,线程B也对全局变量i加了一次,但最终i的值变成了11,并不是12,由此就产生了线程不安全的情况,即产生了多
	个线程使用临界资源时有可能产生的二义性

线程不安全的情况(模拟黄牛抢票)

    1 #include <stdio.h>
    3 #include <unistd.h>
    4 
    5 #define THREAD_NUM 4  //设定黄牛的数量为4
    6 
    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,一种是0
	1:表示当前临界资源可以被访问
	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 _TIMESPEC
	struct timespec {
	time_t tv_sec;//seconds
	long 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 2
    6 
    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     else
   24     {
   25       pthread_mutex_unlock(&my_lock);//解锁1
   26       pthread_exit(NULL);
   27     }
   28     pthread_mutex_unlock(&my_lock);//解锁2
   29   }
   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

在这里插入图片描述

所有线程的信息就都显示出来了

再回头找刚才抢到了票的黄牛

在这里插入图片描述
0x7fbc6855d700
看来是线程3拿到了票

跳转到某个线程的堆栈
	t [线程编号]

在这里插入图片描述

其他的都是库里边的玩意,唯一一个能被我们掌握的也就只有3号了

在这里插入图片描述

__owner代表当前互斥锁被线程编号27373拿着,也就是线程3

说明:线程3第一次加锁成功打印后第二次再去加锁时卡在pthread_lock接口中,此时我们再打印互斥锁变量时发现它被自己拿着

加锁之后一定要记得解锁,否则就会导致死锁
什么时候解锁?
	在执行流所有可能退出的地方进行解锁
什么时候释放互斥锁资源?
	在所有使用该互斥锁的线程退出之后就可以释放该互斥锁了
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值