C++ 多线程:互斥量(mutex)

2023-10-26

C++ 11中的互斥量,声明在 <mutex> 头文件中,互斥量的使用可以在各种方面,比较常用在对共享数据的读写上,如果有多个线程同时读写一个数据,那么想要保证多线程安全,就必须对共享变量的读写进行保护(上锁),从而保证线程安全。
互斥量主要有四中类型:

  • std::mutex,最基本的 Mutex 类。
  • std::recursive_mutex,递归 Mutex 类。
  • std::time_mutex,限时 Mutex 类。
  • std::recursive_timed_mutex,限时递归 Mutex 类。
    当然C++14和C++17各增加了一个:
  • std::shared_timed_mutex,限时读写锁(C++14)
  • std::shared_mutex,读写锁(C++17)

std::mutex

构造函数

mutex();
mutex(const mutex&) = delete;

从上面的构造函数可以看出,std::mutex不允许拷贝构造,当然也不允许move,最初构造的mutex对象是处于未锁定状态的,若构造不成功会抛出 std::system_error

析构函数

~mutex();

销毁互斥。若互斥被线程占有,或在占有mutex时线程被终止,则会产生未定义行为。

lock

void lock();

锁定互斥,调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:

  • 如果该互斥量当前没有被其他线程锁住,则调用线程将该互斥量锁住,直到调用unlock之前,该线程一直拥有该锁。
  • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住,指导其他线程unlock该互斥量。
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

try_lock

bool try_lock();

尝试锁住互斥量,立即返回。成功获得锁时返回 true ,否则返回 false。
如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况:

  • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
  • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

unlock

void unlock();

解锁互斥。互斥量必须为当前执行线程所锁定(以及调用lock),否则行为未定义。

看下面一个简单的例子实现两个线程竞争全局变量g_num对其进行写操作,然后打印输出:

#include <iostream>
#include <chrono>  // std::chrono
#include <thread>  // std::thread
#include <mutex>  // std::mutex
 
int g_num = 0;  // 为 g_num_mutex 所保护
std::mutex g_num_mutex;
 
