diff --git "a/code/ModernCpp-ConcurrentProgramming-Tutorial/09-\345\256\236\347\216\260joining_thread.cpp" "b/code/ModernCpp-ConcurrentProgramming-Tutorial/09-\345\256\236\347\216\260joining_thread.cpp" new file mode 100644 index 00000000..ec488654 --- /dev/null +++ "b/code/ModernCpp-ConcurrentProgramming-Tutorial/09-\345\256\236\347\216\260joining_thread.cpp" @@ -0,0 +1,69 @@ +#include +#include +#include +#include +#include + +class joining_thread { + std::thread t; +public: + joining_thread()noexcept = default; + + template + explicit joining_thread(Callable&& func, Args&&...args) : + t{ std::forward(func), std::forward(args)... } {} + + explicit joining_thread(std::thread t_)noexcept : t{ std::move(t_) } {} + + joining_thread(joining_thread&& other)noexcept : t{ std::move(other.t) } {} + + joining_thread& operator=(std::thread&& other)noexcept { + if (joinable()) { // 如果当前有活跃线程(判断当前对象是否持有资源),那就先执行完(先释放) + join(); // 就相当于释放资源一样的意思 + } + t = std::move(other); + return *this; + } + ~joining_thread() { + if (joinable()) { + join(); + } + } + void swap(joining_thread& other)noexcept { + t.swap(other.t); + } + std::thread::id get_id()const noexcept { + return t.get_id(); + } + bool joinable()const noexcept { + return t.joinable(); + } + void join() { + t.join(); + } + void detach() { + t.detach(); + } + std::thread& data()noexcept { + return t; + } + const std::thread& data()const noexcept { + return t; + } +}; + +int main(){ + auto func = []{ + std::cout << std::this_thread::get_id() << '\n'; + }; + + std::vector vec; + + for (int i = 0; i < 10; ++i){ + vec.emplace_back(func); + } + + for(auto& thread : vec){ + thread.join(); + } +} \ No newline at end of file diff --git a/code/ModernCpp-ConcurrentProgramming-Tutorial/10-C++20jthread.cpp b/code/ModernCpp-ConcurrentProgramming-Tutorial/10-C++20jthread.cpp new file mode 100644 index 00000000..308b281f --- /dev/null +++ b/code/ModernCpp-ConcurrentProgramming-Tutorial/10-C++20jthread.cpp @@ -0,0 +1,20 @@ +#include +#include + +using namespace std::literals::chrono_literals; + +void f(std::stop_token stop_token, int value) { + while (!stop_token.stop_requested()) { // 检查是否已经收到停止请求 + std::cout << value++ << ' ' << std::flush; + std::this_thread::sleep_for(200ms); + } + std::cout << std::endl; +} + +int main() { + std::jthread thread{ f, 1 }; // 打印 1..15 大约 3 秒 + std::this_thread::sleep_for(3s); + thread.request_stop(); // 发送信息,线程终止 + std::cout << "乐\n"; + // jthread 的析构函数调用 request_stop() 和 join()。 +} \ No newline at end of file diff --git "a/code/ModernCpp-ConcurrentProgramming-Tutorial/11-\346\225\260\346\215\256\347\253\236\344\272\211.cpp" "b/code/ModernCpp-ConcurrentProgramming-Tutorial/11-\346\225\260\346\215\256\347\253\236\344\272\211.cpp" new file mode 100644 index 00000000..5607d6c6 --- /dev/null +++ "b/code/ModernCpp-ConcurrentProgramming-Tutorial/11-\346\225\260\346\215\256\347\253\236\344\272\211.cpp" @@ -0,0 +1,19 @@ +#include +#include +#include + +std::vector v; + +int n = 1; + +int main() { + int cnt = 0; + auto f = [&] { cnt++; }; + std::thread t1{ f }, t2{ f }, t3{ f }; // ub 未定义行为 + t1.join(); + t2.join(); + t3.join(); + std::cout << cnt << '\n'; +} +// 数据竞争它是未定义行为,但是 C++ 的编译器,它会假设你的程序(假设程序是对的,代码是对的)没有任何的未定义行为再去进行优化 +// 输出 n,优化,直接缓存这个值 \ No newline at end of file diff --git "a/code/ModernCpp-ConcurrentProgramming-Tutorial/12-\344\275\277\347\224\250\344\272\222\346\226\245\351\207\217.cpp" "b/code/ModernCpp-ConcurrentProgramming-Tutorial/12-\344\275\277\347\224\250\344\272\222\346\226\245\351\207\217.cpp" new file mode 100644 index 00000000..534d2410 --- /dev/null +++ "b/code/ModernCpp-ConcurrentProgramming-Tutorial/12-\344\275\277\347\224\250\344\272\222\346\226\245\351\207\217.cpp" @@ -0,0 +1,44 @@ +#include +#include +#include +#include +#include +#include +#include + +std::mutex m; + +// 写 +void add_to_list(int n, std::list& list) { + std::vector numbers(n + 1); + std::iota(numbers.begin(), numbers.end(), 0); + int sum = std::accumulate(numbers.begin(), numbers.end(), 0); + + { + std::scoped_lock lc{ m }; + list.push_back(sum); + } +} + +// 读 +void print_list(const std::list& list) { + std::scoped_lock lc{ m }; + for (const auto& i : list) { + std::cout << i << ' '; + } + std::cout << '\n'; +} + +int main(){ + std::list list; + std::thread t1{ add_to_list,10,std::ref(list) }; + std::thread t2{ add_to_list,10,std::ref(list) }; + std::thread t3{ print_list,std::cref(list) }; + std::thread t4{ print_list,std::cref(list) }; + t1.join(); + t2.join(); + t3.join(); + t4.join(); + std::cout << "---------------------\n"; + print_list(list); +} \ No newline at end of file diff --git a/code/ModernCpp-ConcurrentProgramming-Tutorial/13-try_lock.cpp b/code/ModernCpp-ConcurrentProgramming-Tutorial/13-try_lock.cpp new file mode 100644 index 00000000..fcbbf365 --- /dev/null +++ b/code/ModernCpp-ConcurrentProgramming-Tutorial/13-try_lock.cpp @@ -0,0 +1,33 @@ +#include +#include +#include +#include +#include +using namespace std::string_literals; + +std::mutex mtx; + +void thread_function(int id) { + // 尝试加锁 + if (mtx.try_lock()) { + std::string s = "线程:"s + std::to_string(id) + " 获得锁"s + "\n"; + std::string s2 = "线程:"s + std::to_string(id) + " 释放锁"s + "\n"; + std::cout << s; + // 临界区代码 + std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟临界区操作 + mtx.unlock(); // 解锁 + std::cout << s2; + } + else { + std::string s = "线程:"s + std::to_string(id) + " 获取锁失败 处理步骤"s + "\n"; + std::cout << s; + } +} + +int main(){ + std::thread t1(thread_function, 1); + std::thread t2(thread_function, 2); + + t1.join(); + t2.join(); +} \ No newline at end of file diff --git "a/code/ModernCpp-ConcurrentProgramming-Tutorial/14-\344\277\235\346\212\244\345\205\261\344\272\253\346\225\260\346\215\256.cpp" "b/code/ModernCpp-ConcurrentProgramming-Tutorial/14-\344\277\235\346\212\244\345\205\261\344\272\253\346\225\260\346\215\256.cpp" new file mode 100644 index 00000000..d93d694d --- /dev/null +++ "b/code/ModernCpp-ConcurrentProgramming-Tutorial/14-\344\277\235\346\212\244\345\205\261\344\272\253\346\225\260\346\215\256.cpp" @@ -0,0 +1,40 @@ +#include +#include +#include + +class Data { + int a{}; + std::string b{}; +public: + void do_something() { + // 修改数据成员等... + } +}; + +class Data_wrapper { + Data data; + std::mutex m; +public: + template + void process_data(Func func) { + std::lock_guard lc{ m }; + func(data); // 受保护数据传递给函数 + } +}; + +Data* p = nullptr; + +void malicious_function(Data& protected_data) { + p = &protected_data; // 受保护的数据被传递到外部 +} + +Data_wrapper d; + +void foo() { + d.process_data(malicious_function); // 传递了一个恶意的函数 + p->do_something(); // 在无保护的情况下访问保护数据 +} + +int main(){ + +} \ No newline at end of file diff --git "a/code/ModernCpp-ConcurrentProgramming-Tutorial/15-\346\255\273\351\224\201\357\274\232\351\227\256\351\242\230\344\270\216\350\247\243\345\206\263.cpp" "b/code/ModernCpp-ConcurrentProgramming-Tutorial/15-\346\255\273\351\224\201\357\274\232\351\227\256\351\242\230\344\270\216\350\247\243\345\206\263.cpp" new file mode 100644 index 00000000..dd22b6e2 --- /dev/null +++ "b/code/ModernCpp-ConcurrentProgramming-Tutorial/15-\346\255\273\351\224\201\357\274\232\351\227\256\351\242\230\344\270\216\350\247\243\345\206\263.cpp" @@ -0,0 +1,28 @@ +#include +#include +#include +#include +using namespace std::chrono_literals; + +struct X { + X(const std::string& str) :object{ str } {} + + friend void swap(X& lhs, X& rhs); +private: + std::string object; + std::mutex m; +}; + +void swap(X& lhs, X& rhs) { + if (&lhs == &rhs) return; + std::scoped_lock guard{ lhs.m,rhs.m }; + swap(lhs.object, rhs.object); +} + +int main(){ + X a{ "🤣" }, b{ "😅" }; + std::thread t{ [&] {swap(a, b); } }; // 1 + std::thread t2{ [&] {swap(b, a); } }; // 2 + t.join(); + t2.join(); +} \ No newline at end of file diff --git a/code/ModernCpp-ConcurrentProgramming-Tutorial/16-unique_lock.cpp b/code/ModernCpp-ConcurrentProgramming-Tutorial/16-unique_lock.cpp new file mode 100644 index 00000000..74e32db7 --- /dev/null +++ b/code/ModernCpp-ConcurrentProgramming-Tutorial/16-unique_lock.cpp @@ -0,0 +1,32 @@ +#include +#include +#include +#include +using namespace std::chrono_literals; + +struct X { + X(const std::string& str) :object{ str } {} + + friend void swap(X& lhs, X& rhs); +private: + std::string object; + std::mutex m; +}; + +void swap(X& lhs, X& rhs) { + if (&lhs == &rhs) return; + std::lock(rhs.m, lhs.m); + + std::unique_lock lock1{ lhs.m, std::adopt_lock }; + std::unique_lock lock2{ rhs.m, std::adopt_lock }; + // std::lock(lock1, lock2); + swap(lhs.object, rhs.object); +} + +int main() { + X a{ "🤣" }, b{ "😅" }; + std::thread t{ [&] {swap(a, b); } }; // 1 + std::thread t2{ [&] {swap(b, a); } }; // 2 + t.join(); + t2.join(); +} \ No newline at end of file diff --git "a/code/ModernCpp-ConcurrentProgramming-Tutorial/17-\345\234\250\344\270\215\345\220\214\344\275\234\347\224\250\345\237\237\344\274\240\351\200\222\344\272\222\346\226\245\351\207\217.cpp" "b/code/ModernCpp-ConcurrentProgramming-Tutorial/17-\345\234\250\344\270\215\345\220\214\344\275\234\347\224\250\345\237\237\344\274\240\351\200\222\344\272\222\346\226\245\351\207\217.cpp" new file mode 100644 index 00000000..dbe08886 --- /dev/null +++ "b/code/ModernCpp-ConcurrentProgramming-Tutorial/17-\345\234\250\344\270\215\345\220\214\344\275\234\347\224\250\345\237\237\344\274\240\351\200\222\344\272\222\346\226\245\351\207\217.cpp" @@ -0,0 +1,19 @@ +#include +#include +#include +#include +#include + +std::unique_lock get_lock() { + extern std::mutex some_mutex; + std::unique_lock lk{ some_mutex }; + return lk; // 选择到 unique_lock 的移动构造,转移所有权 +} +void process_data() { + std::unique_lock lk{ get_lock() }; // 转移到了主函数的 lk 中 + // 执行一些任务... +}// 最后才会 unlock 解锁 + +int main(){ + process_data(); +} \ No newline at end of file diff --git "a/code/ModernCpp-ConcurrentProgramming-Tutorial/18-\344\277\235\346\212\244\345\205\261\344\272\253\346\225\260\346\215\256\347\232\204\345\210\235\345\247\213\345\214\226\350\277\207\347\250\213.cpp" "b/code/ModernCpp-ConcurrentProgramming-Tutorial/18-\344\277\235\346\212\244\345\205\261\344\272\253\346\225\260\346\215\256\347\232\204\345\210\235\345\247\213\345\214\226\350\277\207\347\250\213.cpp" new file mode 100644 index 00000000..8b19028c --- /dev/null +++ "b/code/ModernCpp-ConcurrentProgramming-Tutorial/18-\344\277\235\346\212\244\345\205\261\344\272\253\346\225\260\346\215\256\347\232\204\345\210\235\345\247\213\345\214\226\350\277\207\347\250\213.cpp" @@ -0,0 +1,51 @@ +#include +#include +#include +#include +#include + +struct some{ + void do_something(){} +}; + +std::shared_ptr ptr; +std::once_flag resource_flag; + +void init_resource() { + ptr.reset(new some); +} + +void foo() { + std::call_once(resource_flag, []{ptr.reset(new some); }); // 线程安全的一次初始化 + ptr->do_something(); +} + +void test(){ + std::call_once(resource_flag, [] {std::cout << "f init\n"; }); +} + +std::once_flag flag; +int n = 0; + +void f() { + std::call_once(flag, [] { + ++n; + std::cout << "第 " << n << " 次调用\n"; + throw std::runtime_error("异常"); + }); +} + +class my_class{}; + +inline my_class& get_my_class_instance() { + static my_class instance; // 线程安全的初始化过程 初始化严格发生一次 + return instance; +} + +int main() { + get_my_class_instance(); + get_my_class_instance(); + get_my_class_instance(); + get_my_class_instance(); + get_my_class_instance(); +} \ No newline at end of file diff --git "a/code/ModernCpp-ConcurrentProgramming-Tutorial/19\344\277\235\346\212\244\344\270\215\345\270\270\346\233\264\346\226\260\347\232\204\346\225\260\346\215\256\347\273\223\346\236\204.cpp" "b/code/ModernCpp-ConcurrentProgramming-Tutorial/19\344\277\235\346\212\244\344\270\215\345\270\270\346\233\264\346\226\260\347\232\204\346\225\260\346\215\256\347\273\223\346\236\204.cpp" new file mode 100644 index 00000000..135048b8 --- /dev/null +++ "b/code/ModernCpp-ConcurrentProgramming-Tutorial/19\344\277\235\346\212\244\344\270\215\345\270\270\346\233\264\346\226\260\347\232\204\346\225\260\346\215\256\347\273\223\346\236\204.cpp" @@ -0,0 +1,47 @@ +#include +#include +#include +#include +#include +#include + +class Settings { +private: + std::map data_; + mutable std::shared_timed_mutex mutex_; // “M&M 规则”:mutable 与 mutex 一起出现 + +public: + void set(const std::string& key, const std::string& value) { + std::lock_guard lock{ mutex_ }; + data_[key] = value; + } + + std::string get(const std::string& key) const { + std::shared_lock lock(mutex_); + auto it = data_.find(key); + return (it != data_.end()) ? it->second : ""; // 如果没有找到键返回空字符串 + } +}; + +Settings set; + +void read(){ + (void)set.get("1"); +} + +void write(){ + set.set("1", "a"); +} + +int main(){ + std::thread t{ read }; + std::thread t2{ read }; + std::thread t3{ read }; + std::thread t4{ read }; + std::thread t5{ write }; + t.join(); + t2.join(); + t3.join(); + t4.join(); + t5.join(); +} \ No newline at end of file diff --git a/code/ModernCpp-ConcurrentProgramming-Tutorial/20recursive_mutex.cpp b/code/ModernCpp-ConcurrentProgramming-Tutorial/20recursive_mutex.cpp new file mode 100644 index 00000000..aa01fd21 --- /dev/null +++ b/code/ModernCpp-ConcurrentProgramming-Tutorial/20recursive_mutex.cpp @@ -0,0 +1,21 @@ +#include +#include +#include + +std::recursive_mutex mtx; + +void recursive_function(int count) { + std::lock_guard lc{ mtx }; + std::cout << "Locked by thread: " << std::this_thread::get_id() << ", count: " << count << std::endl; + if (count > 0) { + recursive_function(count - 1); + } +} + +int main() { + std::thread t1(recursive_function, 3); + std::thread t2(recursive_function, 2); + + t1.join(); + t2.join(); +} \ No newline at end of file diff --git a/code/ModernCpp-ConcurrentProgramming-Tutorial/CMakeLists.txt b/code/ModernCpp-ConcurrentProgramming-Tutorial/CMakeLists.txt index 1bd28475..94b0963f 100644 --- a/code/ModernCpp-ConcurrentProgramming-Tutorial/CMakeLists.txt +++ b/code/ModernCpp-ConcurrentProgramming-Tutorial/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required (VERSION 3.8) +cmake_minimum_required (VERSION 3.8) project ("ModernCpp-ConcurrentProgramming-Tutorial") @@ -7,9 +7,9 @@ set(CMAKE_CXX_STANDARD 17) SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) if(MSVC) - add_compile_options("/utf-8" "/permissive-") + add_compile_options("/utf-8" "/permissive-" "/Zc:nrvo" "/Od") elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") add_compile_options("-finput-charset=UTF-8" "-fexec-charset=UTF-8") endif() -add_executable(ModernCpp-ConcurrentProgramming-Tutorial "01-HelloWorld.cpp") +add_executable(ModernCpp-ConcurrentProgramming-Tutorial "20recursive_mutex.cpp") diff --git a/code/ModernCpp-ConcurrentProgramming-Tutorial/test/test_mutex.cpp b/code/ModernCpp-ConcurrentProgramming-Tutorial/test/test_mutex.cpp new file mode 100644 index 00000000..c8a0555d --- /dev/null +++ b/code/ModernCpp-ConcurrentProgramming-Tutorial/test/test_mutex.cpp @@ -0,0 +1,3 @@ +#include + +std::mutex some_mutex; diff --git "a/md/02\344\275\277\347\224\250\347\272\277\347\250\213.md" "b/md/02\344\275\277\347\224\250\347\272\277\347\250\213.md" index 07c54015..ef2d5342 100644 --- "a/md/02\344\275\277\347\224\250\347\272\277\347\250\213.md" +++ "b/md/02\344\275\277\347\224\250\347\272\277\347\250\213.md" @@ -974,7 +974,7 @@ _NODISCARD_CTOR_JTHREAD explicit jthread(_Fn&& _Fx, _Args&&... _Ax) { ### 总结 -**零开销原则**应当很好理解。我们本节的难点只在于使用到了一些 MSVC STL 的源码实现来配合理解,其主要在于“线程停止”。线程停止设施你会感觉是一种类似与外部与线程进行某种信号通信的设施,`std::stop_source` 和 `std::stop_token` 都与线程对象关联,然后来管理函数到底如何执行。 +**零开销原则**应当很好理解。我们本节的难点只在于使用到了一些 MSVC STL 的源码实现来配合理解,其主要在于“线程停止”。线程停止设施你会感觉是一种类似于外部与线程进行某种信号通信的设施,`std::stop_source` 和 `std::stop_token` 都与线程对象关联,然后来管理函数到底如何执行。 我们并没有举很多的例子,我们觉得这一个小例子所牵扯到的内容也就足够了,关键在于理解其设计与概念。 @@ -986,6 +986,6 @@ _NODISCARD_CTOR_JTHREAD explicit jthread(_Fn&& _Fx, _Args&&... _Ax) { 本章节的内容围绕着:“使用线程”,也就是"**使用 `std::thread`**"展开, `std::thread` 是我们学习 C++ 并发支持库的重中之重,在最后谈起了 C++20 引入的 `std::jthread` ,它的使用与概念也非常的简单。本章的内容在市面上并不算少见,但是却是少有的**准确与完善**。即使你早已学习乃至使用 C++ 标准库进行多线程编程,我相信本章也一定可以让你收获良多。 -并且如果是第一次学习本章的内容,能会有一些难以理解的地方。建议你多思考、多记忆,并在以后反复查看和实践。 +并且如果是第一次学习本章的内容,可能会有一些难以理解的地方。建议你多思考、多记忆,并在以后反复查看和实践。 我尽量以简单通俗的方式进行讲解。学完本章后,你可能还无法在实际环境利用多线程提升程序效率,至少还需要学习到使用互斥量来保护共享数据,才能实际应用多线程编程。 diff --git "a/md/03\345\205\261\344\272\253\346\225\260\346\215\256.md" "b/md/03\345\205\261\344\272\253\346\225\260\346\215\256.md" index f1633a48..2bd09ee0 100644 --- "a/md/03\345\205\261\344\272\253\346\225\260\346\215\256.md" +++ "b/md/03\345\205\261\344\272\253\346\225\260\346\215\256.md" @@ -75,7 +75,7 @@ std::thread t1{f}, t2{f}, t3{f}; // 未定义行为 ## 使用互斥量 -互斥量(Mutex),又称为互斥锁(或者直接被称作“锁”),是一种用来保护**临界区**[^1]的特殊对象,其相当于实现了一个公共的“**标志位**”。它可以处于锁定(locked)状态,也可以处于解锁(unlocked)状态: +互斥量(Mutex),又常被称为互斥锁、互斥体(或者直接被称作“锁”),是一种用来保护**临界区**[^1]的特殊对象,其相当于实现了一个公共的“**标志位**”。它可以处于锁定(locked)状态,也可以处于解锁(unlocked)状态: 1. 如果互斥量是锁定的,通常说某个特定的线程正持有这个锁。 @@ -336,7 +336,7 @@ public: Data* p = nullptr; void malicious_function(Data& protected_data){ - p = &protected_data; // 受保护的数据被传递 + p = &protected_data; // 受保护的数据被传递到外部 } Data_wrapper d; @@ -482,7 +482,6 @@ void swap(X& lhs, X& rhs) { std::unique_lock lock2{ rhs.m, std::defer_lock }; std::lock(lock1, lock2); swap(lhs.object, rhs.object); - ++n; } ``` @@ -621,11 +620,10 @@ void f() { 一种可能的使用是允许函数去锁住一个互斥量,并将互斥量的所有权转移到调用者上,所以调用者可以在这个锁保护的范围内执行代码。 ```cpp -std::unique_lockget_lock(){ +std::unique_lock get_lock(){ extern std::mutex some_mutex; std::unique_lock lk{ some_mutex }; return lk; - } void process_data(){ std::unique_lock lk{ get_lock() }; @@ -678,8 +676,7 @@ void process_data(){ 2. C++ 标准委员会也认为处理此问题很重要,**所以标准库提供了 [`std::call_once`](https://zh.cppreference.com/w/cpp/thread/call_once) 和 [`std::once_flag`](https://zh.cppreference.com/w/cpp/thread/once_flag)** 来处理这种情况。比起锁住互斥量并显式检查指针,每个线程只需要使用 `std::call_once` 就可以。**使用 `std::call_once` 比显式使用互斥量消耗的资源更少,特别是当初始化完成之后**。 ```cpp - std::shared_ptrptr; - std::mutex m; + std::shared_ptr ptr; std::once_flag resource_flag; void init_resource(){ @@ -811,7 +808,7 @@ int main() { - [**`lock`**](https://zh.cppreference.com/w/cpp/thread/recursive_mutex/lock):线程可以在递归互斥体上重复调用 `lock`。在线程调用 `unlock` 匹配次数后,所有权才会得到**释放**。 -- [**`unlock`**](https://zh.cppreference.com/w/cpp/thread/recursive_mutex/unlock):若所有权层数为 1(此线程对 [lock()](https://zh.cppreference.com/w/cpp/thread/recursive_mutex/lock) 的调用恰好比 `unlock()` 多一次 )则**解锁互斥体**,否则将所有权层数减少 1。 +- [**`unlock`**](https://zh.cppreference.com/w/cpp/thread/recursive_mutex/unlock):若所有权层数为 1(此线程对 [lock()](https://zh.cppreference.com/w/cpp/thread/recursive_mutex/lock) 的调用恰好比 `unlock()` 多一次 )则**解锁互斥量**,否则将所有权层数减少 1。 我们重点的强调了一下这两个成员函数的这个概念,其实也很简单,总而言之就是 `unlock` 必须和 `lock` 的调用次数一样,才会真正解锁互斥量。