线程
- 线程概念
- 线程的优点
- 线程的缺点
- 线程异常
- 线程用途
- `linux`进程和线程
- 线程控制
- `pthread` 库
- 创建线程
- 线程等待
- 线程退出
- 线程分离
- 线程互斥
- 线程互斥相关概念
- 线程互斥
- 互斥量
- 互斥量接口
- 初始化互斥量
- 销毁互斥量
- 互斥量的加锁和解锁
- 互斥量实现原理
线程概念
线程是进程中的一个执行流
一个进程可以有一到多个执行流
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
在
linux
中,进程和线程的数据结构都是pcb
,线程是CPU调度的基本单位进程的中的执行流共享同一个
mm_struct
和页表
虚拟地址通过页表转换到物理地址
首先,物理地址会被分成一个个的page,每个page为
4kb
,而且页表不只是包含虚拟地址,和物理地址,还包括是否命中,RWX
权限,UK
权限是否命中:访问的数据是否在物理内存中
RWX
权限:读写权限U/K权限:当前是用户级页表还是内核级页表
char* msg="hello world"; *msg='a';
上述代码编译没有问题,但是在执行时会报错,就是因为在运行,执行代码时,在页表转换时,是只读权限,而这句代码需要写的权限,所以运行报错
还有,页表并不是我们想象中直接从32/64位虚拟地址映射到32/64位物理地址,而是将虚拟地址分为3部分(10位/10位/12位),使用前10位通过一级页表(页目录)映射到二级页表,使用第二个10位映射到物理内存的某一个page的起始地址,最后12位正好对应每一个page的大小(
4kb
)
线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程的缺点
- 性能损失(如果计算密集型的线程数量比可用的处理器多,增加了同步和调度的开销,造成了性能损失)
- 健壮性降低,线程不具有独立性,线程是不具有保护的
- 缺乏访问控制
- 编程难度提高
线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,进程内的所有线程也就随即退出
线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验
linux
进程和线程
- 进程是资源分配的基本单位
- 线程调度的基本单位
- 线程共享进程数据,但也拥有自己的一部分数据
- 线程ID(
LWP
)- 一组寄存器数据
- 栈
errno
- 信号屏蔽字
- 调度优先级
进程的多个线程共享同一地址空间,因此代码段、数据段都是共享的,在一个线程中定义一个全局变量或一个函数,在其他线程也可以访问的,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式
- 当前工作目录
- 用户id和组id
线程控制
pthread
库
pthread
库是一个动态库在内核中并没有线程,是使用
LWP
(轻量级进程)模拟的,而在用户层面需要创建进程、等待进程……,所以在在用户层和内核之间使用pthread
库来实现。比如:用户创建线程,通过线程库,在内核中创建轻量级进程。而且管理线程(先描述,再组织)是在
pthread
库中进行的线程栈:每个线程有自己独立的栈,而主线程则直接使用进程地址空间中的栈区;
线程局部存储:同一个进程的多个线程对于全局变量是共享的,那么如果我们想让每个线程对于这个全局变量是独立的,那么每个线程的独立的全局变量就存储再线程局部存储中。
__thread int global=100; //在全局变量前加上__thread,线性局部存储
pthread_t
类型是线程ID,但是和LWP
(线程ID)不同
LWP
(线程ID)是属于进程调度的范畴,内核中,线程是用轻量级进程模拟的,是操作系统调度的基本单位,LWP
(线程ID)是用来唯一的标识线程的。
pthread_t
类型的线程ID,本质上是一个地址,这个地址指向pthread
动态库中的线程控制块,
创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg) /创建线程 /thread: 线程ID(但不是LWP) /attr: 设置线程ID,NULL为线程的默认属性 /start_routine: 该线程执行的函数 /arg: 传给函数的参数 /返回值:成功返回0,失败返回错误码
线程等待
int pthread_join(pthread_t thread, void **value_ptr);//线程退出后,需要进行线程等待,才能释放线程的资源 /thread :线程ID /value_ptr :一个输出型参数,指向线程的返回码 /返回值:成功返回0,失败返回错误码 获取退出进程的退出码: void *retval=nullptr; pthread_join(tid1,&retval); cout<<*((int*)retval)<<endl; /退出码
代码
void *func1(void *args) { while (true) { string name = (char *)args; cout << name << "正在运行" << endl; sleep(1); } } void *func2(void *args) { while (true) { string name = (char *)args; cout << name << "正在运行" << endl; sleep(1); } } int main() { pthread_t tid1, tid2; / 创建线程 pthread_create(&tid1, nullptr, func1, (void *)"thread1"); pthread_create(&tid2, nullptr, func2, (void *)"thread2"); while (true) { cout << "主线程正在运行" << endl; sleep(1); } / 释放线程 pthread_join(tid1, nullptr); pthread_join(tid2, nullptr); return 0; }
查看进程
ps axj|head -1 && ps axj|grep mytest
查看线程
ps -aL
LWP
:线程ID
查看线程自己的ID
pthread_t pthread_self(void);
void *func1(void *args) { sleep(1); pthread_t id = pthread_self(); cout << "tid1: " << id << endl; } void *func2(void *args) { sleep(2); pthread_t id = pthread_self(); cout << "tid2: " << id << endl; } int main() { pthread_t tid1, tid2; pthread_create(&tid1, nullptr, func1, (void *)"thread1"); pthread_create(&tid2, nullptr, func2, (void *)"thread2"); sleep(3); pthread_t id = pthread_self(); cout << "主线程: " << id << endl; pthread_join(tid1, nullptr); pthread_join(tid2, nullptr); return 0; }
线程退出
**
pthread_exit
函数 **
void pthread_exit(void *value_ptr);/线程退出 /等同于return (void*)退出码 例子: int *p =new int(10); pthread_exit((void*)p);
pthread_cancel
函数
int pthread_cancel(pthread_t thread);/取消一个执行中的线程 /thread 线程ID
return 退出码
return (void*)退出码; 例子: int *p =new int(10); return (void*)p;
注意:主线程结束,整个进程直接而结束
线程分离
int pthread_detach(pthread_t thread);/分离线程,不需要回收线程,操作系统直接释放线程 /可以由本线程分离,也可由其他线程分离 /一个线程如果分离了,就不能对该线程进行线程等待
线程互斥
线程互斥相关概念
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
线程互斥
为什么需要线程互斥,看一个例子
一个关于抢票的代码:
int tickets = 10000; / 抢票 void *function(void *argc) { while (true) { if (tickets > 0) { usleep(3000); cout << (char *)argc << " ticket:" << tickets << endl; tickets--; } else { cout << "failed ticket:" << tickets << endl; break; } } } int main() { pthread_t tid1, tid2, tid3, tid4; / 创建线程 pthread_create(&tid1, nullptr, function, (void *)"thread1"); pthread_create(&tid2, nullptr, function, (void *)"thread2"); pthread_create(&tid3, nullptr, function, (void *)"thread3"); pthread_create(&tid4, nullptr, function, (void *)"thread4"); / 主线程 cout << tickets << endl; pthread_join(tid1, nullptr); pthread_join(tid2, nullptr); pthread_join(tid3, nullptr); pthread_join(tid4, nullptr); return 0; }
为什么会出现上面这种情况?
因为在判断完票数不为零和
tickets--
之间,发生了线程切换(因为多个线程并发执行),其他线程票数–了,等线程再切回来,再–,票数就可能变成负数
所以需要满足以下三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
为了解决这个问题,满足这三点,就需要加锁
互斥量
LINUX
中把加的这把锁,叫做互斥量加锁的本质就是将加锁的这段代码由并发执行变为串行执行
一般情况下,加锁的粒度越小越好,只给临界区加锁
互斥量接口
初始化互斥量
静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrictattr); /mutex:要初始化的互斥量 /attr:NULL
销毁互斥量
使用
PTHREAD_MUTEX_INITIALIZER
初始化的互斥量不需要销毁不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量的加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex); /加锁 /返回值:成功返回0,失败返回错误号
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么
pthread_lock
调用会陷入阻塞(执行流被挂起),等待互斥量解锁。int pthread_mutex_unlock(pthread_mutex_t *mutex); /解锁 /返回值:成功返回0,失败返回错误号
改进抢票代码:
int tickets = 10000; pthread_mutex_t mymutex; / 抢票 void *function(void *argc) { while (true) { pthread_mutex_lock(&mymutex); if (tickets > 0) { usleep(3000); cout << (char *)argc << " ticket:" << tickets << endl; tickets--; pthread_mutex_unlock(&mymutex); } else { pthread_mutex_unlock(&mymutex); cout << "failed ticket:" << tickets << endl; break; } } } int main() { pthread_mutex_init(&mymutex,nullptr); pthread_t tid1, tid2, tid3, tid4; / 创建线程 pthread_create(&tid1, nullptr, function, (void *)"thread1"); pthread_create(&tid2, nullptr, function, (void *)"thread2"); pthread_create(&tid3, nullptr, function, (void *)"thread3"); pthread_create(&tid4, nullptr, function, (void *)"thread4"); / 主线程 cout << tickets << endl; pthread_join(tid1, nullptr); pthread_join(tid2, nullptr); pthread_join(tid3, nullptr); pthread_join(tid4, nullptr); pthread_mutex_destroy(&mymutex); return 0; }
互斥量实现原理
如上图代码:
加锁:
- 将0写入寄存器
- 交换寄存器和内存中
mutex
的数据- 如果寄存器大于零,加锁成功,否则,阻塞等待