一、我们知道,同一个进程中的所有线程是共享进程资源的。因此多线程中,线程同步则成为了一个很重要的话题。所谓“同步”,即“协同步调,按预定的先后次序进行运行”,同步是自愿的,参与者必须协调工作使系统运行(此句引用于《POSIX多线程程序设计》)。所以“既需要在线程间共享数据,又需要以一致的顺序在线程间执行一组操作”。则必须借助于线程的同步手段、机制来实现。线程同步方法较多,常用到的有:互斥锁、条件变量(配合互斥锁)、信号量、读写锁、自旋锁等。其中互斥锁是所有“线程同步”手段中最为简单且容易理解的一种。互斥锁和条件变量都出自于POSIX.1
线程标准,它们总是可用来同步一个进程内的各个线程的。
二、互斥锁,一次只能允许有一个线程能够获得互斥量。尝试获取同一个互斥量的其他线程必须等待目前拥有该互斥量的线程将其释放。互斥锁提供了同步的能力,可以控制线程如何共享资源。使用互斥锁可以避免在同一时刻多个线程修改共享的数据并且保证一个线程能够从一套资源中读到一致性数据,其他线程可以修改这些资源。如图:
线程A在对“共享数据”资源进行读写操作的时候,对其进行加锁,这时候线程B是无法对其进行“共享数据”资源操作的,只能耐心的等待线程A将锁打开(释放)。只有当线程A释放了锁之后,线程B才能接着对“共享数据”进行操作。同步不仅仅在修改数据时重要,当线程需要去读取其他线程写入的数据时,而且数据写入的顺序也有影响时,同样是需要同步的。
三、互斥锁常用的API有下面几个(互斥量属性设置高级API这里没有给出)
/**@fn pthread_mutex_init
* @brief 初始化互斥量(attr为互斥量设置属性)
* @param[in] pthread_mutex_t *__mutex, 互斥量
* @param[in] const pthread_mutexattr_t *attr 互斥量高级属性
* @param[out] NONE
* @description 常见错误码:[ENOMEN]内存不足,[EPERM]无权执行此项操作,[EBUSY]互斥量已经初始化,[EINVAL]attr无效
* @return int
**/
int pthread_mutex_init (pthread_mutex_t *__mutex,
const pthread_mutexattr_t *attr);
/**@fn pthread_mutex_lock
* @brief 互斥量加锁(如果当前该互斥量已经加锁,主调线程便会阻塞,直到该互斥量解锁为止。
* 一旦该互斥量解锁,则主调线程就会占有这个互斥量,直到其解锁)-阻塞式
* @param[in] pthread_mutex_t *__mutex, 互斥量
* @param[out] NONE
* @description 常见错误码:[EINVAL]互斥量无线,[EDEADLK] 主调线程已经占有了互斥量
* @return int
**/
int pthread_mutex_lock (pthread_mutex_t *__mutex);
/**@fn pthread_mutex_trylock
* @brief 互斥量加锁(如果当前互斥量已经上锁,则立刻返回 EBUSY,否则,主调
* 线程成为该互斥量的拥有者,并且直到它解锁为止)-非阻塞式
* @param[in] pthread_mutex_t *__mutex, 互斥量
* @param[out] NONE
* @description 常见错误码:[EINVAL] 线程优先级超过互斥量优先级的最大限度,[EDEADLK] 主调线程已经占用这个锁
* [EBUSY] 互斥量已经被加锁,[EINVAL] 互斥量无效
* @return int
**/
int pthread_mutex_trylock (pthread_mutex_t *__mutex);
/**@fn pthread_mutex_timedlock
* @brief 互斥量加锁(等待锁变为可用,或者指定时间过去)
* @param[in] pthread_mutex_t *__mutex, 互斥量
* @param[in] const struct timespec *__restrict __abstime 等待获取所超时时间
* @param[out] NONE
* @description 常见错误码:[EINVAL]互斥量无效,[EPERM]主调线程已经占有了互斥量
* @return int
**/
int pthread_mutex_timedlock (pthread_mutex_t *__restrict __mutex,
const struct timespec *__restrict __abstime);
/**@fn pthread_mutex_unlock
* @brief 互斥量解锁(解锁后,该互斥量便自由了。如果此时有线程试图或等待这个互斥量,
* 则将会被唤醒,然后对这个互斥量进行加锁/解锁)
* @param[in] pthread_mutex_t *__mutex, 互斥量
* @param[out] NONE
* @description 常见错误码:[EINVAL]互斥量无效,[EPERM]主调线程已经占有了互斥量
* @return int
**/
int pthread_mutex_unlock (pthread_mutex_t *__mutex);
/**@fn pthread_mutex_destroy
* @brief 销毁/释放互斥锁
* @param[in] pthread_mutex_t *__mutex, 互斥量
* @param[out] NONE
* @description 常见错误码:[EINVAL]互斥量无效,[EPERM]主调线程已经占有了互斥量
* @return int
**/
int pthread_mutex_destroy (pthread_mutex_t *__mutex);
/**@fn pthread_mutex_getprioceiling
* @brief 获取互斥锁的优先级上限
* @param[in] const pthread_mutex_t *__restrict __mutex 互斥量
* @param[out] int *__restrict __prioceiling 互斥锁优先级
* @description
* @return int
**/
int pthread_mutex_getprioceiling (const pthread_mutex_t *
__restrict __mutex,
int *__restrict __prioceiling)
/**@fn pthread_mutex_setprioceiling
* @brief 设置互斥锁的优先级上限
* @param[in] const pthread_mutex_t *__restrict __mutex 互斥量
* @param[in] int __prioceiling 互斥锁待设置优先级
* @param[in] int *__restrict __old_ceiling 互斥锁旧优先级
* @param[out] NONE
* @description 将互斥锁的优先级上限设置为PRIOCEILING,在*OLD_CEILING中返回旧的优先级上限
* @return int
**/
int pthread_mutex_setprioceiling (pthread_mutex_t *__restrict __mutex,
int __prioceiling,
int *__restrict __old_ceiling);
四、代码示例一,一个线程不断的望std::vector
容器中生产数据,另一个线程不断的从该容器中消费数据。为了让效果更加直观,每生产一条数据的时候,都会睡眠一微妙。这是典型的生产者-消费者模式。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <pthread.h>
#include <istream>
#include <vector>
#include <queue>
#include <cassert>
#include <unistd.h>
using namespace std;
#define ERR_PRT(v,m) do{if(v != 0){printf("errno[%d] msg[%s]\n",v,m);exit(v);}}while(0);
typedef struct{
pthread_mutex_t m_mutex;
std::vector<int> m_Vec;
}MutexVar;
static int iGlobal;
MutexVar strMut;
/**@fn threadWrite
* @brief 向容器中生产数据
* @param[in] pVtemp.m_mutex 互斥锁
* @param[in] pVtemp.m_Vec 数据容器
* @param[out] null
* @param[out] Void*
* @return 错误码
**/
void *threadWrite(void *pVtemp)
{
do
{
if(0 != pthread_mutex_lock(&strMut.m_mutex))
{
printf("pthread_mutex_lock failed.\n");
return (void*)-1;
}
strMut.m_Vec.push_back(iGlobal++);
printf("production data[%d] successful.\n",strMut.m_Vec.back());
if(0!=pthread_mutex_unlock(&strMut.m_mutex))
{
printf("pthread_mutex_lock failed.\n");
return (void*)-1;
}
usleep(1000);
}while(true);
return NULL;
}
/**@fn threadRead
* @brief 向容器中消费数据
* @param[in] pVtemp.m_mutex 互斥锁
* @param[in] pVtemp.m_Vec 数据容器
* @param[out] null
* @param[out] Void*
* @return 错误码
**/
void *threadRead(void *pVtemp)
{
do
{
if(strMut.m_Vec.size()>0)
{
printf("Consumption data[%d] successful.\n", strMut.m_Vec.back());
strMut.m_Vec.pop_back();
goto UNLOCK;
}
else
{
usleep(1000);
}
UNLOCK:
if(0!=pthread_mutex_unlock(&strMut.m_mutex))
{
printf("pthread_mutex_lock failed.\n");
return (void*)-1;
}
usleep(1000);
}while(true);
return NULL;
}
int main(int argc,char **argv)
{
pthread_t threadId[2] = {0};
int iRet = 0;
iRet = pthread_mutex_init(&strMut.m_mutex,NULL);
ERR_PRT(iRet,"pthread_mutex_init failed");
iRet = pthread_create(&threadId[0],NULL,threadRead,NULL);
ERR_PRT(iRet,"pthread_create threadRead failed");
iRet = pthread_create(&threadId[1],NULL,threadWrite,NULL);
ERR_PRT(iRet,"pthread_create threadWrite failed");
for(;;);
for(int i=0;i<2;++i)
{
pthread_join(threadId[i],NULL);
}
pthread_mutex_destroy(&strMut.m_mutex);
return 0;
}
效果如下:
五、使用互斥锁时,有些事项是需要多留意的;常见的有如下几个。
- (1)不能拷贝互斥量变量。拷贝的互斥量变量是不确定的。可以拷贝执行互斥量的指针,这样可以使多个函数或现场来共享互斥量来实现同步。
- (2)如果线程函数一样,互斥量也提供了“动态初始化(
pthread_mutex_init
),静态初始化(PTHREAD_MUTEX_INITIALIZER
)”两种方式。但通常情况下,更建议采用动态初始化方式,比如:要使用malloc/calloc/realloc
去动态申请分配一个包含互斥量的数据结构(如下示例一)时,则必须使用pthread_mutex_init
去初始化互斥量。 - (3)不需要释放一个使用
PTHREAD_MUTEX_INITIALIZER
静态初始化的互斥量。 - (4)将互斥量与它要保护的数据联系在一起是个不错的主意(《UNIX网络编程》和《POSIX多线程程序设计》也曾多次提到)。如:
typedef struct
{
pthread_mutex_t m_Mutex; //互斥量
int m_val; //变量值(通常是一个共享的数据)
}strMutex;
六、pthread_mutex_trylock的使用
前面示例一中讲述了pthread_mutex_lock
的使用,该API是阻塞的。而pthread_mutex_trylock
是非阻塞的,它会尝试去对互斥量加锁,若发现该互斥量已经被其他线程上锁,则会立刻返回EBUSY,而不是选择一直阻塞在那里等待互斥量被释放。EBUSY定义在#include <errno.h>
中,如下:
#define EBUSY 16 /* Device or resource busy */
示例二代码中有2个线程,一个线程在指定的时间内,不断的自增(每次自增过后的值不大于MAX_NUM
),然后睡眠1秒;而另外一个线程(也在指定的时间内)则先睡眠3秒后,然后尝试对互斥量进行加锁,若该互斥量已被加锁,则会返回EBUSY,同时记录尝试加锁失败的次数,如果成功,则打印此时的全局自增变量lCount
的值。
/*************************************************************************
* File Name: _MutexCount.cpp
* Author: The answer
* Function: Other
* Mail: [email protected]
* Created Time: 2018年12月15日 星期六 19时06分08秒
************************************************************************/
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <pthread.h>
#include <stdlib.h>
#include <time.h>
#include <assert.h>
using namespace std;
#ifndef MAX_NUM
#define MAX_NUM 10000000
#endif
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //
static long int lCount = 0;
/**@fn addNumThread
* @brief 增加变量的值
* @param[in] void* pParam 指定时间大小
* @param[out] NULL
* @return 错误码
**/
void* addNumThread(void* pParam)
{
do
{
if(!pParam)
{
printf("addNumThread param is null. pParam[%p]\n",pParam);
exit(-1);
}
time_t *pTime = static_cast<time_t*>(pParam);
for(;time(NULL) < *pTime;)
{
int i = 0;
assert(0 == pthread_mutex_lock(&mutex));
while(i<MAX_NUM)
{
lCount++;
++i;
}
assert(0 == pthread_mutex_unlock(&mutex));
sleep(1); //sleep 1 s
}
printf("addNumThread lCount[%ld]\n",lCount);
}while(false);
return NULL;
}
/**@fn checkNumThread
* @brief 打印变量的值
* @param[in] void* pParam 指定时间大小
* @param[out] NULL
* @return 错误码
**/
void* checkNumThread(void* pParam)
{
int iMissLockNum = 0;
int iRet = 0;
do
{
if(!pParam)
{
printf("checkNumThread param is null. pParam[%p]\n",pParam);
exit(-1);
}
time_t *pTime = static_cast<time_t*>(pParam);
for(;time(NULL) < *pTime;)
{
sleep(3); //sleep 3 s
iRet = pthread_mutex_trylock(&mutex);
if(EBUSY == iRet)
{
printf("trylock EBUSY....\n");
iMissLockNum++;
continue;
}
else
{
if(0 != iRet)
{
printf("pthread_mutex_trylock failed. iRet[%d]\n",iRet);
exit(-1);
}
printf("checkNumThread lCount[%ld lCount/MAX_NUM[%ld]\n",lCount,lCount/MAX_NUM);
assert(0 == pthread_mutex_unlock(&mutex));
}
}
printf("chenkNumThread iMissLockNum[%d]\n",iMissLockNum);
}while(false);
return NULL;
}
int main(int argc,char **argv)
{
pthread_t threadId[2] = {0};
time_t tTime = time(NULL) + 30; //30s
int iRet = 0;
iRet = pthread_create(&threadId[0],NULL,addNumThread,&tTime);
if(0 != iRet)
{
printf("pthread_create failed. iRet[%d]\n",iRet);
exit(0);
}
iRet = pthread_create(&threadId[1],NULL,checkNumThread,&tTime);
if(0 != iRet)
{
printf("pthread_create failed. iRet[%d]\n",iRet);
exit(0);
}
for(int i =0;i<1;++i)
{
pthread_join(threadId[i],NULL);
}
//如上面所描述,静态初始化的互斥量是不需要释放的
//pthread_mutex_destroy(&mutex);
return 0;
}
打印结果:
从打印结果分析可看到,现在"checkNumThread"线程每次尝试加锁都是成功的。但是若增大MAX_NUM
的值,则会增加"checkNumThread"加锁失败的概率。现在修改#define MAX_NUM 500000000
为5亿,则结果如下: