chrome源码中WeakPtr 跨线程使用详解:原理、风险与最佳实践

base::WeakPtr 在 Chromium 中 不能安全地跨线程使用。这是一个很关键的点,下面详细解释原因及正确用法。

🔍原理与使用

✅ 先说答案:

  • base::WeakPtr 本质上是**线程绑定(thread-affine)**的。
  • 不能在多个线程之间创建或使用它,特别是不能在一个线程创建 WeakPtr,然后在另一个线程中使用它。
  • 如果你非要跨线程判断对象是否有效,应使用更底层或线程安全的机制(例如 RefCountedThreadSafe + scoped_refptr)。

📌 官方说明

Chromium 的 base::WeakPtr 文档中有明确说明(可在源码注释中看到):

WeakPtr is not thread-safe. It must be used and dereferenced on the same thread (or Sequence) on which it was obtained from a WeakPtrFactory.

这段代码是chrome源码里面的WeakPtr实现:

WeakPtr<T> GetWeakPtr() { return WeakPtr<T>(weak_reference_owner_.GetRef(), reinterpret_cast<T*>(ptr_)); }

是否线程安全?答案是:不是线程安全的 ,除非你明确知道 weak_reference_owner_ 是线程安全的,而且 ptr_ 的生命周期也受线程安全保护(比如引用计数等)。


🔍 逐行分析:

1. weak_reference_owner_.GetRef()

这看起来是一个类似 base::internal::WeakReferenceOwner 或你自定义的 WeakReferenceOwner 类型。Chromium 中的 WeakReferenceOwner 默认是 线程绑定的(Thread-affine),只能在创建它的线程中使用(否则在 Debug 模式下会崩)。

✅ 所以:GetRef() 调用必须在原始线程中,不是线程安全的。


2. reinterpret_cast<T*>(ptr_)

这个操作没做线程安全保护的话,也是不安全的,尤其是如果 ptr_ 不是线程安全引用类型(如 scoped_refptr<T>)。


✅ 正确的 WeakPtr 创建姿势(来自 Chromium)

Chromium 推荐用 base::WeakPtrFactory<T>,并且只能在绑定线程上使用它:

class MyClass { public: base::WeakPtr<MyClass> GetWeakPtr() { return weak_factory_.GetWeakPtr(); // ✅ 安全:只能在同一个线程上用 } private: base::WeakPtrFactory<MyClass> weak_factory_{this}; };

  • WeakPtrFactory<T> 本身有一个 SequenceChecker,确保你只能在创建线程中用 GetWeakPtr()

  • 即使你多线程中有多个任务,都只能在同一个线程中访问和使用这个 WeakPtrFactory


❌ 错误使用示例

如果你把 GetWeakPtr() 这个函数暴露给了多个线程,并且多个线程中调用了它,比如这样:

thread1: my_object->GetWeakPtr(); thread2: my_object->GetWeakPtr();

即便 GetWeakPtr() 本身没有 crash,也会存在竞争风险或触发 DCHECK。


✅ 如果你真的想跨线程构建 WeakPtr...

你可以:

  1. 在初始化线程上调用 GetWeakPtr()

  2. 然后把这个 WeakPtr 通过 PostTask 发送到其他线程使用(读取、检查是否有效)。

  3. 不能在其他线程上继续调用 GetWeakPtr()


✅ 总结表格

行为 是否线程安全 建议
同一线程调用 GetWeakPtr() ✅ 安全 ✅ 推荐
不同线程调用 GetWeakPtr() ❌ 不安全 ❌ 禁止
一个线程创建 WeakPtr,另一个线程只用它 ⚠️ 有条件安全 ⚠️ 小心使用
自己实现 GetWeakPtr() 如你代码中那样 ❌ 不建议 ❌ 风险高

如你有多线程需求,建议考虑:

  • 使用 scoped_refptr<T>(跨线程完全安全);

