锁的归属权转移也是一个值得研究的事情,比如我们处理一个数据,需要在不同函数中顺序处理,这是非常正常的事情,毕竟一个函数的功能有限。
但在多线程中,有这么个问题,每个函数都是线程安全的,但是一旦组合,就不是线程安全的。为了整体的线程安全,需要不停的将锁的所有权转移给下一个承接的函数。
同时控制锁的颗粒度,对于性能提升及其有意义,可防止因锁操作问题,导致多线程效率不如单线程效率。
std::unique_lock 类是一种可移动但不可复制的锁,其最基本的使用,加锁,和 std::lock_guard 类完全相同。
但正如标题所言,std::unique_lock 可以转移所有权,也就是从一个函数传给另一个函数,如同其他所有权类一样,它可以移动,不可复制。
#include
#include
struct someBigObject
{
someBigObject(int aa, int bb)
: a(aa)
, b(bb)
{}
int a = 0;
int b = 0;
};
void swap(someBigObject &lhs, someBigObject &rhs)
{
std::swap(lhs.a, rhs.a);
std::swap(lhs.b, rhs.b);
}
struct X
{
explicit X(someBigObject &sd)
: someDetail(sd)
{}
friend void swap(X &lhs, X &rhs)
{
if (&lhs == &rhs)
{
return;
}
//利用RAII自动解锁
//因为std::unique_lock类具有成员函数lock()、try_lock()和unlock(),
//所以它的实例得以传给std::lock()函数
// defer_lock: 延迟上锁
// unique_lock: 可移动不可复制的锁
std::unique_lock<std::mutex> lockA(lhs.m, std::defer_lock);
std::unique_lock<std::mutex> lockB(rhs.m, std::defer_lock);
std::lock(lockA, lockB);
//以上代码同如下三行:
// adopt_lock: 采用锁
// std::lock(lhs.m, rhs.m);
// std::lock_guard lock_a(lhs.m, std::adopt_lock);
// std::lock_guard lock_b(rhs.m, std::adopt_lock);
//以上代码同如下一行:
// scoped_lock: 范围锁
// std::scoped_lock guard(lhs.m, rhs.m);
swap(lhs.someDetail, rhs.someDetail);
}
private:
someBigObject someDetail;
std::mutex m;
};
auto main() -> int
{
someBigObject testA(3, 2);
someBigObject testB(8, 0);
X testXa(testA);
X testXb(testB);
swap(testXa, testXb);
return 0;
}
对于 std::unique_lock 类的对象,可以通过函数返回值进行转移,也可以通过 std::move() 进行显示转移。
通过转移锁函数,将处理数据的函数进行封装,然后不停的转移锁,可以一直保证并发安全,即线程的数据安全。
#include
#include
#include
void prepareData()
{
std::cout << "prepareData" << std::endl;
}
void doSomething()
{
std::cout << "doSomething" << std::endl;
}
auto getLock() -> std::unique_lock<std::mutex>
{
extern std::mutex someMutex;
std::unique_lock<std::mutex> lk(someMutex);
prepareData();
return lk;
}
void processData()
{
std::unique_lock<std::mutex> lk(getLock());
// guard: 警卫
// std::lock_guard不可转移
doSomething();
}
auto main() -> int
{
processData();
return 0;
}
std::mutex someMutex;
加锁可以保护多线程并发操作数据的安全,然而也必然影响程序的效率,严谨的程序员需要做到,只在必要的地方加锁,对加锁的边界保持敏感。
以下代码简单模拟了一种处理过程,在获取数据时加锁,处理数据时解锁,写入结果时加锁,精细控制锁的边界。
struct someClass
{
someClass() = default;
private:
int a = 0;
};
auto getNextDataChunk() -> someClass &
{
static someClass a;
return a;
}
struct resultType
{
explicit resultType(someClass aa)
: a(aa)
{}
private:
someClass a;
};
auto process(someClass a) -> resultType &
{
static resultType b(a);
return b;
}
void writeResult(someClass & /*unused*/, resultType & /*unused*/)
{
std::cout << "write someClass and resultType" << std::endl;
}
void getAndProcessData()
{
extern std::mutex theMutex;
std::unique_lock<std::mutex> myLock(theMutex);
someClass dataToProcess = getNextDataChunk();
myLock.unlock();
resultType result = process(dataToProcess);
myLock.lock();
writeResult(dataToProcess, result);
}
auto main() -> int
{
getAndProcessData();
return 0;
}
std::mutex theMutex;
控制锁的颗粒度有时在不经意间,会引起语义的错误,比如以下类对象的 == 比较。
此时的语义就不是真正的同一时刻两个对象值相等,而是某个时刻的A和另一时刻的B是否相等。
这是控制锁的颗粒度时,考虑不周导致的逻辑错误,需要小心防范。
struct Y
{
explicit Y(int sd)
: someDetail(sd)
{}
friend auto operator==(const Y &lhs, const Y &rhs) -> bool
{
if (&lhs == &rhs)
{
return true;
}
const int lhsValue = lhs.getDetail();
const int rhsValue = rhs.getDetail();
return rhsValue == lhsValue;
}
private:
int someDetail = 0;
mutable std::mutex m;
auto getDetail() const -> int
{
std::lock_guard<std::mutex> lockA(m);
return someDetail;
}
};
通过锁的所有权转移,延续线程的函数对数据的控制,起到保护作用,使得并发安全。
通过控制锁的颗粒度,防止性能的下降,但要注意,不要因为过细的颗粒度导致逻辑错误,引起线程并发不安全。