-
Notifications
You must be signed in to change notification settings - Fork 0
Home
易用、异步无锁且线程安全、零拷贝是YY.Base
所追求的目标。很多很多基建都是围绕着这个目标来设计的。我们会从最基础的线程和任务开始,逐步深入到更复杂的内容。
温馨提示:YY.Base借鉴了一些其他库的设计,比如Chrome、QT、.NET等。我可能不生产水,更多的是水的搬运工。
这是一个非常大的话题。但是它们之间确实有点点滴滴的联系。
代码异步后有可能引入其他线程(注意:并非所有异步都会引入线程)。
一旦引入其他线程这样就会设计到线程安全的问题。线程安全的代码需要考虑多个线程同时访问同一资源时的情况,确保不会出现数据竞争和不一致的状态。
人们可能会使用锁来解决这种数据竞争的问题。但是锁有会引入同步,导致线程阻塞或者锁使用不当最终任然产生问题。
很多时候我们可能非常喜欢同步。但是同步确实会带来很多问题,比如同步容易往往容易导致无意义的等待,不利于系统资源的最大化利用。举个简单直接的例子。
假设某个产品逻辑需要100毫秒后执行,某人代码如下:
void Test()
{
Sleep(100); // 线程阻塞100毫秒
// 执行某个产品逻辑
}
有逻辑毛病吗?好像逻辑没有毛病。但是你觉得这个合理吗?为什么这里需要阻塞100毫秒?我们可以安排一个定时器,100毫秒后执行某个产品逻辑。这样就可以避免当前线程阻塞。在YY.Base中我们大致可以这样写:
#include <YY/Base/Threading/TaskRunner.h>
void Test()
{
// 向当前TaskRunner安排一个延迟100毫秒后执行的任务
YY::TaskRunner::GetCurrent()->PostDelayTask(
YY::TimeSpan<YY::TimePrecise::Millisecond>::FromMilliseconds(100),
[]()
{
// 执行某个产品逻辑
});
}
无锁其实也是一个很大的话题。有微观角度与宏观角度。这里我们只说明宏观角度,大致意思就是从工程技术去解决锁的使用。加锁是方法,避免竞争才是目的。除非不得不使用锁,否则就避免避免使用锁!
比如现在有一个需求,需要根据一个用户输入的关键字去搜索用户安装的应用。获取用户安装的应用列表是一个耗时操作。但是产品希望用户输入关键字后,能尽快看到搜索结果。
我们可能会这样想,引入一个缓存,用户缓存应用列表。这样用户输入关键字后,我们可以先从缓存中获取匹配的应用。如果用户安装或者卸载了应用,我们可以异步更新应用列表缓存。大致代码如下:
// 1. 由于内容比较简单,UI线程调用 GetApps方法获取匹配的应用
// - 为了方式oLastAppCache数据竞争,我们使用了一个锁来保护oLastAppCache
// 2. 当用户安装或卸载应用后调用AsyncUpdateAppCache,异步更新应用列表缓存
class AppMgr
{
private:
Lock oLastAppCacheLock; // 锁用于保护缓存
std::vector<App> oLastAppCache;
public:
std::vector<App> GetApps(const wchar_t* _szKey)
{
oLastAppCacheLock.Lock();
std::vector<App> oResult;
for (const auto& app : oLastAppCache)
{
if (app.Name.find(_szKey) != std::wstring::npos)
{
oResult.push_back(app);
}
}
oLastAppCacheLock.Unlock();
return oResult;
}
// 异步更新应用列表缓存,当用户安装或卸载应用后调用
void AsyncUpdateAppCache()
{
_beginthread(
[](void* _pParam) -> void
{
auto _pAppMgr = _(AppMgr*)_pParam;
_pAppMgr->UpdateAppCache(GetInstalledApps());
return;
},
0,
this);
}
private:
// 获取用户安装的应用列表
static std::vector<App> GetInstalledApps();
// 更新应用列表缓存
void UpdateAppCache(std::vector<App> _oApps)
{
// 为了方式oLastAppCache数据竞争,我们使用了加锁来保护oLastAppCache
oLastAppCacheLock.Lock();
oLastAppCache = std::move(_oApps);
oLastAppCacheLock.Unlock();
}
};
我们发现目前这个需求加锁的目的是为了保护oLastAppCache不被多个线程同时访问。我们其实可以异步拿到结果后通知(PostTask)UI线程主动更新oLastAppCache。大致如下:
// 1. 由于内容比较简单,UI线程调用 GetApps方法获取匹配的应用
// 2. 当用户安装或卸载应用后调用AsyncUpdateAppCache,异步更新应用列表缓存
// - 为了避免阻塞调用者,我们安排到异步TaskRunner中获取新的应用列表
class AppMgr
{
private:
std::vector<App> oLastAppCache;
YY::RefPtr<YY::SequencedTaskRunner> _pAppMgrAsyncTaskRunner = YY::SequencedTaskRunner::Create(L"AppMgr异步线程");
public:
// 注意:只能UI线程调用此方法
std::vector<App> GetApps(const wchar_t* _szKey)
{
std::vector<App> oResult;
for (const auto& app : oLastAppCache)
{
if (app.Name.find(_szKey) != std::wstring::npos)
{
oResult.push_back(app);
}
}
return oResult;
}
// 异步更新应用列表缓存,当用户安装或卸载应用后调用
void AsyncUpdateAppCache()
{
_pAppMgrAsyncTaskRunner->PostTask(
[this]()
{
// _pAppMgrAsyncTaskRunner中调用 GetInstalledApps()
// 然后在UI线程中调用UpdateAppCache,更新缓存
if(auto _pUiTaskRunner = GetUiTaskRunner())
{
_pUiTaskRunner->PostTask(std::bind(&AppMgr::UpdateAppCache, this, GetInstalledApps());
}
});
}
private:
// 获取用户安装的应用列表
static std::vector<App> GetInstalledApps();
// 更新应用列表缓存
void UpdateAppCache(std::vector<App> _oApps)
{
oLastAppCache = std::move(_oApps);
}
};
题外话:如果
GetUiTaskRunner()->PostTask(std::bind(&AppMgr::UpdateAppCache, this, GetInstalledApps());
这里AppMgr已经被销毁了,会发生什么?我们应该如何避免这种情况?
刚才的无锁实现虽然避免了数据竞争,解决了锁的使用。但是如果GetUiTaskRunner()->PostTask(std::bind(&AppMgr::UpdateAppCache, this, GetInstalledApps());
这里AppMgr已经被销毁了,这可能会导致访问已释放的内存,也许下一刻我们的程序就会崩溃。
我们可以使用YY::ObserverPtr
(或者类似的weak_ptr的手段)来管理内存,确保在任务执行时AppMgr对象仍然有效。这样就可以避免访问已释放的内存。
YY::ObserverPtr是一个轻量级的观察者指针,它不会增加引用计数,适用于需要观察对象生命周期但不需要拥有对象的场景。如果对象已经释放,则它将返回空指针。它可以帮助我们避免悬空指针等问题。
改造后的代码大致如下:
// 1. 由于内容比较简单,UI线程调用 GetApps方法获取匹配的应用
// 2. 当用户安装或卸载应用后调用AsyncUpdateAppCache,异步更新应用列表缓存
// - 为了避免阻塞调用者,我们安排到异步TaskRunner中获取新的应用列表
// 3. 为了防止AppMgr销毁后TaskRunner继续访问AppMgr,我们使用ObserverPtr来管理AppMgr的生命周期。
#include <YY/Base/Memory/ObserverPtr.h>
#include <YY/Base/Functional/Bind.h>
class AppMgr : public YY::ObserverPtrFactory
{
private:
std::vector<App> oLastAppCache;
YY::RefPtr<YY::SequencedTaskRunner> _pAppMgrAsyncTaskRunner = YY::SequencedTaskRunner::Create(L"AppMgr异步线程");
public:
// 注意:只能UI线程调用此方法
std::vector<App> GetApps(const wchar_t* _szKey)
{
std::vector<App> oResult;
for (const auto& app : oLastAppCache)
{
if (app.Name.find(_szKey) != std::wstring::npos)
{
oResult.push_back(app);
}
}
return oResult;
}
// 异步更新应用列表缓存,当用户安装或卸载应用后调用
void AsyncUpdateAppCache()
{
// 使用ObserverPtr来观察AppMgr的生命周期
YY::ObserverPtr<AppMgr> _pThis = this;
_pAppMgrAsyncTaskRunner->PostTask(
[_pThis]()
{
// _pAppMgrAsyncTaskRunner中调用 GetInstalledApps()
// 然后在UI线程中调用UpdateAppCache,更新缓存
if(auto _pUiTaskRunner = GetUiTaskRunner())
{
// 注意需要使用YY::Bind来绑定YY::ObserverPtr,这样AppMgr释放后,该任务自动取消,不会导致野指针使用
_pUiTaskRunner->PostTask(YY::Bind(&AppMgr::UpdateAppCache, _pThis, GetInstalledApps());
}
});
}
private:
// 获取用户安装的应用列表
static std::vector<App> GetInstalledApps();
// 更新应用列表缓存
void UpdateAppCache(std::vector<App> _oApps)
{
oLastAppCache = std::move(_oApps);
}
};
内存复制往往会带来明显的性能损耗(以前测试过一个简单的扫描引擎,其中至少5%的额外耗时是非必须的内存复制导致的),尤其是当数据量很大时。我们可以通过一些技术手段来减少内存复制的次数。YY.Base也提供了一些零拷贝辅助类库:
- COW 形式的字符串(uString)以及动态数组(Array)
- 借助COW(Copy-On-Write)技术,只有在需要修改数据时才会进行实际的内存复制。这样可以减少不必要的内存复制,提高性能。大家可以按心的传递uString、Array等数据类型,而不必过多担心额外的内存复制。
- View类(StringView/ArrayView)
- 借助StringView等,可以轻松截取字符串等。而没有任何内存复制开销。比如对字符串进行截取、查找、分割等操作时,使用StringView可以避免不必要的内存复制。