目录
编辑
1.什么是进程,线程,并发,并行
优点
缺点
什么资源是线程应该私有的呢
为什么线程切换成本更低呢
3.线程控制
pthread_create
lpthread选项
makefile
代码实现
ps -aL
什么是LWP
轻量级进程ID与进程ID之间的区别
LWP与pthread_create创建的线程之间的关系
4.线程中止,等待,分离
pthread_exit函数
pthread_cancel函数
线程等待
pthread_join编辑
线程分离
pthread_detach
5.线程互斥
进程线程间的互斥相关背景概念
多线程共享资源访问的不安全问题(举个抢票的例子)
锁
互斥量的接口
今天推荐一首歌曲
黄昏 周传雄
依然记得从你口中~
说出再见坚决如铁~
昏暗中有种烈日灼身的错觉~
黄昏的地平线~
划出一句离别~
爱情进入永夜~
开始我们的学习吧!
1.什么是进程,线程,并发,并行
进程是资源分配的最小单位,有独立的地址空间和系统资源。
线程是cpu调度,程序执行的最小单位,一个线程只属于一个进程,而一个进程可以有多个线程,多个线程共享同一个进程的资源。在多核系统下允许几个线程各自独立的在处理器上运行,操作系统提供线程就是为了方便有效地实现这种并发性。
一切进程至少都有一个执行线程
线程在进程内部运行,本质是在进程地址空间内运行
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
多进程:同时运行QQ、微信、浏览器 , 多线程: 用浏览器同时进行浏览网页、播放视频)
并发是把cpu运行时间划分成若干个时间段,每个时间段再分配给各个线程执行, 当一个线程在运行时,其他线程处于挂起状态。
并行是同一时刻当一个cpu执行一个线程时,另一个cpu可以执行另一个线程,两个线程互不抢占cpu资源,是真正意义上的 不同线程在同一时刻同时执行
形象地解释:
进程是拥有一系列资源的集合,这些资源包括内存空间、内核对象、资源文件等等。我们将进程理解为一个工厂,工厂本身不能运作,需要有人来操作。那么这些工人就是线程,每一个工人操作自己的一台设备,这个设备就可以看成是线程的栈,他由这个工人自己使用。一个工厂里有多台设备时,如果只有一个人那么他就需要去一个个的去操作工厂里的设备,如果这些设备需要同时运行,那么这样操作效率太低。因此,工厂会多聘用几个工人,他们每个人操作自己的设备,这样效率就会大大提高。工人在操作设备时,可能两个人需要使用同一个工具,这个工具是全局的变量,因此他们可以共同访问,但是一个工人要去使用这个工具时,他会等在那里,等另一个人使用完,然后他就可以接过工具,继续干活了,这就是线程的同步。创建多个工厂就是多进程程序。工人操作的每台设备还是属于该工厂,因此线程是依附于进程的,占用进程的地址空间,线程之间也可以相互访问对方的地址,需要通过传址能实现,但是一般不会出现这样的情况,试想能有多大的机会在一个函数中访问另一个函数的的局部变量。在代码的实现中,我们可以将线程仅仅看成一函数去分析,只不过他是并发进行的。
工厂就是进程,工人就是线程,工厂所占的位置就是进程空间,工厂里的设备和工具就是数据和资源,多个工人同时工作就是多线程,几个工人要用同时使用一个工具就是线程同步。
重点
进程是资源分配的基本单位,线程是调度的基本单位
线程独有:栈,寄存器,信号屏蔽字,errno...等信息,因此各个线程各自有各自的栈区,但是堆区共用
任何一个线程都可以创建或撤销另一个线程
进程比线程安全的原因是每个进程有独立的虚拟地址空间
Linux内核中有没有真正意义上的线程呢?没有,linux用进程的PCB来模拟线程,是完全属于自己实现的一套方案!
站在CPU的角度来看,每一个PCB,都可以称之为轻量级进程,因为它只需要PCB即可,而进程承担分配的资源更多,量级更重!
Linux线程是CPU调度的基本单位,进程是承担分配系统资源的基本实体!
2.线程的优缺点
优点
缺点
什么资源是线程应该私有的呢
重要的两点
线程的上下文结构也必须是线程的私有资源。(寄存器)
每个线程都有自己的私有栈结构
为什么线程切换成本更低呢
3.线程控制
在Linux下,PCB<=其他OS内的PCB(进程控制块)
Linux下的进程统称为,轻量级进程
pthread_create
pthread_create是创建线程的一个接口
lpthread选项
(如果在编译时不带-lpthread选项,可以看到g++报错pthread_create()函数未定义,其实就是因为链接器链接不上具体的动态库,此时就可以看出来linux内核中并没有真正意义的线程,他无法提供创建线程的接口,而只能通过第三方库libpthread.so或libpthread.a来提供创建线程的接口。)
makefile
//编写makefile 带上 -lpthread
test:test.cppg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f test
代码实现
#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;void *threadRoutine(void *args)//新线程回调在这里
{
while(true)
{
cout<<"新线程:"<<(char*)args <<" running..."<< endl;
sleep(1);
}}int main()
{pthread_t tid; //创建线程pthread_create(&tid,nullptr,threadRoutine,(void*)"thread 1");//进程的名字//下面是主线程
while(true)
{
cout<<"main线程:"<<" running..."<< endl;
sleep(1);
}
}
ps -aL
ps命令用于查看进程信息,其中-L选项用于查看轻量级进程信息
pthread_self() 用于获取用户态线程的tid,而并非轻量级进程ID
getpid() 用于获取当前进程的id,而并非某个特定轻量级进程
通过ps -aL就可以看到正在运行的线程有哪些,可以看到有两个标识符,一个是PID,一个是LWP(light weight process),所以CPU在调度那么多的PCB时,其实是以LWP作为每个PCB的标识符,以此来区分进程中的多个轻量级进程。
主线程的PID和LWP是相同的,所以从CPU调度的角度来看,如果进程内只有一个执行流,那么LWP和PID标识符对于CPU来说都是等价的,但当进程内有多个执行流时,CPU是以LWP作为标识符来调度线程,而不是以PID来进行调度。
什么是LWP
LWP(轻量级进程)是操作系统中用于调度和管理线程的内核层面的实体。
LWP是轻量级进程,在Linux下进程是资源分配的基本单位,线程是cpu调度的基本单位,而线程使用进程pcb描述实现,并且同一个进程中的所有pcb共用同一个虚拟地址空间,因此相较于传统进程更加的轻量化
轻量级进程ID与进程ID之间的区别
-
概念:
- 进程ID(PID)是操作系统为每个正在运行的进程分配的唯一标识符。它是在进程创建时由操作系统分配的,并在整个进程的生命周期中保持不变。
- 轻量级进程ID(LWP ID)是在多线程操作系统中,用于标识线程或轻量级进程(也称为执行上下文)的标识符。一个进程可以包含多个轻量级进程,并且每个轻量级进程都有自己的LWP ID。
-
操作方式:
- 对于PID,通常可以使用系统调用(如fork()和exec())创建新进程或操作现有进程。
- 对于LWP ID,通常使用线程库(如pthread)创建新线程或操作现有线程。一个进程的LWP ID只在该进程内部可见,对于其他进程来说是不可见的。
-
调度与资源分配:
- 操作系统通过PID进行进程调度和资源分配。进程调度通常是基于运行队列中进程的优先级和调度算法来进行决策的。
- 在多线程操作系统中,内核会将CPU时间划分给不同的LWP。调度和资源分配是基于LWP而不是整个进程进行的。
-
上下文切换:
- 在进程切换时,操作系统需要保存和恢复整个进程的上下文,这包括进程的寄存器状态、打开的文件、堆栈等。
- 在轻量级进程切换时,只需要保存和恢复当前线程的上下文,这是因为同一进程内的线程共享同一内存空间和打开的文件。
需要注意的是,LWP ID是在多线程操作系统中使用的概念,而PID是在所有操作系统中都存在的概念。在某些操作系统中,LWP ID可能与线程ID(TID)或任务ID(TID)等概念等效或相似。
LWP与pthread_create创建的线程之间的关系
LWP(轻量级进程)与pthread_create创建的线程之间存在一种关系,可以理解为LWP是内核层面对线程的调度和管理的实体,而pthread_create创建的线程则是用户层面对线程的抽象。
具体来说,pthread_create是一个线程库函数,用于在用户空间创建一个新的线程。这个线程由操作系统内核分配一个LWP,并将其标识为一个用户线程,也称为轻量级进程。该LWP会在它所属的进程中与其他LWP共享进程的地址空间、文件描述符等资源。
LWP与pthread_create创建的线程之间的关系可以总结如下:
- 一个进程可以包含多个LWP,每个LWP都有一个唯一的LWP ID。
- 每个LWP可以与一个或多个pthread_create创建的线程相关联,这些线程共享其所属进程的资源。
- 每个pthread_create创建的线程有自己的线程ID,并且在用户层面上可见,可以使用线程库提供的函数进行操作和管理。
需要注意的是,LWP的创建和管理是由操作系统内核完成的,而pthread_create函数是线程库提供的接口,它在内部会使用操作系统提供的系统调用来创建和管理LWP。因此,对于用户来说,他们只需要使用pthread_create接口来创建和操作线程,而无需直接与LWP进行交互。
4.线程中止,等待,分离
线程终止总共有三种方式,分别为return,pthread_exit,pthread_cancel
pthread_exit函数
那个线程调用pthread_exit函数, 那个线程就退出。俗称“谁调用谁退出”
pthread_cancel函数
在有多个线程的情况下,主线程调用pthread_cancel(pthread_self()), 则主线程状态为Z, 其他线程正常运行
主线程调用pthread_exit只是退出主线程,并不会导致进程的退出
线程等待
与进程类似,进程退出之后要被等待,也就是回收进程的资源,否则会出现僵尸进程,僵尸的这种状态可以通过ps指令+axj选项看到,同时会产生内存泄露的问题。
线程终止同样也需要被等待,但线程这里没有僵尸线程这样的概念,如果不等待线程同样也会造成资源泄露,也就是PCB资源未被回收,线程退出的状态我们是无法看到的,我们只能看到进程的Z状态。
pthread_join
原生线程库给我们提供了对应的等待线程的接口,其中join的第二个参数是一个输出型参数,在join的内部会拿到线程函数的返回值,然后将返回值的内容写到这个输出型参数指向的变量里面,也就是写到我们用户定义的ret指针变量里,通过这样的方式来拿到线程函数的返回值。
线程分离
若要进行分离,推荐创建完线程之后立马设置分离
pthread_detach
新创建出来的线程默认状态是joinable的,也就是说你必须通过pthread_join去等待线程,否则就会造成内存泄露。
但如果我们压根就不想等待线程,那调用pthread_join就是一种负担,这个时候我们就可以通过分离线程的手段,来告诉操作系统,现在我这个线程要和进程分离了,我不再共享进程的地址空间了,我也不要进程的任何资源了,我们俩人以后就形同陌路,互不相干了!操作系统你现在就把我回收吧,我已经和进程没有任何关系了!
所以在设置线程为分离状态后,操作系统会立即回收线程的所有资源,而不需要等待线程自动退出或者是手动来释放资源,表示我们现在已经不关心这个线程了!
joinable和detach是线程的两个对立的状态,一个线程不能既是joinable又是分离的,并且如果线程被设置为detach,那么就不可以用join来等待线程,否则是会报错的!
int pthread_detach(pthread_t thread);
5.线程互斥
进程线程间的互斥相关背景概念
临界资源: 多线程执行流共享的资源就叫做临界资源临界区: 每个线程内部,访问临界资源的代码,就叫做临界区互斥: 任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用原子性: 不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
多线程共享资源访问的不安全问题(举个抢票的例子)
假设现在有一份共享资源tickets,如果我们想让多个线程都对这个资源进行操作,也就是tickets- -的操作,但下面两份代码分别出现了不同的结果,上面代码并没有出现问题,而下面代码却出现了票为负数的情况,这是怎么回事呢?
其实问题产生就是由于多线程被调度器调度的特性导致的。
抢票代码
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
#include<cstdio>
#include<iostream>using namespace std;//加锁保护
//pthread_mutex_t mtx =PTHREAD_MUTEX_INITIALIZER;
//pthread_mutex_t 是原生线程库提供的一个数据类型int tickets=3000;
//在并发访问的时候,会导致数据不一致的问题void *gettickets(void *args)
{(void*)args; //每个线程内部,访问临界资源的代码,就叫做临界区while(true){//pthread_mutex_lock(&mtx); //加锁if(tickets>0){usleep(10000);printf("%s:%d\n",(char*)args,tickets);tickets--;// pthread_mutex_unlock(&mtx); //解锁}else{// pthread_mutex_unlock(&mtx); break;}}return nullptr;
}int main()
{pthread_t t1,t2,t3;//线程创建pthread_create(&t1,nullptr,gettickets,(void*)"thread one");pthread_create(&t2,nullptr,gettickets,(void*)"thread two");pthread_create(&t3,nullptr,gettickets,(void*)"thread three");//线程等待pthread_join(t1,nullptr);pthread_join(t2,nullptr);pthread_join(t3,nullptr);return 0;
}
结果会出现负数(多线程在并发访问的时候,可能会导致数据不一致的问题)
锁
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量
谁持有锁谁才能进入临界区,你没有锁那就只能在临界区外面乖乖的阻塞等待,等待锁被释放,然后你去竞争这把锁,竞争到就拿着锁进入临界区执行代码,竞争不到就老样子,继续乖乖的在临界区外面阻塞等待
互斥量的接口
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t*restrict attr);参数:mutex:要初始化的互斥量attr:NULL
把上面的代码改成这样,就不会出现出现负数的问题,这里用的是静态分配
//加锁保护
pthread_mutex_t mtx =PTHREAD_MUTEX_INITIALIZER; //pthread_mutex_t 是原生线程库提供的一个数据类型 ,静态int tickets=3000;
//在并发访问的时候,会导致数据不一致的问题void *gettickets(void *args)
{(void*)args; //每个线程内部,访问临界资源的代码,就叫做临界区while(true){pthread_mutex_lock(&mtx); //加锁if(tickets>0){usleep(10000);printf("%s:%d\n",(char*)args,tickets);tickets--;pthread_mutex_unlock(&mtx); //解锁}else{pthread_mutex_unlock(&mtx); break;}}return nullptr;
}