上面概念有些抽象,我们来看一个实际的例子方便我们理解——抢票系统:
代码:
// 操作共享变量会有问题的售票系统代码
#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);
}
执行结果:
这是没有加锁(互斥)的代码执行的结果,发现我们抢票抢着抢着竟然抢到了负数!这是万万不行的。
共享资源被访问的时候,没有被保护,并且本身操作不是原子的!
前两者我们好理解,
ticket需要先从内存中读取数据放在CPU上,然后CPU进行加法或者减法操作,最后再将数据放在内存当中。因此就不是原子性的。
-- 操作并不是原子操作,而是对应三条汇编指令:
要解决以上问题,需要做到三点:
要做到这三点,本质上就是需要一把锁。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
这种锁需要我们在局部代码段进行定义和初始化,并且也需要我们自己去手动销毁。
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,失败返回错误号
C++注重RAII的编程思想,所以我们可以将锁自己封装成为一个RAII风格的锁
我们可以将锁进行封装,定义一个LockGuard的类,里面只有一个锁的成员变量,构造函数是加锁,析构函数是解锁,所以我们可以创建一个局部的对象,让编译器自己去调用构造函数和析构函数,这样就不需要我们进行加锁和解锁
#ifndef __LOCK_GUARD_HPP__
#define __LOCK_GUARD_HPP__
#include <iostream>
#include <pthread.h>
class LockGuard
{
public:
LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
{
pthread_mutex_lock(_mutex); // 构造加锁
}
~LockGuard()
{
pthread_mutex_unlock(_mutex);//析构解锁
}
private:
pthread_mutex_t *_mutex;
};
#endif
在我们学习了如何加锁之后,我们就可以将抢票系统进行进一步的优化:
void route(ThreadData *td)
{
while (true)
{
{ // 担心就用这个
LockGuard guard(&td->_mutex); // 临时对象, RAII风格的加锁和解锁
//std::lock_guard<std::mutex> lock(td->_mutex);
//pthread_mutex_lock(&td->_mutex);
if (td->_tickets > 0) // 1
{
usleep(1000);
printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets); // 2
td->_tickets--; // 3
//pthread_mutex_unlock(&td->_mutex);
td->_total++;
}
else
{
//pthread_mutex_unlock(&td->_mutex);
//td->_mutex.unlock();
break;
}
}
}
}
执行结果:
可以看出,加锁之后就完美解决了票数会抢到负数的问题!
所有线程在争锁的时候,只有一个锁,交换的过程,只有一条是汇编——所以该过程是原子的
CPU寄存器硬件只有一套,但是CPU寄存器内部的数据,数据线程的硬件上下文是有多套的。
数据在内存中,所有的线程都能访问,属于共享的。但是如果转移到CPU内部寄存器中,就属于一个线程私有的了!!!