C/C++ — 线程锁
锁的分类比较
1、单线程无锁速度最快,应用场合局限性很大。
2、多线程无锁速度第二快,但是同时操作同一块内存空间,会造成数据污染或者奔溃。
3、多线程原子锁第三快,并且结果不会被污染。
4、多线程**互斥锁(量)**比较慢,与原子锁相比较相差10倍,结果不会被污染。
5、多线程自旋锁最慢,是原子锁的三十分之一,结果正确
所以: 原子锁速度最快,互斥锁和自旋锁相对比较慢但是数据都可以保证。
自旋锁
非阻塞锁,就是Objective-C中的原子性,同一时间只允许一个线程写入。如果该锁已经被占用,则不会被挂起。一致消耗CPU,不停的试图获取锁。
互斥锁(量)
阻塞锁,就是Objective-C中的非原子性。某个线程无法获取互斥锁时,该线程直接挂起,不在消耗资源,当线程释放互斥量之后,操作系统会激活被挂起的线程。
原子锁
atomic原子操作:是在新标准C++11,引入了原子操作的概念,并通过这个新的头文件提供了多种原子操作数据类型,例如,atomic_bool,atomic_int等等
测试代码:
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <time.h>
#define TEST_DATA_LENGTH 100000 //每个线程操作次数
#define THREAD_NUM 10 //线程个数
using namespace std;//引入std命名空间
mutex m;//声明互斥锁m
long n_total = 0;
long m_total = 0;
atomic<long> a_total = 0;//原子量的初始化
//在不采用互斥锁和原子类的情况下
void test_f_normal()
{
for(int i = 0; i < TEST_DATA_LENGTH; i++)
{
n_total += 1;
}
}
//使用mutex互斥锁
void test_f_mutex()
{
for(int i = 0; i < TEST_DATA_LENGTH; i++)
{
m.lock();
m_total += 1;
m.unlock();
}
}
//使用原子操作
void test_f_atomic()
{
for(int i = 0; i < TEST_DATA_LENGTH; i++)
{
a_total += 1;
}
}
void main()
{
thread ts[THREAD_NUM];
//normal mode ,result is error
clock_t start = clock();
for(int i = 0; i < THREAD_NUM; i++)
{
ts[i] = thread(&test_f_normal);
}
for(int i = 0; i < THREAD_NUM; i++)
{
ts[i].join();
}
clock_t end = clock();
cout << "total: " << n_total << endl;
cout << "test_f_normal: " << end - start << endl;
//use mutex ,
clock_t mstart = clock();
for(int i = 0; i < THREAD_NUM; i++)
{
ts[i] = thread(&test_f_mutex);
}
for(int i = 0; i < THREAD_NUM; i++)
{
ts[i].join();
}
clock_t mend = clock();
cout << "total: " << m_total << endl;
cout << "test_f_mutex: " << mend - mstart << endl;
//use atomic
clock_t astart = clock();
for(int i = 0; i < THREAD_NUM; i++)
{
ts[i] = thread(&test_f_atomic);
}
for(int i = 0; i < THREAD_NUM; i++)
{
ts[i].join();
}
clock_t aend = clock();
cout << "total: " << a_total << endl;
cout << "test_f_atomic: " << aend - astart << endl;
system("pause");
return;
}
// 结果
total: 601409
test_f_normal: 29
total: 1000000
test_f_mutex: 11274
total: 1000000
test_f_atomic: 35
总结:
1.在不使用互斥锁和原子量的时候,多线程的操作会使结果是错误的.
2.原子操作的实现跟普通数据类型类似,但是它能够在保证结果正确的前提下,提供比mutex等锁机制更好的性能
提示:开发过程中,对于多线程的情况下,单个基础数据类型的数据共享安全,尽量使用原子操作代替锁机制. 当需要对代码块进行数据安全保护的时候,就需要选择使用锁机制或者自旋锁了。
扩展
Interlocked原子访问
在Windows核心编程中,可以通过Interlocked系列函数实现原子方式控制一个值。
Interlocked函数的工作原理取决于代码运行的CPU平台,如果是x86系列CPU,那么Interlocked函数会在总线上维持一个硬件信号,这个信号会阻止其他CPU访问同一个内存地址。([文档])
LONG InterlockedExchange(
LPLONG Target,
LONG Value
);
InterlockedExchange (a,b) 能以原子的方式交换两个参数a、b并返回a以前的值。因为InterlockedExchange是原子函数,不会要求中止中断,所以交换指针的方式是安全的。
假设有线程1和线程2调用func()函数,线程1先调用InterlockedExchange(&g, TRUE);,线程2在调用时,函数InterlockedExchange()总会返回TRUE ,则线程2 sleep,而直到线程1调用InterlockedExchange(&g, FALSE);后线程2才可能由于调用InterlockedExchange(&g, TRUE),返回FALSE而退出循环接着工作。这样在进行其他的操作这里就能操作共享数据而不会引起争议。当然这种方法会浪费CPU时间,因为CPU要不断地执行InterlockedExchange()函数,使用时应注意。
// 线程1
InterlockedExchange(&g, TRUE); // 步一
//..
InterlockedExchange(&g, FALSE); // 步三
//线程2
while(TRUE == InterlockedExchange(&g, TRUE))// 步二
{
sleep(0);
// 等待
}
// 线程2跳出循环了 // 步四
pthread_mutex_t
利用线程的原子锁实现原子访问
#include <pthread.h>
#include <stdio.h>
#include <string>
class CLocker
{
pthread_mutex_t mutex;
bool m_bLog;
public:
CLocker(){
pthread_mutex_init (&mutex,NULL);
m_bLog = false;
}
void lock(){
pthread_mutex_lock(&mutex);
}
void lock(std::string strFuncName){
if (m_bLog == true)
printf("[LOCK]: %s \n",strFuncName.c_str());
pthread_mutex_lock(&mutex);
}
bool trylock(){
return (pthread_mutex_trylock(&mutex) == 0)?true:false;
}
bool trylock(std::string strFuncName){
if (m_bLog == true)
printf("[TRYLOCK]: %s \n",strFuncName.c_str());
return (pthread_mutex_trylock(&mutex) == 0)?true:false;
}
void unlock(std::string strFuncName){
if (m_bLog == true)
printf("[UNLOCK]: %s \n",strFuncName.c_str());
pthread_mutex_unlock(&mutex);
}
void unlock(){
pthread_mutex_unlock(&mutex);
}
void enablelog(bool bLog){
m_bLog = bLog;
}
};
无锁化编程
__sync_fetch_and_or
__sync_fetch_and_and
我们知道,count++这种操作不是原子的。一个自加操作,本质是分成三步的:
1、 从缓存取到寄存器。
2、 在寄存器加1。
3、 存入缓存。
__sync_fetch_and_add系列一共有十二个函数,有加/减/与/或/异或/等函数的原子性操作函数,
__snyc_fetch_and_add : 先fetch然后自加,返回的是自加以前的值
__snyc_add_and_fetch : 先自加然后返回,返回的是自加以后的值 (参照 ++i 和 i++)
__snyc_fetch_and_add的一个简单使用
int count = 4;
__sync_fetch_and_add(&count, 1); // __sync_fetch_and_add(&count, 1) == 4
cout<<count<<endl; //--->count=5
对于多线程对全局变量进行自加,我们就再也不用理线程锁了。
下面这行代码,和上面被pthread_mutex保护的那行代码作用是一样的,而且也是线程安全的。
__sync_fetch_and_add( &global_int, 1 );
__sync_fetch_and_add,速度是线程锁的6~7倍
函数集
//在用gcc编译的时候要加上选项 -march=i686
type __sync_fetch_and_add (type *ptr, type value, ...);
type __sync_fetch_and_sub (type *ptr, type value, ...);
type __sync_fetch_and_or (type *ptr, type value, ...);
type __sync_fetch_and_and (type *ptr, type value, ...);
type __sync_fetch_and_xor (type *ptr, type value, ...);
type __sync_fetch_and_nand (type *ptr, type value, ...);
type __sync_add_and_fetch (type *ptr, type value, ...);
type __sync_sub_and_fetch (type *ptr, type value, ...);
type __sync_or_and_fetch (type *ptr, type value, ...);
type __sync_and_and_fetch (type *ptr, type value, ...);
type __sync_xor_and_fetch (type *ptr, type value, ...);
type __sync_nand_and_fetch (type *ptr, type value, ...);
type可以是1,2,3或者8字节长度的int类型,即
int8_t
uint8_t
int16_t
uint16_t
int32_t
uint32_t
int64_t
uint64_t
除了上面提到的12个外 还有4个可以实现互斥锁的功能
//以下两个函数提供原子的比较和交换, 如果*ptr = oldValue, 就将newValue写入*ptr
//第一个函数在相等并写入的情况下返回true
//第二个函数返回操作之前的值
bool __sync_bool_compare_and_swap(type* ptr, type oldValue, type newValue, ....);
type __sync_val_compare_and_swap(type* ptr, type oldValue, type newValue, ....);
//将*ptr设为value并返回*ptr操作之前的值
type __sync_lock_test_and_set(type *ptr, type value, ....);
//置*ptr为0
void __sync_lock_release(type* ptr, ....);