怎么样制作一个公司网站,上市公司网站建设报价,钓鱼网站是什么技术的人做的出来,公司网站 源码目录
线程的概念
进程与线程的关系
线程创建
线程终止
线程等待
线程分离
原生线程库
线程局部存储
自己实现线程封装
线程的优缺点
多线程共享与独占资源
线程互斥
互斥锁
自己实现锁的封装
加锁实现互斥的原理
死锁
线程同步 线程的概念
回顾进程相关概念 …目录
线程的概念
进程与线程的关系
线程创建
线程终止
线程等待
线程分离
原生线程库
线程局部存储
自己实现线程封装
线程的优缺点
多线程共享与独占资源
线程互斥
互斥锁
自己实现锁的封装
加锁实现互斥的原理
死锁
线程同步 线程的概念
回顾进程相关概念 ● 进程 内核数据结构(pcb等) 内存中的代码/数据
● 进程创建成本较高需要创建pcb, 进程地址空间页表构建页表映射关系将磁盘的代码和数据加载到内存中等一系列工作
● 一个进程访问的大部分资源都在物理内存中需要通过进程地址空间页表获取到因此可以认为进程地址空间是进程的资源窗口因此进程是承担系统资源分配的基本实体
● 创建进程目的是为了让cpu去调度执行进程中的代码访问相应的数据完成任务因此之前的认知是: 一个进程本质就是一个执行流
线程的概念 ● 在一个进程内只创建若干pcb这些pcb指向同一个进程地址空间通过同一个页表映射到同一个内存看到的是同一份资源
● 目前一个进程内有多个pcb了本质就是有多个执行流了多个执行流的地位是对等的cpu调度时选择任意一个执行流调度即可每个执行流本质就是一个线程所以cpu调度的基本单位是线程
● 每个线程都有自己的pcb, 不同的pcb中保存同一个虚拟地址空间的不同起始地址进而通过页表映射到不同的物理内存区域相当于多线程瓜分了进程地址空间从而并发执行同一个进程内的不同代码共同完成一项任务
● 多个线程由于共用同一个进程地址空间通过同一个页表映射看到的是同一份资源所以资源共享在线程之间显得非常容易比如全局变量、环境变量、命令行参数等
● 线程是在进程内部执行的一种执行流
● 线程是更加轻量级的进程/线程是比进程更加轻量化的执行流
a.创建线程更加简单了因为创建进程时该进程用到的资源都申请好了一系列工作都已经做好了创建线程只是在分配资源!!!
b.创建线程更加简单意味着释放线程也更加容易了!
c.线程调度也更加简单
c.1 因为不同的线程看到的是同一个地址空间访问的是同一个资源因此线程间切换时只需要把一个pcb切换成另一个pcb, 把保存线程临时数据的少量寄存器切换而页表和地址空间不用切换!
c.2 cpu内部集成了高速缓存cache线程间切换不需要切换cache, 因为cache中保存的是整个进程中高频访问的数据但是进程间切换需要切换cache, 因为cache中的大部分数据都失效了!!! 这是线程创建更加简单的最主要的原因
● 创建线程时线程会瓜分进程总体的时间片因为时间片也是资源
● 站在cpu角度cpu不需要区分调度的是线程还是进程只需要找到pcb找到进程地址空间通过页表映射执行代码即可
● Linux中并不存在真正的线程只有轻量级进程的概念
一个进程内可能存在多个线程要不要把所有的线程管理起来呢?? 要管理! 如何管理? 先描述再组织! --- 描述结构体叫做 tcb, 而线程也要有自己的各种队列线程id, 状态调度算法等这都是 tcb中的属性字段最后把所有tcb用链表链接起来!!!
事实上windows就是这样实现的而Linux系统中并没有单独为线程设计tcb因为线程的大部分属性特征进程也是有的线程和进程都是执行流, 不必为线程单独设计反倒会增加程序员的负担因此Linux中用pcb可以充当tcb, 所有代码在线程级别上复用即可 一整套调度算法也可以直接复用!
进程与线程的关系 线程创建
● pthread_create 接口
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
功能: 创建一个新线程
参数:
thread: 输出型参数获取新线程id
attr: 创建线程时设置的线程属性直接设为nullptr即可
start_routine新线程执行的函数
arg: 新线程执行函数的参数
返回值: 创建成功返回0创建失败返回错误码
● 创建线程代码示例
#include iostream
#include pthread.h
#include unistd.h
using namespace std;//新线程
void* ThreadRoutine(void* arg)
{const char* threadName (const char*)arg;while(true){cout I am a new thread threadName endl;sleep(1);}
}int main()
{pthread_t tid;//主线程pthread_create(tid, nullptr, ThreadRoutine, (void*)thread 1);while(true){cout I am main thread endl;sleep(1);}return 0;
} ● 尽管有主线程和新线程两个线程但始终只有1个进程因此打印出的进程pid是一样的
#include iostream
#include pthread.h
#include sys/types.h
#include unistd.h
using namespace std;void* ThreadRoutine(void* arg)
{const char* threadName (const char*)arg;while(true){cout I am a new thread , pid: getpid() , threadName endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(tid, nullptr, ThreadRoutine, (void*)thread 1);while(true){cout I am main thread , pid: getpid() endl;sleep(1);}return 0;
}● ps -aL 指令查看系统内的线程cpu调度线程依据的是LWP(light weight process), PID和LWP一样就是主线程否则是新线程 ● 线程之间看到同一份资源是非常容易的比如定义一个全局变量线程就都能看到了!
#include iostream
#include pthread.h
#include sys/types.h
#include unistd.h
using namespace std;int gcnt 100;void *ThreadRoutine(void *arg)
{while (true){cout I am a new thread, gcnt: gcnt , gcnt : gcnt endl;gcnt--;sleep(1);}
}int main()
{pthread_t tid;pthread_create(tid, nullptr, ThreadRoutine, nullptr);while (true){cout I am main thread, gcnt: gcnt , gcnt : gcnt endl;sleep(1);}return 0;
}● 创建多线程
#include iostream
#include cstring
#include pthread.h
#include unistd.h
#include time.h
#include functional
#include vector
using namespace std;using func_t functionvoid();class ThreadData
{
public:ThreadData(const string name, const uint64_t ctime, func_t f): threadname(name), createtime(ctime), func(f){}public:string threadname;uint64_t createtime;func_t func;
};void Print()
{cout 我是线程执行的大任务的一部分 endl;
}// 新线程
void *ThreadRoutine(void *args)
{ThreadData *td static_castThreadData *(args); // 安全强转while (true){cout new thread , thread name : td-threadname , create time : td-createtime endl;td-func();sleep(1);}
}int main()
{for (int i 0; i 3; i) {char threadname[64]; snprintf(threadname, sizeof(threadname), %s-%d, thread, i);ThreadData *td new ThreadData(threadname, (uint64_t)time(nullptr), Print);pthread_t tid;pthread_create(tid, nullptr, ThreadRoutine, td);sleep(1);}while (true){cout main thread endl;sleep(1);}return 0;
}
● pthread_create 接口的最后一个参数类型是void*可以接收任意数据类型的地址因此除了给线程执行方法传递常规的数据类型还可以传递我们自己封装的类对象
● 类对象中可以封装自定义函数传递给线程执行方法在线程内部进行回调
线程终止
● pthread_self()接口可以获取调用该接口的线程id本质是一个地址
#include iostream
#include cstring
#include unistd.h
#include pthread.h
using namespace std;//十进制数转十六进制数
string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), 0x%x, tid);return id;
}void* threadRoutine(void* args)
{string name static_castconst char*(args);usleep(1000);while(true){cout new thread is running, thread name: name , my thread id: ToHex(pthread_self()) endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(tid, nullptr, threadRoutine, (void*)thread-1);while(true){cout I am main thread, my thread id : ToHex(pthread_self()) endl;sleep(1);}return 0;
} ● 线程终止有很多方法比如在线程中直接return(终止线程)/exit(本质是终止整个进程)也可以调用pthread_exit()接口终止线程
#include iostream
#include cstring
#include unistd.h
#include pthread.h
#include stdlib.h
using namespace std;string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), 0x%x, tid);return id;
}void* threadRoutine(void* args)
{string name static_castconst char*(args);usleep(1000);int cnt 5;while(cnt--){cout new thread is running, thread name: name , my thread id: ToHex(pthread_self()) endl;sleep(1);}pthread_exit(nullptr); //终止调用该接口的线程
}int main()
{pthread_t tid;pthread_create(tid, nullptr, threadRoutine, (void*)thread-1);while(true){cout I am main thread, my thread id : ToHex(pthread_self()) endl;sleep(1);}return 0;
} ● 主线程中调用pthread_cancle() 可以取消指定的线程
int pthread_cancel(pthread_t thread);
参数: 要取消的线程id
返回值: 成功返回0; 失败返回错误码
#include iostream
#include pthread.h
#include unistd.h
#include functional
using namespace std;void* threadRoutine(void* args)
{int cnt 10;while(cnt--){cout thread is running... endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(tid, nullptr, threadRoutine, nullptr);sleep(3);pthread_cancel(tid); //取消tid线程cout 我是主线程取消了新线程 endl;return 0;
} ● 一个线程异常整个进程都会终止
#include iostream
#include cstring
#include pthread.h
#include unistd.h
#include time.h
#include functional
#include vector
using namespace std;using func_t functionvoid();class ThreadData
{
public:ThreadData(const string name, const uint64_t ctime, func_t f): threadname(name), createtime(ctime), func(f){}public:string threadname;uint64_t createtime;func_t func;
};void Print()
{cout 我是线程执行的大任务的一部分 endl;
}// 新线程
void *ThreadRoutine(void *args)
{int a 10;ThreadData *td static_castThreadData *(args); // 安全强转while (true){cout new thread , thread name : td-threadname , create time : td-createtime endl;td-func();//异常终止if(td-threadname thread-2){cout td-threadname 触发了异常 endl;a / 0;}sleep(1);}
}int main()
{for (int i 0; i 3; i){char threadname[64];snprintf(threadname, sizeof(threadname), %s-%d, thread, i);ThreadData *td new ThreadData(threadname, (uint64_t)time(nullptr), Print);pthread_t tid;pthread_create(tid, nullptr, ThreadRoutine, td);sleep(1);}while (true){cout main thread endl;sleep(1);}return 0;
}线程等待
● 线程退出但没有被等待也会出现和进程类似的僵尸问题
● 新线程退出时需要让主线程等待从而获取新线程的退出信息
● 当一个新线程出异常了其他线程也会受到影响整个进程都终止了主线程再等待新线程也就没有了意义
● pthread_join 线程等待代码演示
int pthread_join(pthread_t thread, void **retval);
参数:
thread: 被等待的线程id
retval: 输出型参数根据threadRoutine的返回值可以获取子进程的退出信息如果不关心新线程的退出信息该参数直接设置为nullptr即可
返回值: 成功返回0失败返回错误码
#include iostream
#include pthread.h
#include unistd.h
#include functional
using namespace std;void* threadRoutine(void* args)
{int cnt 5;while(cnt--){cout thread is running... endl;sleep(1);}char* ret 新线程正常退出啦!!!;return ret;
}int main()
{pthread_t tid;pthread_create(tid, nullptr, threadRoutine, nullptr);sleep(3);void* ret nullptr;pthread_join(tid, ret); cout main thread join done, thread return: (char*)ret endl;return 0;
} ● 线程执行方法的返回值是void*可以返回任意类型的数据自定义类对象也是可以的
#include iostream
#include cstring
#include unistd.h
#include pthread.h
#include stdlib.h
using namespace std;string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), 0x%x, tid);return id;
}class ThreadReturn
{
public:ThreadReturn(pthread_t id, const string info, int code):id_(id),info_(info),code_(code){}public:pthread_t id_;string info_;int code_;
};void *threadRoutine(void *args)
{string name static_castconst char *(args);usleep(1000);int cnt 5;while (cnt--){cout 我是新线程, 正在运行噢, 我的线程id是: ToHex(pthread_self()) endl;sleep(1);}ThreadReturn* ret new ThreadReturn(pthread_self(),thread quit normal, 10);return ret;
}int main()
{pthread_t tid;pthread_create(tid, nullptr, threadRoutine, (void *)thread-1);cout I am main thread, my thread id : ToHex(pthread_self()) endl;void *ret nullptr;pthread_join(tid, ret);ThreadReturn* r static_castThreadReturn*(ret); cout main thread get new thread info : r-code_ , ToHex(r-id_) , r-info_ endl;return 0;
} 线程分离
● 大部分软件跑起来之后都是死循环比如用户打开qq, 打开网易云音乐等等打开后不会自动退出的直到用户手动关掉。也就是说新线程大多数是不需要被等待的主线程创建出新线程之后就让新线程去跑了主线程就不管了
● 线程默认是joinable状态的但如果主线程就是不想等待新线程不关心新线程的退出状态, 主线程自己直接做其他事情那么就可以将新线程设置为分离状态
● 可以在主线程中将新线程设置为分离状态新线程也可以让自己设置成分离状态
● 线程分离代码演示
int pthread_detach(pthread_t thread);
参数分离的线程id
返回值: 成功返回0失败返回错误码
#include iostream
#include pthread.h
#include cstring
#include unistd.h
using namespace std;void* threadRoutine(void* args)
{pthread_detach(pthread_self()); //新线程中将自己分离int cnt 5;while(cnt--){cout thread is running... endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(tid, nullptr, threadRoutine, nullptr);//pthread_detach(tid); //主线程中将tid线程分离 int n pthread_join(tid, nullptr);cout n endl;return 0;
} ● 线程被分离后可以被取消但不能被join取消线程后线程返回值是PTHREAD_CANCELED
#define PTHREAD_CANCELED ((void *) -1)
#include iostream
#include pthread.h
#include unistd.h
#include functional
using namespace std;void* threadRoutine(void* args)
{int cnt 5;while(cnt--){cout thread is running... endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(tid, nullptr, threadRoutine, nullptr);sleep(1);pthread_detach(tid); int n pthread_cancel(tid); //取消tid线程cout main thread cancel done, n: n endl;void* ret nullptr;n pthread_join(tid, ret); cout main thread join done, n: n , thread return: (int64_t)ret endl;return 0;
} 原生线程库 ● Linux下没有真线程只有轻量级进程所以OS只会提供轻量级进程创建的系统调用不会提供线程创建的系统调用
● 但用户只认线程而且windows下是有真线程的因此Linux在内核和用户层之间加了一层软件层也就是pthread原生线程库对内核的轻量级进程(LWP)接口进行封装向上提供线程的一系列接口同时管理多个线程先描述再组织因此pthread库中是包含了一堆描述线程属性的结构体
● 原生线程库意思是任何一款操作系统都要默认有的不属于C/C语言本身, 因此编译时要带-l
● 作为用户如果想知道一共创建了几个线程每个线程的状态当前有几个线程一个线程退出了退出结果是多少等信息就直接去pthread库中获取即可
● 线程要有自己的一些独立属性:
1.上下文数据(被OS以轻量级进程形式维护在tcb中)
2.栈结构(栈大小栈在哪里等信息都必须在线程库中维护)
但是线程有多个而地址空间中栈只有1个如何分配?? clone接口 --- 创建轻量级进程 pthread_create的底层和fork的底层都是clone 第一个参数是线程执行的函数 第二个参数是线程库在堆区new的一段空间的起始地址作为栈起始地址 第三个参数flags表示是创建轻量级进程还是创建一个真正的子进程 ● 进程地址空间中的栈默认由主线程使用
● 线程库是共享的, 所以线程内部要管理整个系统中, 多个用户启动的多个线程!
● 而库要管理线程就要在库中存在管理线程的结构体 --- struct pthread
● 线性局部存储是存放一些只能被线程自己看见的数据
● 线程栈就是保存了堆区new出来的一块空间的起始地址
● 每个线程在库中都是这三部分可以把多个这部分看成一个数组因此对线程的管理就转化成了对数组的增删查改
● 当线程退出时退出结果会保存到库中的struct pthread中因此主线程只需要去库中的struct pthread拷贝数据拿到结果即可!
● 结论: pthread_t tid 表示的是线程属性集合在库中的地址!!! LWP是内核的概念!
● C的线程库本质是对pthread的封装, 因为去掉-lpthread选项之后报链接错误
线程局部存储
● 全局变量本身就是被线程共享的而如果定义全局变量时带上__thread会发现全局变量不是只有1份了而是每个线程都有一份!
● __thread修饰全局变量会把全局变量拷贝到每个线程内的线程局部存储空间中!
#include iostream
#include pthread.h
#include unistd.h
using namespace std;//__thread是一个编译选项, 编译的时候就会把线程控制块中的空间开辟出来 --- 拷贝到线程局部存储空间中!
__thread int g_val 100; void* threadRoutine(void* args)
{string name static_castconst char* (args);while(true){cout I am new thread , thread name: name , g_val: g_val , g_val: g_val endl endl;g_val;sleep(1);}
}int main()
{pthread_t tid;pthread_create(tid, nullptr, threadRoutine, (void*)thread-1);while(true){cout I am main thread , g_val: g_val , g_val: g_val endl endl;sleep(1);}pthread_join(tid, nullptr); return 0;
} ● 线程局部存储的用途: 定义一个全局变量用__thread修饰这样就可以在每个线程内部获取到线程的lwp
#include iostream
#include pthread.h
#include sys/types.h
#include sys/syscall.h
#include unistd.h
using namespace std;__thread pid_t lwp; void* threadRoutine(void* args)
{string name static_castconst char* (args);lwp syscall(SYS_gettid); //系统调用获取当前线程的lwpwhile(true){cout I am new thread , thread name: name new thread lwp: lwp endl; sleep(1);}
}int main()
{pthread_t tid;pthread_create(tid, nullptr, threadRoutine, (void*)thread-1);lwp syscall(SYS_gettid); //系统调用获取当前线程的lwpwhile(true){cout I am new thread new thread lwp: lwp endl; sleep(1);}pthread_join(tid, nullptr); return 0;
} 注意:
●__thread string threadname; //err, __thread只能存储内置类型不能存储一些容器
● 线程中可以fork, 本质是在创建子进程, 也可以调用execl, 不过替换的是整个进程会影响其他所有线程因此不建议在线程中excel如果要execl建议先fork, 再execl
自己实现线程封装 Thread.hpp #pragma once #include iostream
#include string
#include pthread.h
#include functional
using namespace std;//设计方的视角templateclass T
using func_t functionvoid(T); //返回值为void, 参数为T的类型templateclass T
class Thread
{
public:Thread(const string threadname, func_tT func, T data):_tid(0),_threadname(threadname),_isrunning(false),_func(func),_data(data){}//改为static, 参数就没有this指针了!static void* ThreadRoutine(void* args) //不加static, 类内方法, 默认携带this指针{Thread* ts static_castThread *(args); ts-_func(ts-_data);return nullptr; }//启动线程(内部调用线程创建)bool start(){int n pthread_create(_tid, nullptr, ThreadRoutine, this);if(n 0) {_isrunning true;return true;}else {return false;}}//线程等待bool join(){if(!_isrunning) return true;int n pthread_join(_tid, nullptr);if(n 0){_isrunning false;return true;}else{return false;}}string ThreadName(){return _threadname;}bool IsRunning(){return _isrunning;}~Thread(){}private:pthread_t _tid; //线程idstring _threadname; //线程名bool _isrunning; //线程是否在运行func_tT _func; //线程执行方法T _data;
}; main.cc #include iostream
#include unistd.h
#include vector
#include thread.hpp//应用方的视角
string GetThreadName()
{static int number 1;char name[64];snprintf(name, sizeof(name), Thread-%d, number);return name;
}void Print(int num)
{while(num){cout hello world: num-- endl;sleep(1);}
}int main()
{Threadint t(GetThreadName(), Print, 10);t.start();t.join();return 0;
} 线程的优缺点
优点:
● 创建一个新线程的代价要比创建一个新进程小得多
● 与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多
● 线程占用的资源要比进程少很多
● 能充分利用多处理器的可并行数量
● 在等待慢速I/O操作结束的同时程序可执行其他的计算任务
●计算密集型应用为了能在多处理器系统上运行, 将计算分解到多个线程中实现
● I/O密集型应用为了提高性能将I/O操作重叠。线程可以同时等待不同的I/O操作。
缺点:
● 缺乏访问控制
进程是访问控制的基本粒度 由于大部分资源都是共享的在一个线程中调用某些OS函数会对整个进程造成影响而同步和互斥就是在解决这个问题
● 健壮性/鲁棒性降低
多线程中一个线程崩溃整个进程都崩溃而多进程程序一个进程崩溃不影响其他进程因为进程具有独立性
● 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
● 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多那么可能会有较大的性能损失这里的性能损失指的是增加了额外的同步和调度开销而可用的资源不变。
多线程共享与独占资源
多线程之间共享的资源
1. 进程代码段
2. 进程的公有数据(利用这些共享的数据线程很容易的实现相互之间的通讯)
3. 进程打开的文件描述符
4. 信号的处理器
5. 进程的当前目录
6. 进程用户ID与进程组ID。
多线程之间独立的资源
1.线程ID
2.寄存器组的值
3.线程的堆栈
4.错误返回码
5.线程的信号屏蔽码
6.线程的优先级
线程互斥
下面是一段模拟多线程抢票的代码
#include iostream
#include unistd.h
#include vector
#include Thread.hpp//构造线程名称
string GetThreadName()
{static int number 1;char name[64];snprintf(name, sizeof(name), Thread-%d, number);return name;
}//抢票逻辑
int ticket 10000; // 全局的共享资源
void GetTicket(string name)
{while (true){if (ticket 0){// 充当抢票花费的时间usleep(1000);printf(%s get a ticket : %d\n, name.c_str(), ticket);ticket--; }else{break;}// 实际情况, 还有后续的动作}
}int main()
{string name1 GetThreadName();Threadstring t1(name1, GetTicket, name1);sleep(2);string name2 GetThreadName();Threadstring t2(name2, GetTicket, name2);sleep(2);string name3 GetThreadName();Threadstring t3(name3, GetTicket, name3);sleep(2);t1.start();sleep(2);t2.start();sleep(2);t3.start();sleep(2);t1.join();t2.join();t3.join();return 0;
} 运行代码发现最后出现了票数出现了负数但是我们在if语句中判断票数0了呀为啥还会出现票数为负数呢???
显然票数是公共资源可以被多个执行流同时访问而多个执行流同时访问公共资源显然出现了问题因此我们需要把公共资源保护起来使得任何一个时刻只允许一个线程正在访问公共资源此时公共资源就叫做临界资源! 而我们的代码中只有一部分代码会去访问临界资源的进程中访问临界资源的代码叫做临界区
任何时刻只允许一个执行流进入临界区使得多个执行流只能串行访问临界资源叫做互斥!
/--本质是三条汇编语句, 每一条汇编语句都是原子性的, 而执行每一条汇编语句都有可能被中断, 三条汇编语句过程是 先把内存中的a拷贝到cpup寄存器中然后在寄存器中对a, 最后将寄存器的a拷贝会回内存空间中! 多线程同时访问公共资源有什么问题呢??? 举个例子
比如有A线程和B线程, 公共资源是int a 10, 两个线程都要进行a操作, 目前的情况是A线程把汇编的第二步执行完毕寄存器中a为11, 然后被切换走了于是A线程的上下文数据中就保存了a为11, 此时线程B被cpu调度一直将内存空间中的a到了100此时被切走了线程A被调度接着执行第三条汇编语句将自己的上下文数据a11恢复到寄存器中然后将寄存器内容写回内存于是内存空间中的a改为了11就出现了数据不一致的问题!!
而我们今天的抢票代码最后票出现了负数原因是:
当票数减为1时多个线程进行了if条件判断都是成立的语句进入到了if循环内部此时某个线程被调度将内存中的tickets--到了0, 此时其他线程都执行过了if判断, 再对票数--, 就会将内存中的票数--到负数!
互斥锁
● 互斥锁的功能就是用来实现互斥使得临界资源只能同时被一个执行流访问
● 尽量要给少的代码块加锁 (因为加锁之后同一时间内只允许一个线程访问临界区资源如果给大量代码加锁多线程就没有意义了效率可能大大降低)
● 一般都是给临界区加锁
● 使用锁的相关接口
定义全局互斥锁并初始化
pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;
加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);使用全局锁实现互斥访问临界资源
//定义全局锁
pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;//抢票逻辑
int ticket 10000; // 全局的共享资源
void GetTicket(string name)
{while (true){pthread_mutex_lock(mutex); if (ticket 0){// 充当抢票花费的时间usleep(1000);printf(%s get a ticket : %d\n, name.c_str(), ticket);ticket--; pthread_mutex_unlock(mutex); }else{pthread_mutex_unlock(mutex); break;}// 实际情况, 还有后续的动作}
}注意
● 为了实现互斥访问临界资源我们定义了一把全局锁而全局锁也是全局变量也是公共资源的也得保证申请锁是安全的呀!!! 而申请锁本身是原子性操作是安全的!
● 加锁是由程序员自己保证的是一种规则不遵守就是自己写的bug
● 根据互斥的定义, 任何时刻只允许一个线程申请锁成功! 就注定了会有多个线程申请锁失败失败的线程默认会在mutex锁上阻塞阻塞的本质就是等待
● 一个线程在临界区访问临界资源的时候, 是完全有可能发生线程切换的但是切换走的线程依旧没有释放锁可以理解成把锁带走了其他线程依旧访问不了临界资源
● 加锁的情况下if里面的代码块也表现出原子性, 因为这段代码要么不执行要么执行完别的线程才能访问
定义局部互斥锁
初始化局部锁(第二个参数可以设置锁属性传nullptr即可)
int pthread_mutex_init(pthread_mutex_t *restrict mp,const pthread_mutexattr_t *restrict mattr);
释放局部锁
int pthread_mutex_destroy(pthread_mutex_t *mutex)
加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);使用局部锁实现互斥访问临界资源
#include iostream
#include unistd.h
#include vector
#include Thread.hppstring GetThreadName()
{static int number 1;char name[64];snprintf(name, sizeof(name), Thread-%d, number);return name;
}int ticket 10000; // 全局的共享资源void GetTicket(pthread_mutex_t* mutex)
{while (true){ pthread_mutex_lock(mutex); if (ticket 0){// 充当抢票花费的时间usleep(1000);printf(get a ticket : %d\n, ticket);ticket--;pthread_mutex_unlock(mutex);}else{pthread_mutex_unlock(mutex);break;}// 实际情况, 还有后续的动作}
}int main()
{pthread_mutex_t mutex; //定义局部锁pthread_mutex_init(mutex, nullptr); //初始化局部锁string name1 GetThreadName();Threadpthread_mutex_t* t1(name1, GetTicket, mutex);string name2 GetThreadName();Threadpthread_mutex_t* t2(name2, GetTicket, mutex);string name3 GetThreadName();Threadpthread_mutex_t* t3(name3, GetTicket, mutex);t1.start();t2.start();t3.start();t1.join();t2.join();t3.join();pthread_mutex_destroy(mutex); //释放局部锁return 0;
}
自己实现锁的封装 LockGuard.hpp #include iostream
#include string
#include pthread.h
using namespace std;//不定义锁, 默认外部会传入锁对象
class Mutex
{
public:Mutex(pthread_mutex_t* lock):_lock(lock){}void Lock(){pthread_mutex_lock(_lock);}void Unlock(){pthread_mutex_unlock(_lock);}~Mutex(){}
private:pthread_mutex_t* _lock;
};class LockGuard
{
public:LockGuard(pthread_mutex_t* lock):_mutex(lock){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}
private:Mutex _mutex;
}; main.cc #include iostream
#include unistd.h
#include vector
#include Thread.hpp
#include LockGuard.hppstring GetThreadName()
{static int number 1;char name[64];snprintf(name, sizeof(name), Thread-%d, number);return name;
}int ticket 10000; // 全局的共享资源
pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; // 全局锁对象void GetTicket(string name)
{while (true){LockGuard lockguard(mutex);{if (ticket 0){// 充当抢票花费的时间usleep(1000);printf(get a ticket : %d\n, ticket);ticket--;}else{break;}}// 实际情况, 还有后续的动作}
}int main()
{string name1 GetThreadName();Threadstring t1(name1, GetTicket, name1);string name2 GetThreadName();Threadstring t2(name2, GetTicket, name2);string name3 GetThreadName();Threadstring t3(name3, GetTicket, name3);t1.start();t2.start();t3.start();t1.join();t2.join();t3.join();return 0;
}加锁实现互斥的原理 ● 大多数体系结构都提供了swap和exchange指令, 作用是把寄存器内容和内存单元的数据进行交换
● 而锁本质是一个结构体可以简单认为内部有一个变量 int mutex 1
● A线程加锁时先将寄存器%al内容置成0然后交换寄存器%al的内容和内存变量mutex内容于是%al的内容变为了1, mutex变为了0
此时就算线程A被切走了会把上下文数据包括%al的内容带走线程B开始调度运行 内存内容已经是0了因此尽管线程B交换寄存器%al的值和内存中的mutex的值交换完之后还是0此时进入else分支挂起等待。因此可以认为线程切换时把锁带走了!
● 所以交换本质是将一个共享的mutex资源交换到自己的上下文中属于线程自己了!
● 解锁就是直接将mutex置为1其他线程申请锁时就正常执行上述汇编语句即可!
● 加锁和解锁的一般规则: 谁加锁谁解锁
死锁
● 死锁是指在一组进程中的各个进程均占有不会释放的资源但因互相申请被其他进程所站用不会释放的资 源而处于的一种永久等待状
● 死锁的四个必要条件
1. 互斥条件一个资源每次只能被一个执行流使用
2. 请求与保持条件一个执行流因请求资源而阻塞时对已获得的资源保持不放
3. 不剥夺条件:一个执行流已获得的资源在末使用完之前不能强行剥夺
4. 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
● 避免死锁:
1. 破坏死锁的四个必要条件
2. 加锁顺序一致
3. 避免锁未释放的场景
4. 资源一次性分配
● 避免死锁算法
1.死锁检测算法
2.银行家算法
线程同步
● 同步本质是让多执行流访问临界资源具有一定的顺序
多线程访问公共资源可能会出现问题因此我们使用互斥锁保证任何一个时刻只有1个线程访问公共资源但有些线程竞争锁的能力很强每次竞争时都会拿到锁导致其他线程长时间访问不了公共资源也就是会导致线程饥饿问题
● 互斥能保证访问资源的安全性但只有安全是不够的同步能够较为充分高效的使用资源
● 条件变量本质就是实现线程同步的一种机制是pthread库提供的一个线程向另一个线程通知信息的方式
● 举个例子理解条件变量
一张桌子两个人一个人放苹果另一个蒙着眼睛的人拿苹果放苹果的时候不能拿拿的时候不能放因此要加锁实现互斥而拿苹果的人不知道放苹果的人什么时候放因此拿苹果的人不断的申请锁检测释放锁导致了放苹果人的饥饿问题
于是放了一个铃铛让放苹果的人放苹果之后敲一下铃铛此时拿苹果的人再去拿苹果!
上述的铃铛本质就是条件变量可以理解成以下结构体:
struct cond
{int flag; //条件是否就绪tcb_queue; //条件不满足就排队!
}
● 上述例子中只有1个拿苹果的人实际可以有多个拿苹果的人可以认为所有拿苹果的人都要排队拿完苹果之后再去队尾重新排队这就使得多执行流可以较为均衡地按照一定顺序访问资源
● 条件变量使用接口
在cond条件变量下进行等待
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);唤醒条件变量下等待的一个线程
int pthread_cond_signal(pthread_cond_t *cond);唤醒条件变量下等待的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);● 条件变量使用示例
#include iostream
#include pthread.h
#include unistd.h
#include cstring
using namespace std;pthread_cond_t cond PTHREAD_COND_INITIALIZER; // 定义条件变量
pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; // 定义全局锁void *threadRoutine(void *args)
{const char* threadname static_castconst char*(args);while(true){pthread_mutex_lock(mutex);pthread_cond_wait(cond, mutex); //在指定的条件变量下等待cout I am a new thread: threadname endl;pthread_mutex_unlock(mutex);}
}int main()
{pthread_t t1, t2, t3;pthread_create(t1, nullptr, threadRoutine, (void*)thread-1);pthread_create(t2, nullptr, threadRoutine, (void*)thread-2);pthread_create(t3, nullptr, threadRoutine, (void*)thread-3);sleep(5); // 5s之后唤醒线程while(true){pthread_cond_signal(cond); //每次唤醒一个线程sleep(1);}pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);
} 说明:
● cout是往显示器上打印多个线程都执行cout语句访问显示器资源此时显示器资源也是公共资源打印会出现混乱的情况因此显示器资源也需要被保护起来因此在cout语句前后加锁解锁
● 只加锁发现线程1、2、3打印没有任何的顺序性且一个线程一打印就是一批语句这就是竞争锁的能力不同而导致的
● 为了让线程1、2、3打印具有一定的顺序性我们引入了条件变量在加锁和解锁之间使用pthread_cond_wait 接口让线程在条件变量下等待在主线程中每隔1s唤醒一个线程从而使得打印结果具有明显的顺序性
● 如果在主线程中每隔1s唤醒所有线程那么所有线程都会去参与锁的竞争因此最后打印的顺序依旧不确定
抢票代码加入线程同步:
#include iostream
#include pthread.h
#include unistd.h
#include cstring
using namespace std;pthread_cond_t cond PTHREAD_COND_INITIALIZER; // 定义条件变量
pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; // 定义全局锁int tickets 5000;void *threadRoutine(void *args)
{string threadname static_castconst char *(args);while (true){pthread_mutex_lock(mutex);if (tickets 0){usleep(1000); // 充当抢票花费的时间cout threadname : get a ticket, ticket : tickets endl;tickets--;}else{cout threadname , 没有票了 endl;pthread_cond_wait(cond, mutex); // 没有票了, 就去条件变量下等待}pthread_mutex_unlock(mutex);}
}int main()
{pthread_t t1, t2, t3;pthread_create(t1, nullptr, threadRoutine, (void *)thread-1);pthread_create(t2, nullptr, threadRoutine, (void *)thread-2);pthread_create(t3, nullptr, threadRoutine, (void *)thread-3);sleep(5); // 5s之后唤醒线程while (true){sleep(6);pthread_mutex_lock(mutex);tickets 1000; // 每隔6s, 就再放1000张票pthread_mutex_unlock(mutex);pthread_cond_signal(cond); // 唤醒一个线程// pthread_cond_broadcast(cond); // 唤醒所有线程}pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);return 0;
}说明:
● 加锁和解锁之间往往要访问临界资源但是临界资源不一定是满足条件的所以我们要判断条件不满足就应该让线程去条件变量下等待
● 线程在条件变量下等待的时候, 会自动释放锁
● 线程被唤醒的时候是在临界区内被唤醒的当线程被唤醒在wait函数返回时要重新申请并持有锁才能真正被唤醒这也就是 pthread_cond_wait 的参数中同时有条件变量和锁的原因而重新申请并持有锁也是要参与锁的竞争的!