多线程学习

什么是多线程

那什么是线程呢?线程可以理解成是在进程中独立运行的子任务。比如QQ.exe运行时就会有很多的子任务在同时运行。比如,好友视频线程、下载文件线程、传输数据线程、发送表情线程等。这些不同的任务或者说功能都可以同时运行,其中每一个任务完全可以理解成是“线程”在工作,传文件、听音乐、发送图片表情等功能都有对应的线程在后台默默地运行。
在这里插入图片描述
任务1和任务2是两个完全独立、互不相关的两个任务。

多进程的优点

在单任务环境下,任务1等待远程服务器返回数据,以便进行后期的处理,这是CPU一直处于等待状态,如果任务2是10s之后被运行,虽然任务2用的时间仅有1秒,但也必须在任务1结束后才能运行任务2。单任务的特点就是排队执行,也就是同步,就像在cmd中输入一条命令后,必须等待这条命令执行完才可以执行下一条命令一样。这就是单任务环境的缺点,即CPU利用率非常低。

在多任务环境下,CPU完全可以在任务1和任务2之间来回切换,使任务2不必等到10s再运行。CPU的利用率大大提高。这就是要使用多线程技术、要学习多线程的原因。这是多线程的优点,使用多线程也就是在使用异步。

多进程的缺点

多线程相对多进程的确大大提升了CPU的利用率,但是多线程也有一些弊病,比如线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题,这个问题也是最常见的,解决这个问题先了解一下线程安全和可重入的概念。

可重入
可重入函数也可以这样理解,重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有多个该函数的副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。如果确实需要访问全局变量(包括static),一定要注意实施互斥手段。可重入函数在并行运行环境中非常重要,但是一般要为访问全局变量付出一些性能代价。

线程安全的条件:概念比较直观。一般说来,一个函数被称为线程安全的,当且仅当被多个并发线程反复调用时,它会一直产生正确的结果。

要确保函数线程安全,主要需要考虑的是线程之间的共享变量。属于同一进程的不同线程会共享进程内存空间中的全局区和堆,而私有的线程空间则主要包括栈和寄存器。因此,对于同一进程的不同线程来说,每个线程的局部变量都是私有的,而全局变量、局部静态变量、分配于堆的变量都是共享的。在对这些共享变量进行访问时,如果要保证线程安全,则必须通过加锁的方式。

所以我们知道线程只有一些东西是私有的,他们就像是工厂的工人,工厂里的东西是公共的,工人自己的物品是私有的。
线程私有
ID,每个线程都有自己的ID作为进程中唯一的表述。

  • 一组寄存器值,用来保存状态
  • 各自的堆栈
  • 错误返回码,防止错误还未被处理就被其他线程修改。
  • 信号屏蔽码,每个线程感兴趣的信号不同。
  • 优先级
  • 共享的:进程的代码段,公有数据,进程打开的文件描述符,全局内存,- 进程的栈,堆内存等。

关于线程的堆栈

说一下线程自己的堆栈问题。

是的,生成子线程后,它会获取一部分该进程的堆栈空间,作为其名义上的独立的私有空间。(为何是名义上的呢?)由于,这些线程属于同一个进程,其他线程只要获取了你私有堆栈上某些数据的指针,其他线程便可以自由访问你的名义上的私有空间上的数据变量。(注:而多进程是不可以的,因为不同的进程,相同的虚拟地址,基本不可能映射到相同的物理地址)

多进程编程

pthread_create():

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void
*arg);
  • 第一个参数thread是一个pthread_t类型的指针,他用来返回该线程的线程ID。每个线程都能够通过pthread_self()来获取自己的线程ID(pthread_t类型)。
  • 第二个参数是线程的属性,其类型是pthread_attr_t类型,其定义如下:
  • 第三个参数start_routine是一个函数指针,它指向的函数原型是 void *func(void *),这是所创建的子线程要执行的任务
    (函数);
    -第四个参数arg就是传给了所调用的函数的参数,如果有多个参数需要传递给子线程则需要封装到一个结构体里传进去;