  • 或将 WeakPtrFactory 迁移到后台线程中,并保证所有访问都在同一个线程;

  • 或用 CrossThreadWeakPtr 模式(Chromium中个别模块使用,较复杂)。

这段代码的作用是 在回调或异步任务执行时,确保 weak_self 所指向的对象仍然有效(未被销毁) ,以防止访问已经释放的对象从而导致 野指针或崩溃


🔍 代码解析:

if (!weak_self || weak_self.WasInvalidated()) { return; }

这是一个典型的 base::WeakPtr<T> 使用场景,常用于异步任务或跨线程操作中。

✅ 判断目的:
  1. !weak_self

    • 检查 weak_self 是否为空(即未绑定任何对象)。

    • 这相当于检查 WeakPtr 是否有效初始化。

  2. weak_self.WasInvalidated()

    • 判断该 WeakPtr 是否已经 失效(即指向的原始对象已经销毁)

    • 即使指针本身不为空,如果 T 已被析构,也不能再访问。


🛡️ 为什么这么写?

异步/延迟任务常见问题:

举个例子:

// 假设这是某个类中的成员函数 PostTask(FROM_HERE, base::BindOnce(&MyClass::DoWork, weak_factory_.GetWeakPtr()));

  • 如果 MyClass 在任务执行前被析构,那么 DoWork() 中的 this 就是悬空指针。

  • WeakPtr 就是防止"任务晚于对象生命周期"这种典型 bug。

  • 所以在 DoWork() 内部首先要判断 weak_self 是否有效。


🧠 小结:

if (!weak_self || weak_self.WasInvalidated()) { return; // 安全退出,防止访问已销毁对象 }

这个判断非常关键,用于:

  • 保证异步回调安全性;

  • 防止 use-after-free;

  • 是 C++ 客户端编程(尤其是 Chromium 项目)中的推荐模式。

🛡️ 工程案例:

cookie_manager->SetCanonicalCookie(*canonical_cookie, cookie_info.url, options, base::BindOnce(&InProcessImporterBridge::SetCanonicalCookieCallback, weak_factory_.GetWeakPtr())); 这个代码在ui线程,SetCanonicalCookieCallback这个回调要是在其他线程调用会有什么问题\

在 Chromium 的线程模型中,SetCanonicalCookieCallback 回调若在其他线程(非 UI 线程)调用可能导致严重问题。以下是详细分析:


问题根源

  1. 线程安全性违例

    • InProcessImporterBridge 类及其成员变量通常非线程安全,假设仅在 UI 线程访问。

    • 若回调在其他线程(如 IO 线程)执行,直接访问成员变量会导致数据竞争(Data Race)。

  2. weak_factory_.GetWeakPtr() 的线程安全性

    • WeakPtrFactory 本身线程安全(GetWeakPtr() 可跨线程调用),但不保证回调执行时的对象存活

    • 若 UI 线程已销毁 InProcessImporterBridge,其他线程调用回调会访问无效内存。

  3. UI 线程依赖的操作

    • 回调中若操作 UI 相关资源(如更新界面、访问 Profile),必须在 UI 线程执行。

具体风险场景

复制代码
// 错误示例:回调可能在其他线程执行
cookie_manager->SetCanonicalCookie(
    *canonical_cookie, 
    cookie_info.url, 
    options,
    base::BindOnce(&InProcessImporterBridge::SetCanonicalCookieCallback,
                  weak_factory_.GetWeakPtr()));  // 若回调在非UI线程运行
  • 崩溃风险

    • 其他线程调用时,若 InProcessImporterBridge 已销毁,weak_factory_ 返回 nullptr,回调被丢弃(安全)。

    • 但若对象存活,回调直接访问成员变量(如 std::vectorstd::map)会触发未定义行为。

  • 逻辑错误

    • 非 UI 线程修改 UI 相关状态(如界面标志位),导致界面显示异常。

解决方案

1. 强制回调到 UI 线程

使用 base::BindPostTask 将回调派发到 UI 线程的任务队列:

复制代码
cookie_manager->SetCanonicalCookie(
    *canonical_cookie,
    cookie_info.url,
    options,
    base::BindOnce(
        base::BindPostTask(
            base::SequencedTaskRunner::GetCurrentDefault(),  // 获取UI线程TaskRunner
            base::BindOnce(&InProcessImporterBridge::SetCanonicalCookieCallback,
                         weak_factory_.GetWeakPtr())
        )
    )
);
2. 回调中显式检查线程

在回调内部验证是否运行在 UI 线程:

复制代码
void InProcessImporterBridge::SetCanonicalCookieCallback(...) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // 安全操作成员变量
}
3. 使用 ThreadSafeRefCounted(若适合)

InProcessImporterBridge 需跨线程共享,改为继承 base::RefCountedThreadSafe,但需确保所有成员访问均线程安全(通常不推荐用于 UI 相关类)。


为什么 WeakPtr 不能完全解决问题?

