线程

了解线程

1 什么是线程?
进程中的一个执行路线就是一个线程,线程是进程中的控制序列
线程可以被理解为是轻量级的进程
一个进程至少有一个线程
线程是在进程内部进行运行的,本质是在进程的地址空间内运行

2 线程的优点
创建线程的带价比创建进程代价小
线程之间的切换需要OS做的工作更小
线程能提高处理器的并行数量
在计算密集型的系统中,为了能在多处理器系统上运行能够将资源分解到线程中计算
在I/O密集型的时候,线程能实现同时等待不同的I/O操作

**3  线程的缺点**

  1  健壮性差 ,局部小错误对整体影响有时也很大,   解引用野指针,空指针
  2  性能影响:
       比如计算密集型while (1) ; 自己会独占一个处理器,导致其他线程暂时无法共享处理器,这就会以牺牲CPU时间为代价进行调度
       

3 缺乏线程访问控制机制,
      进程是访问调度的基本粒度.
  4 编程困难,因为你要考虑多个线程的时间以及执行顺序,这样会复杂很多. 
   
  用途:

在I/O密集型能提高用户的体验舒适度
    在CPU集型的程序中能提高程序的执行效率

线程和进程的区别

	1  线程进行资源调度
   	进程资源分配的基本单位
	2 线程共享进程数据,也拥有自己的一份数据  :
					1  线程id  
					2 一组寄存器 
	 			3 errno
	 			4 栈
					5 信号屏蔽字
					6   调度优先级
	3  进程的所有线程共享同一地址空间,所以共享数据段和代码段
		   线程共享 文件描述符
		   					
  ***

线程同步与互斥原理

    1  线程不是越多越好,达到一定数量效率就不再提升
    2  如果多个线程访问同一个资源就会发生互斥(就像打架)
    3  线程如果某一个线程始终得不到资源,就会处于处于饥饿状态
    4  如果一个线程崩溃了,整个进程也就崩溃了,其他线程也就停止了

1 线程的调用必须包含头文件 “pthread.h”

2 线程控制相关函数不是系统调用,因为系统只认进程,不认线程

3 **线程的控制相关函数都在库函数 ============>>posix 线程库**

4 thread的相关函数都是以 pthread_开头,pthread_ 中的p就指 posix线程库

通过 ps -eLf 查看所有的线程id LWP 这一列表示线程的id

ps 得到的id 是站在内核的角度给PCB加了一个编号,
pthread_self 是站在posix 线程库角度上的编号 ,其实两者是一样的

我们可以通过 pstack pid的线程号来查看是一致的

thread n 切换到 n号线程 再bt就可以查看调用栈

进程在内核中具有进程描述符,同样的线程也在内核中具有进程描述符, .
多线程的进程 又叫线程组,线程组 内每个线程都再内核中存在一个 进程描述符与之对应,进程描述符结构体中的pid ,表面上看是进程的ID ,其实是线程的ID,进程描述符中的tgid 对应用户层面的进程ID

怎么查看进程的id

ps -eLf
LWP 对应线程的 id , 即gettid() 的返回值
NLWP 对应进程的个数 PID 对应进程号

gettid() 函数并未被封装在 POSIX库,如果真需要使用线程的ID 我们需要包含头文件

#include “sys/syscall.h” 在 pid_t tid tid =syscall(SYS_gettid) ;

**内核在创建线程时,
1 **会将线程组中第一个线程的id 设为该线程组的组长id 即进程描述符,组长id 在用户态被称为主线程 ,在内核中称为组长,该主线程的id 等于进程的id

2 线程组中其他的线程id 则有内核负责分配. 线程组的id 和主线程的 id 一样的,不论线程怎样创建.

最后
pthread_t create(&tid ,NULL,handler,void* arg) 该函数中的tid 属于 NPTL(标准线程裤)范畴,不属进程调度范畴.

pthread_create 函数成功 返回0 失败将 -1;但不设置全局变量errno。

