1. 前言
如果你不了解线程的基本概念,请你先
移步上一篇文章: 线程基本概念
本章重点:
本篇文章着重讲解线程互斥以及线程
同步的相关概念,以及如何实现它们.
周边概念包括临界资源,原子性,互斥量
等也会在本文当中提及
2. 多线程互斥相关背景概念
在学习互斥前,需要先补充一些相关概念:
临界资源:
多线程执行流共享的资源就叫做临界资源临界区:
每个线程内部,访问有临界资源的代码,就叫做临界区互斥:
任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用原子性:
不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
。大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互,多个线程并发的操作共享变量,会带来一些问题
比如说我们最常见的高铁售票系统,可以把买票操作分为三步: 第一步: 判断现在还有无车票.第二步: 乘客付款后,钱包金额减少. 第三步: 乘客获得一张车票,高铁的总票数减一.多个执行流执行这三步时可能会出现问题,如下图:
可以写一段代码来验证上面的情况:
// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void \*route(void \*arg)
{
char \*id = (char\*)arg;
while ( 1 ) {
if ( ticket > 0 ) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
} else {
break;
}
}
}
int main( void )
{
pthread_t t1, t2, t3, t4;
pthread\_create(&t1, NULL, route, "thread 1");
pthread\_create(&t2, NULL, route, "thread 2");
pthread\_create(&t3, NULL, route, "thread 3");
pthread\_create(&t4, NULL, route, "thread 4");
//等待线程结束
pthread\_join(t1, NULL);
pthread\_join(t2, NULL);
pthread\_join(t3, NULL);
pthread\_join(t4, NULL);
}
发现多次执行这段代码得到的结果可能不同
为什么会出现不同的结果?
3. 多线程互斥详解
为啥上面可能会出现多种结果?
是有多种原因在里面的:
- if 语句判断条件为真以后,代码可以并发的切换到其他线程
- usleep这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
- - -ticket操作本身就不是一个原子操作
这里有一个问题,为什么- -ticket操作不是原子的?其实我们鉴定一个操作是不是原子性的可以查看这个操作的汇编代码,若汇编代码只有一条,则我们认为这个操作是原子的,反之则这个操作不是原子性的,可以来看看减减的汇编代码是有三条:
要解决上面的问题,需要满足以下条件:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
本质上就是需要一把锁, 互斥锁
任何一个时间,只允许一个线程获得这把锁并且继续向后执行,没拿到锁的线程默认只能在加锁处阻塞等待其他线程释放掉锁才能继续往后走,多个线程来竞争一把锁,它们的关系就是互斥
4. 互斥锁的接口使用
互斥锁的使用一般分为四个步骤:
- 初始化互斥锁
- 在到达临界区前加锁
- 在跑完临界区代码后解锁
- 用完互斥锁后进行销毁
第一步: 初始化互斥锁
方法一, 静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法二, 动态分配
第二步和第三步: 加解锁
调用pthread_ lock 时,可能会遇到以下情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,
那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁
第四步: 销毁互斥锁
所以现在可以更改一下售票系统:
int ticket = 100;
pthread\_mutex\_t mutex;//全局
void \*route(void \*arg)
{
char \*id = (char\*)arg;
while ( 1 ) {
pthread\_mutex\_lock(&mutex);
if ( ticket > 0 ) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread\_mutex\_unlock(&mutex);
// sched\_yield(); 放弃CPU
} else {
pthread\_mutex\_unlock(&mutex);
break;
}
}
}
int main( void )
{
pthread\_t t1, t2, t3, t4;
pthread\_mutex\_init(&mutex, NULL);
pthread\_create(&t1, NULL, route, "thread 1");
pthread\_create(&t2, NULL, route, "thread 2");
pthread\_create(&t3, NULL, route, "thread 3");
pthread\_create(&t4, NULL, route, "thread 4");
pthread\_join(t1, NULL);
pthread\_join(t2, NULL);
pthread\_join(t3, NULL);
pthread\_join(t4, NULL);
pthread\_mutex\_destroy(&mutex);
}
5. 死锁相关概念
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。这样干说可能有点抽象,举个例子:
形成死锁的必要条件:
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
所以大家在写代码时,要避免写出死锁
6. 线程安全和可重入的关系
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
所以说,重入实际上比线程安全更加严格
下面是常见的不可重入的情况:
最全的Linux教程,Linux从入门到精通
======================
-
linux从入门到精通(第2版)
-
Linux系统移植
-
Linux驱动开发入门与实战
-
LINUX 系统移植 第2版
-
Linux开源网络全栈详解 从DPDK到OpenFlow
第一份《Linux从入门到精通》466页
====================
内容简介
====
本书是获得了很多读者好评的Linux经典畅销书**《Linux从入门到精通》的第2版**。本书第1版出版后曾经多次印刷,并被51CTO读书频道评为“最受读者喜爱的原创IT技术图书奖”。本书第﹖版以最新的Ubuntu 12.04为版本,循序渐进地向读者介绍了Linux 的基础应用、系统管理、网络应用、娱乐和办公、程序开发、服务器配置、系统安全等。本书附带1张光盘,内容为本书配套多媒体教学视频。另外,本书还为读者提供了大量的Linux学习资料和Ubuntu安装镜像文件,供读者免费下载。
本书适合广大Linux初中级用户、开源软件爱好者和大专院校的学生阅读,同时也非常适合准备从事Linux平台开发的各类人员。
需要《Linux入门到精通》、《linux系统移植》、《Linux驱动开发入门实战》、《Linux开源网络全栈》电子书籍及教程的工程师朋友们劳烦您转发+评论
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!