临界资源:多个执行流都能看到并能访问的资源,临界资源。
临界区:多个执行流代码中有不同的代码,访问临界资源的代码,我们称之为临界区。
互斥特性:当我们访问某种资源的时候,任何时刻,都只有一个执行流在进行访问,这个就叫做:互斥特性!!!,访问临界资源,通常对临界资源起保护作用。
线程互斥:指的是在多个线程间对临界资源进行争抢访问时有可能会造成数据二义,因此通过保证同一时间只有一个线程能够访问临界资源的方式实现线程对临界资源的访问安全性。
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。
int tickets;
tickets--;
tickets->是由3条语句完成的:
– 操作并不是原子操作,而是对应三条汇编指令:
① load tickets to 寄存器
② 更新寄存器里面的值,执行-1操作;
③ 将新值,从寄存器写回共享变量ticket的内存地址
CPU内的寄存器是被所有的执行流共享的,但是寄存器里面的数据是属于当前执行流的上下文数据。线程被切换的时候,需要保存上下文;线程被换回的时候,需要恢复上下文。
情况1:线程A先抢到一张票时,寄存器中tickets 10000——>9999, 还未写回内存,A的时间片到了就被切走了,开始执行线程B了;线程B也抢票,直接抢了9950张,还剩50张,此时B的时间片到了,又切回线程A,又把9999写入内存,就错误了。
情况2:或者在抢最后一张时,线程A先抢最后一张票,if (tickets > 0)为真,进入if语句,此时A的时间片到了就被切走了,开始执行线程B了;线程B也抢票,此时显示票数仍是1,if (tickets > 0)为真,进入if语句,并执行tickets–;,tickets变为0,此时B的时间片到了,又切回线程A,线程A又继续执行tickets–;,此时直接把票数减到了负数,就出错了。
#include
#include
#include
#include
#include
using namespace std;
int tickets = 10000;
void *getTicket(void *args) {
const char *name = static_cast<const char *>(args);
while (true) {
if (tickets > 0) {
usleep(1254);
cout << name << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;
//模拟其他业务逻辑的执行
}
else {
cout << "票抢完了 >>>>> !!!!!" << endl;
break;
}
}
return nullptr;
}
int main() {
pthread_t t1, t2, t3, t4;
pthread_create(&t1, nullptr, getTicket, (void *)"thread 1");
pthread_create(&t2, nullptr, getTicket, (void *)"thread 2");
pthread_create(&t3, nullptr, getTicket, (void *)"thread 3");
pthread_create(&t4, nullptr, getTicket, (void *)"thread 4");
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_join(t4, nullptr);
return 0;
}
原子性:一件事要么不做,要么全做完。
把tickets–这个临界区设为原子的,使不想被打扰,进行加锁。
加锁范围:临界区,只要对临界区加锁,而且加锁的力度越细越好
加锁本质:加锁的本质是让线程执行临界区代码串行化
加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加
锁保护的是临界区, 任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁!
那这把锁,本身不就也是临界资源吗?锁的设计者早就想到了!
pthread_mutex_lock: 竞争和申请锁的过程,就是原子的!申请锁的过程不会中断,不会被打扰。
难度在加锁的临界区里面,就没有线程切换了吗????
mutex简单理解就是一个0/1的计数器,用于标记资源访问状态:
0表示已经有执行流加锁成功,资源处于不可访问,
1表示未加锁,资源可访问。
下列操作中,需要执行加锁的操作是()[多选]
A.x++;
B.x=y;
C.++x;
D.x=1;
答:D 常量的直接赋值是一个原子操作
ABC选项中涉及到了数据的运算,则涉及从内存加载数据到寄存器,在寄存器中运算,将寄存器中数据交还内存的过程,因此需要加锁保护的操作中,正确选项为:ABC
初始化互斥量有两种:
方法1 静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法2 动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数: mutex:要初始化的互斥量
attr:NULL
注意:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调用 pthread_ lock 时,可能会遇到以下情况:
#include
#include
#include
#include
#include
#include
using namespace std;
int tickets = 10000;
pthread_mutex_t mutex;
void *getTickets(void* args) {
const char* name = static_cast<const char *>(args);
while (true) {
pthread_mutex_lock(&mutex);
if (tickets > 0) {
usleep(800);
cout << name << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;
pthread_mutex_unlock(&mutex);
}
else {
cout << name << "] 已经放弃抢票了,因为没有了..." << endl;
pthread_mutex_unlock(&mutex);
break;
}
}
return nullptr;
}
int main() {
pthread_mutex_init(&mutex,nullptr); // 上锁
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;
pthread_t tid4;
pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");
pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");
pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");
pthread_create(&tid4, nullptr, getTickets, (void *)"thread 4");
int n = pthread_join(tid1, nullptr);
cout << n << ":" << strerror(n) << endl;
n = pthread_join(tid2, nullptr);
cout << n << ":" << strerror(n) << endl;
n = pthread_join(tid3, nullptr);
cout << n << ":" << strerror(n) << endl;
n = pthread_join(tid4, nullptr);
cout << n << ":" << strerror(n) << endl;
pthread_mutex_destroy(&mutex);// 销毁锁
}
#include
#include
#include
using namespace std;
int tickets = 1000;
void* startRoutine(void * args) {
pthread_mutex_t* mutex_p = static_cast<pthread_mutex_t*>(args);
while (true) {
pthread_mutex_lock(mutex_p);
if (tickets > 0) {
usleep(1000);
cout << "thread: " << pthread_self() << "get a ticket: " << tickets << endl;
tickets --;
pthread_mutex_unlock(mutex_p);
//做其他的事
usleep(500);
}
else {
pthread_mutex_unlock(mutex_p);
break;
}
}
return nullptr;
}
int main() {
pthread_t t1, t2, t3, t4;
static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_create(&t1, nullptr, startRoutine, (void *)&mutex);
pthread_create(&t2, nullptr, startRoutine, (void *)&mutex);
pthread_create(&t3, nullptr, startRoutine, (void *)&mutex);
pthread_create(&t4, nullptr, startRoutine, (void *)&mutex);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_join(t4, nullptr);
pthread_mutex_destroy(&mutex);
return 0;
}
我在临界资源对应的临界区中上锁了,临界区还是多行代码,是多行代码就可以被切换。加锁 不等于 不会被切换。加锁后仍然可以切换进程,因为线程执行的加锁解锁等对应的也是代码,线程在任意代码处都可以被切换,只是线程加锁是原子的——要么你拿到了锁,要么没有。
因为每个线程进入临界区都必须先申请锁!
假设当前的锁被A申请走了,即便当前的线程A没有被调度,因为它被切走的时候是抱着锁走的.
其他线程想进入临界区需要先申请锁,但是已经有线程A持有锁了,则其他线程在申请时会被阻塞。
即:一旦一个线程持有了锁,该线程根本就不担心任何的切换问题!对于其他线程而言,线程A访问临界区,只有没有进入和使用完毕两种状态,才对其他线程有意义!
总之:对于其他线程而言,线程A访问临界区具有一定的原子性
注意:尽量不要在临界区内做耗时的事情!因为只有持有锁的线程能访问,其他线程都会阻塞等待。
每一个CPU任何时刻只能有一个线程在跑
单独的一条汇编代码是具有原子性的
经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题 为了实现互斥锁操作,大多数体系结构(芯片体系结构)都提供了swap或exchange指令,该指令的作用是使用一条汇编代码把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
mutex中的值默认是1
%al :CPU中的寄存器( 凡是在寄存器中的数据,全部都是线程的内部上下文! !)
mutex :内存中的一个变量
加锁原理解释:线程A执行 movb $0,%al :把0放入寄存器%al中。然后执行xchgb %al, mutex:通过一条汇编代码交换 寄存器%al (值是0) 和 变量mutex (值是1) 的值,交换后 寄存器%al中是1, 变量mutex中是0。
还未执行判断,此时突然进程切换,线程A会自动带走%al中的上下文数据1,线程B开始执行:线程B执行 movb $0,%al :把0放入寄存器%al中。然后执行xchgb %al, mutex:通过一条汇编代码交换 寄存器%al (值是0) 和 变量mutex (值是0) 的值,交换后 寄存器%al 和 变量mutex中都是0。再判断——>因为%al是0,不大于0就挂起。此时线程B挂起,该线程A继续执行,线程A会把自己上下文数据恢复到%al中,此时%al=1,该执行判断了——>因为%al是1,就返回。这样就成功做到:多个线程看起来同时在访问寄存器,但是互不影响
线程安全指的是在多线程编程中,多个线程对临界资源进行争抢访问而不会造成数据二义或程序逻辑混乱的情况。
线程安全的实现,通过同步与互斥实现。
线程是安全的,但是线程中调用的函数不一定是可重入函数,因为线程安全指的是当前线程中对各项操作时安全的,但不表示内部调用的函数是安全的,两个之间并没有必然关系
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。(我们写的不加锁的抢票函数就是线程不安全函数,因为可能抢票抢到-1)
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。(90%函数是不可重入函数,带_r是可重入函数,不带_r是不可重入函数)
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。