  • WeakPtr 仅保证对象销毁后不执行回调 ,但若对象存活:

    • 不保证回调线程安全。

    • 不自动切换线程上下文。

  • 必须配合 BindPostTask 或线程检查使用。


Chromium 的线程模型要求

  1. UI 线程专属对象

    • ProfileWebContents、UI 控件等,必须通过 UI 线程访问。
  2. 线程跳转代价

    • 跨线程任务提交(如 PostTask)有开销,但比崩溃/数据竞争更可接受。

正确代码示例

复制代码
// 方法1:使用 BindPostTask(推荐)
cookie_manager->SetCanonicalCookie(
    *canonical_cookie,
    cookie_info.url,
    options,
    base::BindPostTask(
        base::SequencedTaskRunner::GetCurrentDefault(),
        base::BindOnce(&InProcessImporterBridge::SetCanonicalCookieCallback,
                      weak_factory_.GetWeakPtr())
    )
);

// 方法2:回调内手动跳转(备用)
class InProcessImporterBridge {
 public:
  void SetCanonicalCookieCallback(...) {
    if (!base::SequencedTaskRunner::GetCurrentDefault()->RunsTasksInCurrentSequence()) {
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(&InProcessImporterBridge::SetCanonicalCookieCallback,
                        weak_factory_.GetWeakPtr(), ...));
      return;
    }
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    // 实际处理逻辑
  }
};

总结

问题 风险等级 解决方案
非UI线程访问成员变量 高(崩溃) BindPostTask 跳转回 UI 线程
WeakPtr 对象已销毁 低(安全) 自动跳过回调
未同步的 UI 状态修改 中(逻辑错误) 添加 DCHECK_CALLED_ON_VALID_SEQUENCE

最佳实践

  • 默认假设回调可能在任何线程执行。

  • 对 UI 相关对象,始终通过 BindPostTaskPostTask 确保线程安全。

下面是个跨线程场景使用的一个完整例子

cpp 复制代码
class QQBrowserImporter : public Importer {
public:
  QQBrowserImporter();
  explicit QQBrowserImporter(bool first_run);

  void StartImport(const importer::SourceProfile& source_profile,
                   uint16_t items,
                   ImporterBridge* bridge) override;

  static void ImportCookies(const std::wstring& localPath, base::WeakPtr<QQBrowserImporter> client);
  
protected:
  friend class base::RefCountedThreadSafe<QQBrowserImporter>;
  ~QQBrowserImporter() override;

private:
  scoped_refptr<base::SequencedTaskRunner> GetdbTaskRunner();
  bool ScheduleTask(const base::RepeatingClosure& task);