每个进程创建的时候都会创建一个缺省的线程,我们称为主线程,调用pthread_create(),创建一个子线程,子线程具有自己的线程ID,具有自己的调用栈,自己的寄存器。调用pthread_self()可以获取ID。
开辟了很多线程后,一旦主线程退出,所有子进程无论退出与否都终止执行,就和僵尸进程一样进入僵尸状态,线程所占用的空间得不到释放。所以线程之间有两种状态结合(joinable)和分离(detached)
结合
线程之间是关联的,主线程不能提前退出,必须阻塞这所有的子线程终止为止,然后一个个回收他们的资源。
分离
线程之间是不关联的,主线程退出释放自己的资源,子线程无需与主线程回合,退出时自动释放自己的资源。

线程属性结构

typedef struct
{
	int detachstate; 		//线程的分离状态
 	int schedpolicy; 		//线程调度策略
 	struct sched_param schedparam; //线程的调度参数
 	int inheritsched; 		//线程的继承性
 	int scope; 	//线程的作用域
 	size_t guardsize; 	//线程栈末尾的警戒缓冲区大小
 	int stackaddr_set;
 	void * stackaddr; 	//线程栈的位置
 	size_t stacksize; 	//线程栈的大小
}pthread_attr_t;
pthread_t tid; //线程ID
pthread_attr_t pthread; //创建一个实例属性

pthread_addr_init(&pthread); //属性初始化
pthread_attr_setstacksize(&pthread, 120*1024); //私有栈大小设置
pthread_attr_setdetachstate(&pthread, PTHREAD_CREATE_DETACHED   or   PTHREAD_CREATE_JOINABLE); //状态设置为分离或者结合

pthread_create(&tid , &pthread, funtion(), &command); //创建线程去执行function()

pthread_attr_destroy(&pthread); //回收线程

更多属性设置:
https://blog.csdn.net/pbymw8iwm/article/details/6721038

死锁 和 互斥锁 和 信号量(PV操作)

死锁
多线程以及多进程改善了系统资源的利用率并提高了系统 的处理能力。然而,并发执行也带来了新的问题——死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

下面我们通过一些实例来说明死锁现象。

先看生活中的一个实例,2个人一起吃饭但是只有一双筷子,2人轮流吃(同时拥有2只筷子才能吃)。某一个时候,一个拿了左筷子,一人拿了右筷子,2个人都同时占用一个资源,等待另一个资源,这个时候甲在等待乙吃完并释放它占有的筷子,同理,乙也在等待甲吃完并释放它占有的筷子,这样就陷入了一个死循环,谁也无法继续吃饭。。。
在计算机系统中也存在类似的情况。例如,某计算机系统中只有一台打印机和一台输入 设备,进程P1正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程P2 所占用,而P2在未释放打印机之前,又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去,均无法继续执行,此时两个进程陷入死锁状态。

死锁产生的必要条件
产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。
互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有

互斥量(Mutex)

互斥量表现互斥现象的数据结构,也被当作二元信号灯。一个互斥基本上是一个多任务敏感的二元信号,它能用作同步多任务的行为,它常用作保护从中断来的临界段代码并且在共享同步使用的资源。

在这里插入图片描述
Mutex本质上说就是一把锁,提供对资源的独占访问,所以Mutex主要的作用是用于互斥。Mutex对象的值,只有0和1两个值。这两个值也分别代表了Mutex的两种状态。值为0, 表示锁定状态,当前对象被锁定,用户进程/线程如果试图Lock临界资源,则进入排队等待;值为1,表示空闲状态,当前对象为空闲,用户进程/线程可以Lock临界资源,之后Mutex值减1变为0。

Mutex可以被抽象为四个操作:

  • 创建 Create

  • 加锁 Lock

  • 解锁 Unlock

  • 销毁 Destroy

Mutex被创建时可以有初始值,表示Mutex被创建后,是锁定状态还是空闲状态。在同一个线程中,为了防止死锁,系统不允许连续两次对Mutex加锁(系统一般会在第二次调用立刻返回)。也就是说,加锁和解锁这两个对应的操作,需要在同一个线程中完成。

