C++并发编程(5):std::unique_lock、互斥量所有权传递、锁的粒度

std::unique_lock<>灵活加锁

参考博客

线程间共享数据——使用互斥量保护共享数据

C++多线程unique_lock详解

多线程编程(五)——unique_lock

相较于std::lock_guard,std::unqiue_lock并不与互斥量的数据类型直接相关,因此使用起来更加灵活

它在构造时可以传入额外的参数,如std::adopt_lock与std::defer_lock等

std::unqiue_lock的时空间性能均劣于std::lock_guard,这也是它为灵活性付出的代价

std::unqiue_lock内存在某种标志用于表征其实例是否拥有特定的互斥量,显然,这些标志需要占据空间,并且标志的检查与更新也需要耗费时间

std::adopt_lock参数

  • std::adopt_lock参数表示互斥量已经被lock,不需要再重复lock
  • 该互斥量之前必须已经lock,才可以使用该参数

std::try_to_lock参数

  • 可以避免一些不必要的等待,会判断当前mutex能否被lock,如果不能被lock,可以先去执行其他代码
  • 这个和adopt不同,不需要自己提前加锁

举个例子来说就是如果有一个线程被lock,而且执行时间很长,那么另一个线程一般会被阻塞在那里,反而会造成时间的浪费。那么使用了try_to_lock后,如果被锁住了,它不会在那里阻塞等待,它可以先去执行其他没有被锁的代码

#include 
#include 

std::mutex mlock;

void work1(int& s) {
	for (int i = 1; i <= 5000; i++) {
		std::unique_lock<std::mutex> munique(mlock, std::try_to_lock);
		if (munique.owns_lock() == true) {
			s += i;
		}
		else {
			// 执行一些没有共享内存的代码
		}
	}
}

void work2(int& s) {
	for (int i = 5001; i <= 10000; i++) {
		std::unique_lock<std::mutex> munique(mlock, std::try_to_lock);
		if (munique.owns_lock() == true) {
			s += i;
		}
		else {
			// 执行一些没有共享内存的代码
		}
	}
}

int main()
{
	int ans = 0;
	std::thread t1(work1, std::ref(ans));
	std::thread t2(work2, std::ref(ans));
	t1.join();
	t2.join();
	std::cout << ans << std::endl;
	return 0;
}

std::defer_lock参数

  • 这个参数表示暂时先不lock,之后手动去lock,但是使用之前也不允许lock
  • 一般用来搭配unique_lock的成员函数去使用
#include 
#include 

std::mutex mlock;

void work1(int& s) {
	for (int i = 1; i <= 5000; i++) {
		std::unique_lock<std::mutex> munique(mlock, std::defer_lock);
		munique.lock();
		s += i;
		munique.unlock();         // 这里可以不用unlock,可以通过unique_lock的析构函数unlock
	}
}

void work2(int& s) {
	for (int i = 5001; i <= 10000; i++) {
		std::unique_lock<std::mutex> munique(mlock, std::defer_lock);
		munique.lock();
		s += i;
		munique.unlock();
	}
}

int main()
{
	int ans = 0;
	std::thread t1(work1, std::ref(ans));
	std::thread t2(work2, std::ref(ans));
	t1.join();
	t2.join();
	std::cout << ans << std::endl;
	return 0;
}

当使用了defer_lock参数时,在创建了unique_lock的对象时就不会自动加锁,那么就需要借助lock这个成员函数来进行手动加锁,当然也有unlock来手动解锁。这个和mutex的lock和unlock使用方法一样

try_lock( )成员函数

和try_to_lock参数的作用差不多

void work1(int& s) {
	for (int i = 1; i <= 5000; i++) {
		std::unique_lock<std::mutex> munique(mlock, std::defer_lock);
		if (munique.try_lock() == true) {
			s += i;
		}
		else {
			// 处理一些没有共享内存的代码
		}
	}
}

release( )成员函数

  • 解除unique_lock和mutex对象的联系,并将原mutex对象的**指针**返回出来
  • 如果之前的mutex已经加锁,需在后面自己手动unlock解锁
void work1(int& s) {
	for (int i = 1; i <= 5000; i++) {
		std::unique_lock<std::mutex> munique(mlock);   // 这里是自动lock
		std::mutex *m = munique.release();
		s += i;
		m->unlock();
	}
}

不同域中互斥量所有权的传递