void slow_increment(int id) 
{
    for (int i = 0; i < 3; ++i) {
        g_num_mutex.lock();
        ++g_num;
        std::cout << "th" << id << " => " << g_num << '\n';
        g_num_mutex.unlock();
 
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}  
 
int main()
{
    std::thread t1(slow_increment, 0);
    std::thread t2(slow_increment, 1);
    t1.join();
    t2.join();
}

加了互斥量实现有序的写操作并输出:

th0 => 1
th1 => 2
th0 => 3
th1 => 4
th1 => 5
th0 => 6

如果不增加mutex包含,可能输出就不是有序的打印1到6,如下:

thth01 => 2 => 2

th1 => 3
th0 => 4
th0 => 5
th1 => 6

std::recursive_mutex

如上面所说的,如果使用std::mutex,如果一个线程在执行中需要再次获得锁,会出现死锁现象。要避免这种情况下就需要使用递归式互斥量std::recursive_mutex,它不会产生上述的死锁问题,可以理解为同一个线程多次获得锁“仅仅增加锁的计数”,同时,必须要确保unlock和lock的次数相同,其他线程才可能取得这个mutex。它的接口与std::mutex的完全一样,用法也基本相同除了可重入(必须同一线程才可重入,其他线程需等待),看下面的例子:

#include <iostream>
#include <thread>
#include <mutex>
 
class X {
    std::recursive_mutex m;
    std::string shared;
  public:
    void fun1() {
      m.lock();
      shared = "fun1";
      std::cout << "in fun1, shared variable is now " << shared << '\n';
      m.unlock();
    }
    void fun2() {
      m.lock();
      shared = "fun2";
      std::cout << "in fun2, shared variable is now " << shared << '\n';
      fun3(); // 递归锁在此处变得有用
      std::cout << "back in fun2, shared variable is " << shared << '\n';
      m.unlock();
    }
    void fun3() {
      m.lock();
      shared = "fun3";
      std::cout << "in fun3, shared variable is now " << shared << '\n';
      m.unlock();
    }
};
 
int main() 
{
    X x;
    std::thread t1(&X::fun1, &x);
    std::thread t2(&X::fun2, &x);
    t1.join();
    t2.join();
}

在fun2中调用fun3,而fun3中还使用了lock和unlock,只有递归式互斥量才能满足当前情况。

输出如下:

in fun1, shared variable is now fun1
in fun2, shared variable is now fun2
in fun3, shared variable is now fun3
back in fun2, shared variable is fun3

std::time_mutex

timed_mutex增加了带时限的try_lock。即try_lock_fortry_lock_until

try_lock_for
尝试锁互斥。阻塞直到超过指定的 timeout_duration 或得到锁,取决于何者先到来。成功获得锁时返回 true,否则返回false 。函数原型如下:

template< class Rep, class Period >
bool try_lock_for( const std::chrono::duration<Rep,Period>& timeout_duration );

timeout_duration小于或等于timeout_duration.zero(),则函数表现同try_lock()。由于调度或资源争议延迟,此函数可能阻塞长于timeout_duration

#include <iostream>
#include <sstream>
#include <thread>
#include <chrono>
#include <vector>
#include <mutex>

std::timed_mutex mutex;

using namespace std::chrono_literals;

void do_work(int id) {
  std::ostringstream stream;
  for (int i = 0; i < 3; ++i) {
    if (mutex.try_lock_for(100ms)) {
      stream << "success ";
      std::this_thread::sleep_for(100ms);
      mutex.unlock();
    } else {
      stream << "failed ";
    }
    std::this_thread::sleep_for(100ms);
  }

  std::cout << "[" << id << "] " << stream.str() << std::endl;
}

int main() {
  // try_lock_for
  std::vector<std::thread> threads;
  for (int i = 0; i < 4; ++i) {
    threads.emplace_back(do_work, i);
  }

  for (auto& t : threads) {
    t.join();
  }
}

[3] failed success failed 
[0] success failed success 
[2] failed failed failed 
[1] success success success 

try_lock_until
也是尝试锁互斥。阻塞直至抵达指定的timeout_time或得到锁,取决于何者先到来。成功获得锁时返回 true,否则返回false。timeout_time与上面的timeout_duration不一样,timeout_duration表示一段时间,比如1秒,5秒或者10分钟,而timeout_time表示一个时间点,比如说要等到8点30分或10点24分才超时。

使用倾向于timeout_time的时钟,这表示时钟调节有影响。从而阻塞的最大时长可能小于但不会大于在调用时的 timeout_time - Clock::now() ,依赖于调整的方向。由于调度或资源争议延迟,函数亦可能阻塞长于抵达timeout_time之后。同try_lock(),允许此函数虚假地失败并返回false,即使在 timeout_time 前的某点任何线程都不锁定互斥。函数原型如下:

template< class Clock, class Duration >
bool try_lock_until( const std::chrono::time_point<Clock,Duration>& timeout_time);

看下面的例子:

#include <iostream>
#include <sstream>
#include <thread>
#include <chrono>
#include <vector>
#include <mutex>

std::timed_mutex mutex;
using namespace std::chrono;

void do_work() {
    mutex.lock();
    std::cout << "thread 1, sleeping..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(4));
    mutex.unlock();
}
 
void do_work2() {
    auto now = std::chrono::steady_clock::now();
    if (mutex.try_lock_until(now + 5s)) {
        auto end = steady_clock::now();
        std::cout << "try_lock_until success, ";
        std::cout << "time use: " << duration_cast<milliseconds>(end-now).count() 
            << "ms." << std::endl;
        mutex.unlock();
    } else {
        auto end = steady_clock::now();
        std::cout << "try_lock_until failed, ";
        std::cout << "time use: " << duration_cast<milliseconds>(end-now).count() 
            << "ms." << std::endl;
    }
}

int main() {
  // try_lock_until
  std::thread t1(do_work);
  std::thread t2(do_work2);
  t1.join();
  t2.join();
}

获得锁时输出:

thread 1, sleeping...
try_lock_until success, time use: 4000ms.

修改一下,让其超时,输出:

thread 1, sleeping...
try_lock_until failed, time use: 5000ms.

std::recursive_timed_mutex

以类似std::recursive_mutex的方式,recursive_timed_mutex提供排他性递归锁,同线程可以重复获得锁。另外,recursive_timed_mutex通过try_lock_fortry_lock_until方法,提供带时限地获得recursive_timed_mutex锁,类似std::time_mutex

std::shared_mutex

c++ 17 新出的具有独占模式和共享模式的锁。共享模式能够被std::shared_lock(这个后面再详细将)占有。

std::shared_mutex 是读写锁,把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。

它提供两种访问权限的控制:共享性(shared)和排他性(exclusive)。通过lock/try_lock获取排他性访问权限(仅有一个线程能占有互斥),通过lock_shared/try_lock_shared获取共享性访问权限(多个线程能共享同一互斥的所有权)。这样的设置对于区分不同线程的读写操作特别有用。

std::shared_mutex通常用于多个读线程能同时访问同一资源而不导致数据竞争,但只有一个写线程能访问的情形。比如,有多个线程调用shared_mutex.lock_shared(),多个线程都可以获得锁,可以同时读共享数据,如果此时有一个写线程调用 shared_mutex.lock(),则读线程均会等待该写线程调用shared_mutex.unlock()。对于C++11 没有提供读写锁,可使用 boost::shared_mutex

std::shared_mutex新增加的三个接口:

void lock_shared();
bool try_lock_shared();
void unlock_shared();

一个简单例子如下:

#include <iostream>
#include <mutex>  // 对于 std::unique_lock
#include <shared_mutex>
#include <thread>
 
class ThreadSafeCounter {
 public:
  ThreadSafeCounter() = default;
 
  // 多个线程/读者能同时读计数器的值。
  unsigned int get() const {
    std::shared_lock<std::shared_mutex> lock(mutex_);
    return value_;
  }
 
  // 只有一个线程/写者能增加/写线程的值。
  void increment() {
    std::unique_lock<std::shared_mutex> lock(mutex_);
    value_++;
  }
  
  // 只有一个线程/写者能重置/写线程的值。
  void reset() {
    std::unique_lock<std::shared_mutex> lock(mutex_);
    value_ = 0;
  }
 
 private:
  mutable std::shared_mutex mutex_;
  unsigned int value_ = 0;
};
 
int main() {
  ThreadSafeCounter counter;
 
  auto increment_and_print = [&counter]() {
    for (int i = 0; i < 3; i++) {
      counter.increment();
      std::cout << std::this_thread::get_id() << ' ' << counter.get() << '\n';
 
      // 注意:写入 std::cout 实际上也要由另一互斥同步。省略它以保持示例简洁。
    }
  };
 
  std::thread thread1(increment_and_print);
  std::thread thread2(increment_and_print);
 
  thread1.join();
  thread2.join();
}

// 解释:下列输出在单核机器上生成。 thread1 开始时,它首次进入循环并调用 increment() ,
// 随后调用 get() 。然而,在它能打印返回值到 std::cout 前,调度器将 thread1 置于休眠
// 并唤醒 thread2 ,它显然有足够时间一次运行全部三个循环迭代。再回到 thread1 ,它仍在首个
// 循环迭代中,它最终打印其局部的计数器副本的值,即 1 到 std::cout ,再运行剩下二个循环。
// 多核机器上,没有线程被置于休眠,且输出更可能为递增顺序。

可能的输出:

139847802500864 1
139847802500864 2
139847802500864 3
139847794108160 4
139847794108160 5
139847794108160 6

std::shared_timed_mutex

它是从C++14 才提供的限时读写锁:std::shared_timed_mutex
对比std::shared_mutex新增下面两个接口,其实这两个接口与上面讲到的std::timed_mutextry_lock_fortry_lock_until类似。都是限时等待锁。只不过是增加了共享属性。

template< class Rep, class Period >
bool try_lock_shared_for( const std::chrono::duration<Rep,Period>& timeout_duration );

template< class Clock, class Duration >
bool try_lock_shared_until( const std::chrono::time_point<Clock,Duration>& timeout_time );

总结

由于它们额外的复杂性,读/写锁std::shared_mutexstd::shared_timed_mutex优于普通锁std::mutexstd::timed_mutex的情况比较少见。但是理论上确实存在。

如果在频繁但短暂的读取操作场景,读/写互斥不会提高性能。它更适合于读取操作频繁且耗时的场景。当读操作只是在内存数据结构中查找时,很可能简单的锁会胜过读/写锁。

如果读取操作的开销非常大,并且您可以并行处理许多操作,那么在某些时候增加读写比率应该会导致读取/写入器性能优于排他锁的情况。断点在哪里取决于实际工作量。

另请注意,在持有锁的同时执行耗时的操作通常是一个坏兆头。可能有更好的方法来解决问题,然后使用读/写锁。

还要注意,在使用mutex时,要时刻注意lock()与unlock()的加锁临界区的范围,不能太大也不能太小,太大了会导致程序运行效率低下,大小了则不能满足我们对程序的控制。并且我们在加锁之后要及时解锁,否则会造成死锁,lock()与unlock()应该是成对出现。

参考:
https://www.apiref.com/cpp-zh/cpp/thread.html

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++ 多线程:互斥量(mutex) 的相关文章

  • 在 VS2017 下使用 Conan 和 CMake 项目进行依赖管理

    我正在尝试使用 CMake 与 VS2017 集成为 C 设置一个开发环境 以便在 Linux x64 下进行编译 为了更好地管理依赖关系 我选择使用 Conan 但我对这个软件还很陌生 我想知道让 VS2017 识别项目依赖关系的最佳方法
  • 内联函数/方法

    声明 内联函数必须在调用之前定义 这个说法正确吗 EDIT 该问题最初是德语 内联功能穆森 弗 伊赫雷姆 奥夫鲁夫定义 sein 也许它对任何人都有帮助 是的 它是正确的 但只是部分正确 它可能正确地重新构建如下 内联函数必须在每个翻译单位
  • Subversion 和 Visual Studio 项目的最佳实践

    我最近开始在 Visual Studio 中处理各种 C 项目 作为大型系统计划的一部分 该系统将用于替换我们当前的系统 该系统是由用 C 和 Perl 编写的各种程序和脚本拼凑而成的 我现在正在进行的项目已经达到了颠覆的临界点 我想知道什
  • 如何使用MySqlCommand和prepare语句进行多行插入?(#C)

    Mysql 给出了如何使用准备语句和 NET 插入行的示例 http dev mysql com doc refman 5 5 en connector net programming prepared html http dev mysq
  • 劫持系统调用

    我正在编写一个内核模块 我需要劫持 包装一些系统调用 我正在暴力破解 sys call table 地址 并使用 cr0 来禁用 启用页面保护 到目前为止一切顺利 一旦完成 我将公开整个代码 因此如果有人愿意 我可以更新这个问题 无论如何
  • 无法解析远程名称 - webclient

    我面临这个错误 The remote name could not be resolved russgates85 001 site1 smarterasp net 当我请求使用 Web 客户端读取 html 内容时 出现错误 下面是我的代
  • TcpClient 在异步读取期间断开连接

    我有几个关于完成 tcp 连接的问题 客户端使用 Tcp 连接到我的服务器 在接受客户端后listener BeginAcceptTcpClient ConnectionEstabilishedCallback null 我开始阅读netw
  • 如何在 C++ 中将 CString 转换为 double?

    我如何转换CString to a double在 C 中 Unicode 支持也很好 Thanks A CString可以转换为LPCTSTR 这基本上是一个const char const wchar t 在 Unicode 版本中 知
  • 两种类型的回发事件

    1 我发现了两篇文章 每篇文章对两种类型的回发事件的分类都略有不同 一位资源说两种类型的回发事件是Changed事件 其中控件实现 IPostbackDataHandler 当数据在回发之间更改时触发 然后Raised事件 其中控件实现 I
  • 预处理后解析 C++ 源文件

    我正在尝试分析c 使用我定制的解析器的文件 写在c 在开始解析之前 我想摆脱所有 define 我希望源文件在预处理后可以编译 所以最好的方法是运行C Preprocessor在文件上 cpp myfile cpp temp cpp or
  • 为什么具有相同名称但不同签名的多个继承函数不会被视为重载函数?

    以下代码片段在编译期间产生 对 foo 的调用不明确 错误 我想知道是否有任何方法可以解决此问题而不完全限定对 foo 的调用 include
  • 默认析构函数做了多少事情

    C 类中的默认析构函数是否会自动删除代码中未显式分配的成员 例如 class C public C int arr 100 int main void C myC new C delete myC return 0 删除 myC 会自动释放
  • C# 中的常量和只读? [复制]

    这个问题在这里已经有答案了 可能的重复 const 和 readonly 之间有什么区别 https stackoverflow com questions 55984 what is the difference between cons
  • 为什么要在 C++ 中使用 typedef?

    可以说我有 set
  • 0-1背包算法

    以下 0 1 背包问题是否可解 浮动 正值和 浮动 权重 可以是正数或负数 背包的 浮动 容量 gt 0 我平均有 这是一个相对简单的二进制程序 我建议用蛮力进行修剪 如果任何时候你超过了允许的重量 你不需要尝试其他物品的组合 你可以丢弃整
  • 从 Delphi 调用 C# dll

    我用单一方法编写了 Net 3 5 dll 由Delphi exe调用 不幸的是它不起作用 步骤 1 使用以下代码创建 C 3 5 dll public class MyDllClass public static int MyDllMet
  • C++、三元运算符、std::cout

    如何使用 C 用三元运算符编写以下条件 int condition1 condition2 condition3 int double result int or double std cout lt lt condition1 resul
  • 在 Xamarin 中获取 OutOfMemoryException

    java lang OutOfMemoryError 考虑增加 JavaMaximumHeapSize Java 执行时内存不足 java exe 我的 Visualstudio Xamarin 项目出现内存不足异常 请帮助我如何解决此问题
  • 服务器响应 PASV 命令返回的地址与建立 FTP 连接的地址不同

    System Net WebException 服务器响应 PASV 命令返回的地址与建立 FTP 连接的地址不同 在 System Net FtpWebRequest CheckError 在 System Net FtpWebReque
  • C#中为线程指定特殊的cpu

    我有 2 个线程 我想告诉其中一个在第一个 cpu 上运行 第二个在第二个 cpu 上运行 例如在具有两个 cpu 的机器中 我怎样才能做到这一点 这是我的代码 UCI UCIMain new UCI Thread UCIThread ne

随机推荐

  • Docker Harbor 私有镜像仓库的部署和管理

    目录 一 什么是Harbor 二 Harbor的特性 三 Harbor的构成 四 部署配置Docker Harbor 首先需要安装 Docker Compose 服务 部署 Harbor 服务 修改配置文件 docker配置文件添加本地仓库
  • 蚁群算法(ACO)分析总结(Matlab+C#模拟解决TSP旅行商问题)

    蚁群算法 1 1 简介 1 2 整体框架 1 3 蚁群算法的基本要素 1 3 1 信息素的正反馈机制 1 3 2 信息素的更新策略 1 3 3 算法停止准则 1 4 蚂蚁个体的建模问题 1 5 蚁群算法的重要参数 1 6 蚁群算法的基本流程
  • 怎么判断私网地址_如何判断一个IP地址是私有地址

    如何判断一个IP地址是私有地址 首先 我们得先了解什么是私有地址 本文所指的IP地址 皆是IPV4 一个IPV4地址 由四段组成 最大值为255 一个IP地址其实就是一个32位的bit串 每8位一段 所谓私有地址 就是非注册地址 只能做内网
  • char数组和指针问题

    这个问题是C 基础问题中相当折腾人的一个 死记硬背解决不了根本问题 记住还是要忘 需要仔细研究其本质 这两种方式就是数组和指针的方式 char a 6 abcde char b abcde 第一行声明了并初始化了一个char数组 第二行是声
  • 从传统软件开发到云原生转型:大数据和AI如何引领软件开发的新趋势

    文章目录 1 数据驱动的开发 2 智能化的用户体验 3 云原生的可扩展性 4 实时处理和决策 5 自动化和效率提升 6 持续集成和交付的加速 7 数据安全和隐私 8 持续学习和创新 个人主页 程序员 小侯 CSDN新晋作者 欢迎 点赞 评论
  • C语言:IP地址

    题目 include
  • C6385:从“buffer”中读取的数据无效: 可读大小为“recv()`72”个字节,但可能读取了“25”个字节。

    C 网络编程中接收结构体对象遇到的问题 想从客户端发送一个结构体对象到服务器 在网上查询后发现可以在客户端用memcpy把结构体拷贝到字符串上发送给客户端 再在客户端把字符串转化为结构体 具体代码如下 结构体 typedef struct
  • 算法知识之最长公共子序列问题(动态规划)

    最近朋友让帮做个关于动态规划的最长公共子序列的问题 翻看以前的笔记并完成该题后 顺便写这样一篇文章 希望对大家有所帮助 同时也帮助自己回顾该知识点 一 最长公共子序列的定义 子序列 若给定序列X x1 x2 xm 则另一序列Z z1 z2
  • 刷脸支付必定是站在时代舞台中央的新星

    2020年初这场突如其来的疫情 打乱了各行各业的节奏 据2019年中国刷脸支付技术应用社会价值专题研究报告预计 2022年刷脸支付市场规模将突破7 6亿人 这说明现在大家习惯的支付是扫码支付 而一两年后人们将形成全新的支付习惯 刷脸支付 疫
  • 【布局 Widget】 Flutter SizedBox

    Flutter Sizedbox 是一个 布局组件 用来给 child 添加 tight 约束的 也可以用来添加空白 width height是 Sizedbox 的参数 BoxConstraints get additionalConst
  • 用户名 不在 sudoers文件中,此事将被报告。

    sudo password for lkp lkp 不在 sudoers 文件中 此事将被报告 sudo命令可以让你以root身份执行命令 来完成一些我们这个帐号完成不了的任务 其实并非所有用户都能够执行sudo 因为有权限的用户都在 et
  • CTFshow web1~8 WP Write by SunnyDog

    文章目录 web签到题 查看源码 web2 SQL注入 web3 input伪协议 log注入 Web4 log注入 Web5 代码审计 Web6 SQL注入 但是过滤 Web7 SQL注入 Web8 脚本布尔盲注 web签到题 查看源码
  • 解决:c++: internal compiler error: Killed (program cc1plus)

    TOC 解决 c internal compiler error Killed program cc1plus 1 出现问题 博主在VMware虚拟机中安装sophus库 执行make命令时出现错误 如下图所示 2 分析问题 多方查找 确定
  • 文件自动化处理

    文件自动化处理 1 1 读写文件 1 1 1 文件与文件路径 1 1 2 当前工作目录 1 1 3 路径操作 1 1 3 1 绝对路径和相对路径 1 1 3 2 路径操作 1 1 3 3 路径有效性检查 1 1 4 文件及文件夹操作 1 1
  • 光线追踪算法

    文章目录 光线追踪算法 1 光线追踪算法介绍 2 光线追踪算法重要的公式 3 光线追踪算法在matlab中的应用实例 4 光线追踪算法在Lingo中的应用实例 5 个人感悟 光线追踪算法 1 光线追踪算法介绍 光线追踪算法是一种基于物理光线
  • Android--自定义数字输入框

    工具类 前台页面 keyboadview xml 键盘页面 Java后台 KeyboardPopupWindow 步骤 1 initView 每自定义一个数字输入框 方法内部的代码都要重写并绑定相应输入框控件 2 onKeyDown int
  • 城市扩张对生境质量影响评价与生态安全格局构建—以黄河流域“几”字弯为例

    1 亮点 以高生境质量为 源地 构建生态廊道 城市扩张对生境质量的影响是异质的 提出 几 字弯 两屏三廊七区 的生态格局 2 关键词 城市扩张 生境质量 生态安全格局 城市化 InVEST模型 MCR模型 可持续发展目标 干旱地区 3 摘要
  • awk命令详解

    http www cnblogs com serendipity archive 2011 08 01 2124118 html 简单使用 awk 对于文件中一行行的独处来执行操作 awk F print 1 4 使用 来分割这一行 把这一
  • 二维我的世界源代码(c++)

    include
  • C++ 多线程:互斥量(mutex)

    C 11中的互斥量 声明在