另外,获得线程自身tid 可以用
pthread_t pthread_self(void)函数
/*#include “sys/syscall.h” 在 pid_t tid tid =syscall(SYS_gettid) ;
最后 线程之间人人平等,不存在父子关系之称

在这里插入图片描述

在这里插入图片描述
测试进程结束之后线程肯定不会执行:
主线程执行结束了,return 掉了
在这里插入图片描述

在这里插入图片描述

多线程的进程,又被称为线程组,线程组内每一个线程在内核中都存在一个进程描述符(task_struct);即一个task_struct含有多个pid,这些pid表面上看是进程id,实则是线程id

ps -L显示

LWP线程id NLWP 线程数

pthread_create的第一个参数是线程ID,这个ID和轻量级线程ID不一样 ,轻量级的线程ID是用于系统调度的最小单位,所以需要一个数值唯一表示该线程

而pthread_create 产生的线程ID是第一个参数的地址,是在标准线程裤(NPTL)线程裤的基础上定义的,后续的线程裤操作都在这个地址 线程ID上进行。

查看ID函数
pthread_t pthread_self(void);

来看几个线程的函数’

线程终止的方法:
1 pthread_exit;
2 `pthread_cancel
3 从线程函数return ,这种方法对主线程不适用,从main函数return相当于调用exit终结进程
4 线程入口函数结束,线程结束
5 进程结束 ,所以线程结束

1 void pthread_exit(void * value_ptr)
功能 :线程终止自己
注意 value_ptr 不能指向局部变量,只能指向全局变量或者 malloc 出来的变量的地址,

2 int pthread_cancel(pthread_t thread) 不太建议使用
功能 : 线程终止同一进程中的自己调用的其他线程,
参数: 其他线程的id
返回值: 成功返回0 失败返回错误码
本函数 不建议使用,因为会稍等一会才会终止线程

3 pthread_join(pthread_t thread,void** value_ptr)
功能:该线程等待参数id的线程结束,等待其结束的结果.
参数: thread 等待线程的id
value _ptr: 指向一个指针,这个指针指向参数线程的返回值.
注意这个参数是void 输出型出参数 ,若不关注它,用NULL替代**
返回值: 成功返回0 失败返回错误码

pthread_join 函数的第二个参数有多种 情况,但都对应的是参数进程的一种结果 ,如果不关心 ,可以将第二个参数设置为NULL;

默认情况下创建的线程都是需要等待回收资源的,即pthread_join(pthread_t tid,void** ret);

若不关心线程的返回值,join 也是一种负担,这个时候,我们用pthread_detach函数告诉系统,当线程退出时,自动释放线程资源。

pthread_datach(tid);
pthread_detach ( pthread_self() );
//将线程自己和其他线程分离
可以是线程组内其他线程进行分离,也可以是线程自己分离。

pthread_detach  线程分离  使用之后就不需要进行线程等待,他不关注线程的结果. 
线程等待的目的:
在线程执行完毕后,如果不进行线程资源回收,将导致进程的一部分地址空间不能使用,造成内存泄漏.

线程互斥

先看几个概念:

临界资源: 多线程的执行流共享的资源称为临界资源

临界区: 共享临界资源的代码成为临界区(代码)

互斥: 同一时刻只能由一个执行流进入临界区,此时别的执行流不能进入.

原子性: 完成性的意思,不可分割!

互斥量: 有时候静态变量或者局部变量可以通过数据共享,完成线程间的交互, eg :循环变化

多线程并发操作共享变量,带来一些问题, 抢占式就是 万恶之源

下面是我在虚拟机终端上拷贝过来的代码,使用时 不要每行的序号

    1 #include"stdio.h"
    2 #include"pthread.h"
    3 #include"unistd.h"
    4 int ticket=100;
    5 void* Sell(void* arg)
    6 {
    7   char* id=(char*)arg;
    8 while(1)
    9 {
   10 if(ticket>0)
   11 {
   12   usleep(10000);                                                                                                                                                                      
   13   --ticket;
   14   printf("出票成功:%s,当前余量%d 张\n",id,ticket);
   15 }
   16 else
   17 break;
   18 }
   19 return NULL;
   20 }
   21 int main()
   22 {
   23   pthread_t t1, t2 ,t3 ,t4;
W> 24     char*p1 ="pthread 1";
W> 25     char*p2 ="pthread 2";
W> 26     char*p3 ="pthread 3";
W> 27     char*p4 ="pthread 4";
   28   pthread_create(&t1,NULL,Sell,p1);
   29   pthread_create(&t2,NULL,Sell,p2);
   30   pthread_create(&t3,NULL,Sell,p3);
   31   pthread_create(&t4,NULL,Sell,p4);
   32 
   33   pthread_join(t1,NULL);
   34   pthread_join(t2,NULL);
   35   pthread_join(t3,NULL);
   36   pthread_join(t4,NULL);
   37   return 0;
   38 }

在这里插入图片描述
我们 看到 不仅顺序有问题(其实是线程抢占式执行的结果),还有延迟,产生了-1 -2 这样的共享问题,其实是抢占的结果

下面加上互斥锁

#include"stdio.h"
#include"unistd.h"
#include"pthread.h"

int ticket=100;//初始值设为100
pthread_mutex_t mutex;

void* Windows(void* arg )
{
  char* A=(char*)arg;
  while(1)
  {
    pthread_mutex_lock(&mutex);//为保证出票期间线程安全,我们给在操纵数据时进行加锁


    if(ticket >0)
    {
      //  我尝试了下 如果 sleep久了 ,就会导致其他线程饥饿.
      sleep(10);//这里模拟出票是需要一丢丢时间的,否则怎么会发生线程抢占导致不安全呢,对吧?
      --ticket;
      printf(" pthread_thread   %s 出票成功 ,当前余量:%d \n ",A,ticket);
      pthread_mutex_unlock(&mutex);//操作结束,记得解 锁  即 放开 CPU
    }


    else
    {
      pthread_mutex_unlock(&mutex);
      break;
    }
  }
return NULL;
}
  int main()
  {
    pthread_t t1, t2 ,t3 ,t4;
    pthread_mutex_init(&mutex,NULL);//初始化互斥量

    char* a[5]={ "pthread 1","pthread 2","pthread 3","pthread 4",NULL};

    pthread_create(&t1,NULL,Windows, a[0]);
    pthread_create(&t2,NULL,Windows, a[1]);
    pthread_create(&t3,NULL,Windows, a[2]);
    pthread_create(&t4,NULL,Windows, a[3]);

    //  下面我们注意解决下出现僵尸线程,进行一下线程等待,我们暂不关注被等线程的结果

    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    pthread_join(t3,NULL);
    pthread_join(t4,NULL);

    pthread_mutex_destroy(&mutex);
    return 0;
  }

这样执行就没问题了 .
思路 被访问期间排斥别的线程访问

不加锁可能会导致的后果就是破坏原子性
互斥就需要的锁 在 linux 中 称为 互斥量

好了 ,现在我们来看看互斥量

互斥量我们常用 mutex 来表示
pthread_mutex_t mutex

1 互斥量需要初始化

两种方法 1 静态分配
2 动态分配
静态分配:pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER

初始化英语 initialize 全大写再多了一个R

动态分配: int pthread_mutex_init(pthread_mutex_t *restrict mutex ,const pthread_mutexattr_t *restrict attr)

互斥量加锁 解锁:
pthread_mutex_lock(pthread_mutex_t* mutex);
pthread_mutex_unlock(pthread_mutex_ *mutex);
成功返回 0 失败返回 错误号

注意 执行pthread_mutex_lock时 两种情况
1 未上锁则上锁
2 上锁了 或者跟别的线程抢占时没争到锁的控制权,均会挂起等待.

最后来介绍一下 可重入

什么是可重入:
一个函数在任意顺序的执行流调用结果不会受到影响,即都是安全的,

一般不可重入的情况:
1 调用malloc 或者new 动态开辟出来的空间的操作,因为malloc使用全局链表来管理堆.
2 调用了标准I/O 库函数,标准I/O库的很多实现都是以不可重入的方式使用全局数据结构
3 可重用函数体内使用 静态 或 全局的数据结构.

4 函数是可重入的,线程是安全的,否则可能不安全

可重入与线程安全的区别:
1 可重入函数是线程安全的一种
2 线程安全不一定可重入,可重入函数线程一定安全
3对临界资源访问加锁,则这个函数是线程安全的,但若未释放锁就会导致死锁,这时不可重入.

最后 死锁的四个必要条件

1 互斥
2 请求与保持(也就是上了锁未释放锁)
3 不剥夺(别的线程不能强行剥夺)
4循环等待(若干资源首尾相接循环等待)

四个避免死锁的方法:

1 破坏四个必要条件之一
2 加锁顺序一致
3 申请锁要及时释放
4 一次性分配好资源

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值