由于std::unique_lock并没有与自身相关的互斥量,因此互斥量所有权可以在不同实例间相互传递,std::unique_lock是一个标准的`move only object”

互斥量所有权传递十分常见,比如在某个函数内完成对互斥量的上锁,并在其后将其所有权转交至调用者以保证它可以在该锁的保护范围内执行额外操作

std::unique_lock<std::mutex> get_lock() {
  extern std::mutex some_mutex;
  std::unique_lock<std::mutex> lk(some_mutex);
  prepare_data();
  return lk;
} 

void process_data() {
  std::unique_lock<std::mutex> lk(get_lock());
  do_something(); 
}

对unique_lock的对象来说,一个对象只能和一个mutex锁唯一对应,不能存在一对多或者多对一的情况,不然会造成死锁的出现

所以如果想要传递两个unique_lock对象对mutex的权限,需要运用到移动语义或者移动构造函数

注意,unique_locklock_guard都不能复制,lock_guard不能移动,但是unique_lock可以

// unique_lock 可以移动,不能复制
std::unique_lock<std::mutex> guard1(_mu);
std::unique_lock<std::mutex> guard2 = guard1;  // error
std::unique_lock<std::mutex> guard2 = std::move(guard1); // ok

// lock_guard 不能移动,不能复制
std::lock_guard<std::mutex> guard1(_mu);
std::lock_guard<std::mutex> guard2 = guard1;  // error
std::lock_guard<std::mutex> guard2 = std::move(guard1); // error

锁的粒度

锁的粒度是一个摆手术语(hand-waving term),用来描述一个锁保护着的数据量大小

一个细粒度锁(a fine-grained lock)能够保护较小的数据量

一个粗粒度锁(a coarse-grained lock)能够保护较多的数据量

简而言之,我们应当针对待保护数据量的大小选择合适的粒度,太大则过犹不及,太小则不足以保护

互斥锁保证了线程间的同步,但是却将并行操作变成了串行操作,这对性能有很大的影响,所以我们要尽可能的减小锁定的区域,也就是使用细粒度锁

这一点lock_guard做的不好,不够灵活,lock_guard只能保证在析构的时候执行解锁操作,lock_guard本身并没有提供加锁和解锁的接口,但是有些时候会有这种需求

class LogFile {
    std::mutex _mu;
    ofstream f;
public:
    LogFile() {
        f.open("log.txt");
    }
    ~LogFile() {
        f.close();
    }
    void shared_print(string msg, int id) {
        {
            std::lock_guard<std::mutex> guard(_mu);
            //do something 1
        }
        //do something 2
        {
            std::lock_guard<std::mutex> guard(_mu);
            // do something 3
            f << msg << id << endl;
            cout << msg << id << endl;
        }
    }

};

上面的代码中,一个函数内部有两段代码需要进行保护,这个时候使用lock_guard就需要创建两个局部对象来管理同一个互斥锁(其实也可以只创建一个,但是锁的力度太大,效率不行),修改方法是使用unique_lock。它提供了lock()和unlock()接口,能记录现在处于上锁还是没上锁状态,在析构的时候,会根据当前状态来决定是否要进行解锁(lock_guard就一定会解锁)

class LogFile {
    std::mutex _mu;
    ofstream f;
public:
    LogFile() {
        f.open("log.txt");
    }
    ~LogFile() {
        f.close();
    }
    void shared_print(string msg, int id) {

        std::unique_lock<std::mutex> guard(_mu);
        //do something 1
        guard.unlock(); //临时解锁

        //do something 2

        guard.lock(); //继续上锁
        // do something 3
        f << msg << id << endl;
        cout << msg << id << endl;
        // 结束时析构guard会临时解锁
        // 这句话可要可不要,不写,析构的时候也会自动执行
        // guard.ulock();
    }

上面的代码可以看到,在无需加锁的操作时,可以先临时释放锁,然后需要继续保护的时候,可以继续上锁,这样就无需重复的实例化lock_guard对象,还能减少锁的区域。同样,可以使用std::defer_lock设置初始化的时候不进行默认的上锁操作

void shared_print(string msg, int id) 
{
    std::unique_lock<std::mutex> guard(_mu, std::defer_lock);
    //do something 1

    guard.lock();
    // do something protected
    guard.unlock(); //临时解锁

    //do something 2

    guard.lock(); //继续上锁
    // do something 3
    f << msg << id << endl;
    cout << msg << id << endl;
    // 结束时析构guard会临时解锁
}

这样使用起来就比lock_guard更加灵活!然后这也是有代价的,因为它内部需要维护锁的状态,所以效率要比lock_guard低一点,在lock_guard能解决问题的时候,就是用lock_guard,反之,使用unique_lock

后面在学习条件变量的时候,还会有unique_lock的用武之地

你可能感兴趣的:(编程,c++,开发语言)