• Pros & cons: simple & possibly inefficient

Low-Level Lock API#

常用的low-level锁的API有:

  • Lock.acquire(): 阻塞,直到获取到锁
  • Lock.try_acquire(): 返回锁是否已经被占用了,如果是,返回false,否,占用锁并返回true。不阻塞
  • Lock.release(): 释放锁
L.acquire();
r1 = X;
X = r1 + 1;
L.release();

L.acquire();
r2 = X;
X = r2 + 1;
L.release();

但是,这些API给用户在使用时造成了很多挑战(心智负担):

  • Relating lock and resource: 用户只有在拿到锁时才能访问被保护的变量;
  • Matching acquire/release: 用户只能释放已经拿到的锁。

如果锁没有得到正确的处理,会造成很多潜在的问题,并且,这些问题很难发现。因此,在并发编程时,low-level的锁API存在如下问题:

  • High cost:程序员需要始终关注API的使用;
  • Potential bugs:不正确的使用容易造成很多潜在的bugs。

High-Level Lock API#

  • 想要一个易用地,始终能保证安全地high-level API。
    • Acquire/release自动匹配;
    • Lock和resource显式关联。

C++中,这种API被称作RAIIResource Acquisition Is Initialization

#include <string>
#include <mutex>
#include <iostream>
#include <fstream>
#include <stdexcept>

void write_to_file(const std::string & message)
{
    // 创建关于文件的互斥锁
    static std::mutex mutex;

    // 在访问文件前进行加锁
    std::lock_guard<std::mutex> lock(mutex);

    // 尝试打开文件
    std::ofstream file("example.txt");
    if (!file.is_open())
        throw std::runtime_error("unable to open file");

    // 输出文件内容
    file << message << std::endl;

    // 当离开作用域时,文件句柄会被首先析构 (不管是否抛出了异常)
    // 互斥锁也会被析构 (同样地,不管是否抛出了异常)
}

RAII要求,资源的有效期与持有资源的对象的生命期严格绑定,即由对象的构造函数完成资源的分配(获取),同时由析构函数完成资源的释放。在这种要求下,只要对象能正确地析构,就不会出现资源泄露问题。

RAII的主要作用是在不失代码简洁性的同时,可以很好地保证代码的异常安全性。

但是,C++的lock_guard并不能保证代码完全安全!!!

// data: Lock<int>
auto data_guard = data.lock();
auto data_ptr = (int *) &data_guard;
//...
// data_guard is dropped, lock is released
*data_ptr = 666; // unsafe!!!

data_ptr已经超出了data_guard的生命周期,但是仍然可以使用!!!归根究底,因为C++本身的语言特性赋予了程序员太多的自由性,很多代码安全需要程序员自己去保证(心智负担)。

==Solution:Rust基于所有权和生命周期的类型系统。==