pthread_mutex_init();//初始化
pthread_mutex_lock();//加锁
pthread_mutex_unlock();//解锁
pthread_mutex_destroy();//销毁

信号量
信号量的本质是一种数据操作锁、用来负责数据操作过程中的互斥、同步等功能。
信号量用来管理临界资源的。它本身只是一种外部资源的标识、不具有数据交换功能,而是通过控制其他的通信资源实现进程间通信。
可以这样理解,信号量就相当于是一个计数器。当有进程对它所管理的资源进行请求时,进程先要读取信号量的值,大于0,资源可以请求,等于0,资源不可以用,这时进程会进入睡眠状态直至资源可用。当一个进程不再使用资源时,信号量+1(对应的操作称为V操作),反之当有进程使用资源时,信号量-1(对应的操作为P操作)。对信号量的值操作均为原子操作。

创建/获取一个信号量集合

int semget(key_t key,int nsems,int semflg);

返回值:成功返回信号量集合的semid。失败返回-1。
key:可以用key_t ftok(const char* pathname,int proj_id)获取。
nsems:这个参数表示你要创建的信号量集合中的信号量的个数。信号量只能以集合的形式创建。
semflg:同时使用IPC_CREAT和IPC_EXCL则会创建一个新的信号量集合。若已经存在的话则返回-1。单独使用IPC_CREAT的话会返回一个新的或者已经存在的信号量集合。

信号量结合操作

int semop(int semid,struct sembuf *sops,unsigned nsops);
int semtimedop(int semid, struct sembuf *sops, unsigned nsops, struct timespec *timeout);

返回值:成功返回0,失败返回-1。

semid:信号量结合的id。
struct sembuf *sops:
struct sembuf
{
      unsigned short sem_num;  /* semaphore number */
      short          sem_op;   /* semaphore operation */
      short          sem_flg;  /* operation flags */
};

sem_num: 为信号量是以集合的形式存在的,就相当所有信号量在一个数组里边,sem_num表示信号量在集合中的编号。
sem_op:表示该信号量的操作(P操作还是V操作)。如果其值为正数,该值会加到现有的信号内含值中。通常用于释放所控资源的使用权;如果sem_op的值为负数,而其绝对值又大于信号的现值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用权
sem_flg:信号操作标志,它的取值有两种。IPC_NOWAIT和SEM_UNDO。
IPC_NOWAIT:对信号量的操作不能满足时,semop()不会阻塞,而是立即返回,同时设定错误信息。
SEM_UNDO: 程序结束时(不管是正常还是不正常),保证信号值会被设定semop()调用之前的值。这样做的目的在于避免程序在异常的情况下结束未将锁定的资源解锁(死锁),造成资源永远锁定。
nsops:表示要操作信号量的个数。因为信号量是以集合的形式存在,所以第二个参数可以传一个数组,同时对一个集合中的多个信号量进行操作。

int semctl(int semid,int semnum,int cmd,…);
semctl()在semid标识的信号量集合上,或者该信号量集合上第semnum个信号量上执行cmd指定的控制命令。根据cmd不同,这个函数有三个或四个参数,当有第四个参数时,第四个参数的类型是union。

union semun{
int val;   //使用的值
struct semid_ds *buf;  //IPC_STAT、IPC_SET使用缓存区
unsigned short *array; //GETALL、SETALL使用的缓存区
struct seminfo *__buf; //IPC_INFO(linux特有)使用缓存区
};
返回值:失败返回-1。成功返回0。
semid:信号量集合的编号。
semnum:信号量在集合中的标号。

命令:ipcs -s //查看创建的信号量集合的个数
ipcrm -s semid //删除一个信号量集合

互斥量和信号量的区别

  1. 互斥量用于线程的互斥,信号量用于线程的同步。

这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。

互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源

  1. 互斥量值只能为0/1,信号量值可以为非负整数。

也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。

  1. 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值