  scoped_refptr<base::SequencedTaskRunner> db_thread_runner_;
  base::WeakPtrFactory<QQBrowserImporter> weak_factory_;
  DISALLOW_COPY_AND_ASSIGN(QQBrowserImporter);
};


QQBrowserImporter::QQBrowserImporter(bool first_run) : first_run_(first_run), weak_factory_(this) {}

QQBrowserImporter::~QQBrowserImporter(void){}

void QQBrowserImporter::StartImport(const importer::SourceProfile& source_profile,
                                    uint16_t items,
                                    ImporterBridge* bridge) {
  bridge_ = bridge;
  bridge_->NotifyStarted();
  std::wstring source_path = source_profile.source_path.value();

  if ((items & importer::COOKIES) && !cancelled()) {
    ScheduleTask(base::BindRepeating(&QQBrowserImporter::ImportCookies, source_path, weak_factory_.GetWeakPtr()));
  }
}

// TODO extract to base class
void QQBrowserImporter::ImportCookies(const std::wstring& localPath, base::WeakPtr<QQBrowserImporter> client) {
  std::wstring local_data_path = localPath;
  std::string desc;
  local_data_path += L"\\Default\\Network\\Cookies";
  base::FilePath qq_login_path(local_data_path);

  sql::Database db;
  if (!db.Open(qq_login_path)) {
    content::GetUIThreadTaskRunner({})->PostTask(
    FROM_HERE,
    base::BindOnce([](base::WeakPtr<QQBrowserImporter> weak_self) {
      if (!weak_self || weak_self.WasInvalidated()) {
        return;
      }

      if (weak_self->bridge_) {
        base::Value::Dict import_res;
        import_res.Set("successcount", 0);
        import_res.Set("faileddesc", "Failed to open QQ login data DB");

        std::string value_str;
        base::JSONWriter::Write(import_res, &value_str);
        weak_self->bridge_->NotifyEnded(false, importer::TYPE_COOKIES_QQ, value_str);
      }
    },
    client));
    return;
  }

  const char* kQuery = "SELECT host_key, encrypted_value, name, path, creation_utc, expires_utc, last_access_utc, is_secure, is_httponly, samesite, priority FROM cookies";
  sql::Statement stmt(db.GetUniqueStatement(kQuery));
  if (!stmt.is_valid()) {
    content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
        base::BindOnce([](base::WeakPtr<QQBrowserImporter> weak_self) {
        if (!weak_self || weak_self.WasInvalidated()) {
          return;
        }

        if (weak_self->bridge_) {
          base::Value::Dict import_res;
          import_res.Set("successcount", 0);
          import_res.Set("faileddesc", "Failed to prepare login query statement");

          std::string value_str;
          base::JSONWriter::Write(import_res, &value_str);
          weak_self->bridge_->NotifyEnded(false, importer::TYPE_COOKIES_QQ, value_str);
        }
      },
      client));
    return;
  }
  std::string localPathUtf8 = base::WideToUTF8(localPath);
  auto aesKey = chrome::GetDecryptedKey(localPathUtf8);

  std::vector<importer::CookiesInfo> cookies_info;
  while (stmt.Step()) {
    std::string host_key  = stmt.ColumnString(0);
    std::string name = stmt.ColumnString(2);
    std::string path = stmt.ColumnString(3);
    int64_t creation_utc = stmt.ColumnInt64(4);
    int64_t expires_utc = stmt.ColumnInt64(5);
    int64_t last_access_utc = stmt.ColumnInt64(6);
    
    // compatible with old browser kernels, new kernels will add SameSite=None by default
    int is_http_only = true; // stmt.ColumnInt(8);
    int is_secure = true; // stmt.ColumnInt(7);
    int samesite_val = stmt.ColumnInt(9);
    int priority_val = stmt.ColumnInt(10);

    base::span<const uint8_t> blob_span = stmt.ColumnBlob(1);
    std::vector<unsigned char> encrypted(blob_span.begin(), blob_span.end());

    std::string value;
    if (encrypted.size() > 15 && encrypted[0] == 'v' && encrypted[1] == '1') {
      std::vector<unsigned char> nonce(encrypted.begin() + 3, encrypted.begin() + 15);
      std::vector<unsigned char> ciphertext(encrypted.begin() + 15, encrypted.end() - 16);
      std::vector<unsigned char> tag(encrypted.end() - 16, encrypted.end());

      auto decrypted = chrome::AESGCMDecrypt(aesKey, nonce, ciphertext, tag);
      value = std::string(decrypted.begin(), decrypted.end());
    }
    else {
      continue;
    }

    std::string cookie_str = FormatCookieString(
      name, value, host_key, path, expires_utc, is_secure == 0 ? false : true, is_http_only == 0 ? false : true, samesite_val
    );

    std::string scheme = is_secure ? "https://" : "http://";
    GURL url(scheme + ConvertHostKeyToDomain(host_key) + path);
    cookies_info.emplace_back(std::move(cookie_str), std::move(url), is_http_only);
  }

