1、线程同步的必要性
- 共享资源,多个线程都可对共享资源操作
- 线程操作共享资源的先后顺序不确定
- 处理器对存储器的操作一般不是原子操作
比如下面一个例子:两个线程同时对一个变量操作(假如对一个变量i进行加1),有可能就会出现数据混乱(即每线程对i+1但是结果不是真正的结果),注意:线程是并发的,出现这种情况很正常。下面写个例子演示一下。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 5000 //这里我让每个线程加5000次
int counter; //定义一个全局变量来让线程自加
//线程执行函数
void *fun(void *arg)
{
int i = 0;
int val = 0;
int ID = 0;
ID = (int)arg;
//每个线程加个5000次
for (i = 0; i < NLOOP; i++)
{
val = counter;
printf("我是第%d个线程: %d\n", ID+1, val + 1);
counter = val + 1;
}
return NULL;
}
int main(void)
{
//建立两个线程ID
pthread_t tidA, tidB;
//创建线程,让这两个线程执行fun函数
pthread_create(&tidA, NULL, &fun, NULL);
pthread_create(&tidB, NULL, &fun, NULL);
//回收进程
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
return 0;
}
当我们把这段函数放到虚拟机中运行,最后的结果就可能会是10000的结果,或者少于10000 的结果,但是,按照正常来说应该要是10000,而少于10000的结果就是数据混乱的结果。
注意:编译这个文件的时候要加上-lpthread,比如gcc test.c -o test -lpthread
2、互斥锁
2.1、概念(简单比喻)
互斥锁我们可以想象成在超市的保险柜(进商场前存放东西的柜子)。然后把线程比喻成一个人,你进超市的时候,你就要先找到没有锁住的柜子,把柜子锁住(表示你正在使用),这样其他的人(其他的线程)就无法再使用这个柜子了,如果想要使用该柜子就需要你解锁,别人才可以使用。
2.2、临界区
保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共 享资源进行访问。如果有多个线程试图同时访问临界区,那么 在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释 放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。(你可以这么认为,这临界区就是我上面说的柜子)。
注意:这个临界区要尽可能的小!太大会影响程序的性能!
2.3、互斥锁相关的API函数
- pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 作用:创建一个静态的互斥锁
- 解析:pthread_mutex_t是一个结构体,PTHREAD_MUTEX_INITIALIZER是一个常量,这样创建出来的额互斥锁是默认属性的互斥锁。
- int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
- 作用:初始化一个互斥锁,常用于动态的互斥锁(就是属性可换的那种)
- 解析:你需要先创建一个互斥锁,参数1就是要传一个互斥锁的地址,参数二就是要出入互斥锁的类型,如果位NULL就是默认属性的互斥锁(详细见2.4),一般常用的就是默认类型的互斥锁。
- int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 作用:销毁互斥锁
- 解析:参数1:传入要销毁的互斥锁地址。该函数销毁后,可用 pthread_mutex_init再重新定义。
- int pthread_mutex_lock(pthread_mutex_t *mutex)
- 作用:加锁(相当于我上面说的给柜子上锁,防止其他线程使用)
- 解析:参数1:互斥锁地址。该函数使用后,其他线程要是想在访问共享内存的资源就会被挂起(阻塞等待)。
- int pthread_mutex_trylock(pthread_mutex_t *mutex);
- 作用:试图加锁
- 解析:参数1:互斥锁地址。该函数和pthread_mutex_lock的功能相似,但是他试图去加锁,如果这个锁被已经其他线程上锁的话,我这个线程不会阻塞(不会等待其他线程使用完),直接返回一个错误编码,如果我这个线程加锁成功,就返回0;
- int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 作用:解锁(使用完了,让其他线程使用)
- 解析:参数1:互斥锁地址。该函数使用后,就相当于我这个保险柜用完了,可以被后面的线程的抢占了。
2.4、互斥锁属性(不重要)
注意:常用的就是默认属性,就是pthread_mutex_init函数的第二个参数传NULL
- PTHREAD_MUTEX_TIMED_NP,普通锁(默认的锁,传NULL就是这属性)。当一个线程加锁以后,其他的线程就阻塞(等待前面一个线程使用完),并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
- PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。不常用。
- PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样保证当不允许多次加锁时不出现最简单情况下的死锁。不怎么常用。
- PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。不怎么常用。
2.5、死锁
产生原因
- 同一个线程在拥有A锁的情况下再次请求获得A锁 ,
解决方法:避免两次加锁 - 线程一拥有A锁,请求获得B锁;线程二拥有B锁,请求获得A锁
解决方法:使用pthread_mutex_trylock,或者某个线程先放手
造成结果:那个线程也别想用。就会卡住
2.6、示例代码
在下面两个示例代码中,C文件建立静态互斥锁,C++那份代码采用动态建立互斥锁。
2.6.1、基于C的示例代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 5000 //用于每个线程加5000次
int counter; //建立一个全局变量给线程操作
//设计一个全局的锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *doit(void *vptr)
{
int i, val;
//谁要操作全局资源,谁拿锁
for (i = 0; i < NLOOP; i++)
{
//拿锁
pthread_mutex_lock(&mutex);
//直到解锁,都是临界区
val = counter;
printf("%x: %d\n", (unsigned int)pthread_self(), val + 1);
counter = val + 1;
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(void)
{
pthread_t tidA, tidB;
//建立线程
pthread_create(&tidA, NULL, &doit, NULL);
pthread_create(&tidB, NULL, &doit, NULL);
//回收线程
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
return 0;
}
2.6.2、基于C++的示例代码
头文件
#ifndef BASETHRED_MUTEX_H
#define BASETHRED_MUTEX_H
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
class CBaseThread_mutex
{
public:
CBaseThread_mutex();
~CBaseThread_mutex();
bool lock(); //加锁
bool unlock(); //解锁
bool trylock(); //尝试加锁
protected:
pthread_mutex_t m_mutex; //互斥锁
}
#define endif
cpp文件
#include "CBase_pthread_mutex.h"
//构造函数建立互斥锁
CBaseThread_mutex::CBaseThread_mutex()
{
//互斥量的初始化
if(pthread_mutex_init(&m_mutex,NULL)!=0)
{
perror("mutex_init error:");
}
}
//析构函数销毁互斥锁
CBaseThread_mutex::~CBaseThread_mutex()
{
//互斥量的销毁
if(pthread_mutex_destroy(&m_mutex)!=0)
{
perror("mutex_destroy error:");
}
}
//拿锁函数
bool CBaseThread_mutex::lock()
{
int ret = pthread_mutex_lock(&m_mutex);
if(ret != 0)
{
perror("mutex_lock error:");
}
return ret ==0 ? true:false
}
//解锁函数
bool CBaseThread_mutex::unlock()
{
int ret = pthread_mutex_unlock(&m_mutex);
if(ret != 0)
{
perror("mutex_unlock error:");
}
return ret ==0 ? true:false
}
//试图拿锁函数
bool CBaseThread_mutex::trylock()
{
int ret = pthread_mutex_trylock(&m_mutex);
if(ret != 0)
{
perror("mutex_lock error:");
}
return ret ==0 ? true:false
}