了解线程
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 一次性分配好资源