  if (cookies_info.empty()) {
    content::GetUIThreadTaskRunner({})->PostTask(
    FROM_HERE,
    base::BindOnce([](base::WeakPtr<QQBrowserImporter> weak_self) {
      if (!weak_self || weak_self.WasInvalidated()) {
        return;
      }

      if (weak_self->bridge_) {
        base::Value::Dict import_res;
        import_res.Set("successcount", 0);
        import_res.Set("faileddesc", se_import_user_info::kEmptyData);

        std::string value_str;
        base::JSONWriter::Write(import_res, &value_str);
        weak_self->bridge_->NotifyEnded(false, importer::TYPE_COOKIES_QQ, value_str);
      }
    },
    client));
    return;
  }

  auto task = base::BindOnce([](base::WeakPtr<QQBrowserImporter> weak_self, std::vector<importer::CookiesInfo> cookies_info) {
    if (!weak_self || weak_self.WasInvalidated()) {
      return;
    }

    if (weak_self->bridge_) {
      weak_self->bridge_->SetCookie(cookies_info, se_import_user_info::BrowserType::kQQ, importer::TYPE_COOKIES_QQ);
    }
    }, client, cookies_info);

  content::GetUIThreadTaskRunner({})->PostTask(FROM_HERE, std::move(task));
}

scoped_refptr<base::SequencedTaskRunner> QQBrowserImporter::GetdbTaskRunner() {
  if (!db_thread_runner_) {
    db_thread_runner_ = base::ThreadPool::CreateSingleThreadTaskRunner(
        {base::TaskPriority::HIGHEST, base::MayBlock()});
  }
  return db_thread_runner_;
}

bool QQBrowserImporter::ScheduleTask(const base::RepeatingClosure& task) {
  scoped_refptr<base::SequencedTaskRunner> task_runner(GetdbTaskRunner());
  if (task_runner.get())
    return task_runner->PostTask(FROM_HERE, task);
  return false;
}
相关推荐
csdn_aspnet2 分钟前
C++ 扇形的面积(Area of a Circular Sector)
开发语言·c++
weixin_428498491 小时前
NVC++ 介绍与使用指南
c++·cuda
Susea&1 小时前
趣味编程:抽象图(椭圆组成)
c语言·c++·技术美术·easyx绘图
xun_xin6662 小时前
C++ for QWidget:正则表达式和QRegExp
c++
一匹电信狗3 小时前
【Linux我做主】探秘进程与fork
linux·运维·服务器·c++·ubuntu·小程序·unix
云梦谭4 小时前
chrome 多关键字搜索插件
chrome
张彦峰ZYF4 小时前
一键启动多个 Chrome 实例并自动清理的 Bash 脚本分享!
前端·chrome·bash
虾球xz4 小时前
游戏引擎学习第302天:使用精灵边界进行排序
c++·学习·算法·游戏引擎
虾球xz4 小时前
游戏引擎学习第297天:将实体分离到Z层中
c++·人工智能·学习·游戏引擎