文章目录
4. 线程安全问题,不可重入情况,可重入情况
上篇我们提到,在抢票逻辑中,多线程访问情况下会出现票数为“0”和“-1”的情况,这显然是不符合生活实际的。因为座位数的固定,溢出的票则会导致超载的风险。
原子操作与非原子操作
非原子操作
以上述案例,通过简单的反汇编可知, ++
操作分为三步
- mov
- add
- mov
所以可知,明显++
不是原子的
原子操作
-
std::atomic 类型: 使用 std::atomic 声明的对象能够提供原子操作。在这种情况下,我们声明了两个 std::atomic 类型的对象 a 和 b。
-
exchange 方法: exchange 方法是 std::atomic 类提供的一个原子操作函数,用于将对象的值原子地交换为给定的值,并返回原来的值。在示例中,我们使用了 a.exchange(b) 来原子地将 a 的值交换为 b 的值,并将 a 的原始值保存在 temp 中。
-
多线程环境下的原子性: 在多线程环境下,如果没有原子操作,可能会出现竞态条件,导致数据不一致或未定义的行为。通过使用 std::atomic 类和 exchange 方法,我们确保了对 a 和 b 的操作是原子的。这意味着在执行交换操作时,不会被其他线程中断,也不会出现半途而废的情况。只有当交换完成时,才会允许其他线程访问这些值。
4.1 线程安全问题
- 线程安全: 多线程环境下,共享数据的操作不会引发数据不一致或不确定的结果。
- 线程不安全:多线程环境下,共享数据的操作可能引发数据不一致或不确定的结果。
线程安全问题是指在多线程环境中,当多个线程同时访问共享的数据或资源时可能导致的一系列问题。这些问题主要源于并发执行的线程之间的竞态条件(Race Condition)和资源竞争。线程安全问题可能导致程序产生不确定的行为,包括数据损坏、内存泄漏、死锁等。
主要的线程安全问题包括:
- 竞态条件(Race Condition):
- 定义: 当两个或多个线程同时访问共享的数据,其中至少一个是写操作时,可能导致未定义的行为。
- 导致原因: 竞态条件发生的主要原因是多线程执行时的不确定性,导致对共享资源的访问顺序无法预测。
- 死锁(Deadlock):
- 定义: 多个线程互相等待对方释放资源,导致所有线程都无法继续执行。
- 导致原因: 当线程持有某个资源并等待另一个资源时,而其他线程持有了该另一个资源并等待第一个资源时,就可能发生死锁。
- 数据竞争(Data Race):
- 定义: 多个线程同时访问共享的内存位置,其中至少一个是写操作,并且没有足够的同步来保护这些访问。
- 导致原因: 缺乏适当的同步机制,导致多个线程对相同的内存位置进行读写操作而发生竞争。
- 内存泄漏(Memory Leak):
- 定义: 分配的内存没有被释放,导致系统中的可用内存逐渐减少。
- 导致原因: 在多线程环境中,如果没有正确管理内存的分配和释放,可能导致某个线程分配的内存被另一个线程无法释放。
导致线程安全问题的原因主要包括:
共享资源
: 多个线程共享相同的数据或资源。缺乏同步机制
: 没有适当的同步措施来保护共享资源的访问。不可重入函数
: 在多线程环境中调用不可重入的函数可能引起线程安全问题。未正确使用锁
: 如果对锁的使用不正确,例如未正确地获取或释放锁,会导致死锁或其他问题。
解决线程安全问题通常涉及到使用同步机制,例如互斥锁、信号量、条件变量等,以确保对共享资源的访问是有序的和安全的。正确设计和实现多线程程序需要深入理解这些问题,并采取适当的措施来保障线程安全。
4.2 不可重入情况
不可重入: 函数在执行期间不可被中断,可能导致数据不一致。
#include <stdio.h>
int globalVar = 0; // 全局变量
void nonReentrantFunction() {
globalVar++;
printf("Global variable: %d\n", globalVar);
}
int main() {
// 在多任务或多线程环境中,多个任务调用 nonReentrantFunction 可能导致竞态条件
nonReentrantFunction();
nonReentrantFunction();
return 0;
}
nonReentrantFunction 使用了全局变量 globalVar,这使得它在多任务或多线程环境中容易引起竞态条件。
(明显的,上述抢票逻辑是不可重入情况。由上述可知,
因为在多线程情况下,线程 1 在进行++
操作时,++
中的三步操作(mov、add、mov)可能随时被另一线程 2 中断,则另一线程拿到的仍是线程 1处理之前的结果,再次同被中断线程做了相同的操作,此时和线程 1产生冲突)
4.3 可重入情况
可重入: 函数在执行期间可以被中断,中断后能继续执行而不会导致数据不一致。
函数可重入条件
函数不使用任何静态数据或全局数据:
可重入函数不依赖于全局变量或静态变量的状态,因为这些变量在多个调用之间是共享的。如果函数修改了全局或静态变量的状态,那么在多个任务同时调用时,可能会产生竞态条件或不一致的结果。函数不调用不可重入的函数:
可重入函数内部不调用不可重入的函数,因为不可重入的函数可能会依赖于全局状态或静态状态,从而导致竞态条件或不一致的结果。
#include <iostream>
void swap(int& x, int& y) {
int temp = x;
x = y;
y = temp;
}
int main() {
int a = 5;
int b = 10;
std::cout << "Before swap: a = " << a << ", b = " << b << std::endl;
swap(a, b);
std::cout << "After swap: a = " << a << ", b = " << b << std::endl;
return 0;
}
swap函数内部没有任何静态状态(如静态变量、全局变量等),并且它没有调用任何可能会导致非可重入行为的函数,因此它是可重入的。可重入函数是指在并发执行时能够安全地被多个线程调用的函数,而不会导致不一致或不确定的结果。
swap函数执行的是简单的变量交换操作,没有涉及到任何需要同步的共享资源,因此它也是线程安全的。然而,要注意的是,这里的线程安全性并不是因为原子性,而是因为在单线程环境下执行,因此不存在并发访问的情况。
4.4 可重入与线程安全联系和区别
联系:
可重入函数与线程安全函数之间的主要联系在于它们都关注并发环境下的正确性。当一个函数是可重入的,它意味着它可以在被多个并发实例调用时,能够正确地处理数据和状态,不会出现冲突或错误的结果。这种特性对于多线程环境尤为重要,因为多个线程可能同时调用同一个函数。由于可重入函数能够确保每个实例之间的数据是独立的,不会相互干扰或产生竞争条件,因此它们通常是线程安全的。换句话说,可重入函数的一种重要特性就是其线程安全性。
区别:
-
关注点:
可重入性主要关注函数在被多个并发实例调用时的行为。它强调函数的中断处理能力,即函数能够在任何时候被中断,并在恢复执行时不会出错。这要求函数内部不使用全局变量或静态变量,以避免数据竞争和状态不一致的问题。
线程安全性则主要关注函数在多线程环境下的行为。它强调对共享数据的正确处理,确保多个线程在访问和修改同一数据时不会引发冲突或错误。 -
范围:
可重入性是一个更广泛的概念,它不仅适用于多线程环境,也适用于其他并发环境,如多进程或多任务系统。只要存在并发执行的实例,可重入性就是一个重要的考虑因素。
线程安全性则特指在多线程环境下的正确性。它关注线程之间的同步和互斥,以确保数据的一致性和完整性。
假设有一个函数calculate_sum,它接受一个整数数组并返回数组元素的总和。
可重入性:
如果calculate_sum函数被设计为可重入的,那么无论它被多少个并发实例调用,它都能正确地计算出每个数组的总和。为了实现这一点,calculate_sum函数应该避免使用全局变量或静态变量来存储中间结果或状态。它应该只依赖于传递给它的参数(即整数数组)来进行计算。这样,即使多个实例同时调用calculate_sum函数,它们也不会相互干扰或产生错误的结果。
线程安全性:
如果calculate_sum函数需要在多线程环境下工作,并且需要访问共享数据(例如全局变量或静态变量),那么它必须是线程安全的。这可以通过使用互斥锁或其他同步机制来实现,以确保在任何时候只有一个线程能够访问和修改共享数据。例如,可以在函数开始时获取一个锁,进行必要的计算后释放锁。这样,即使多个线程同时调用calculate_sum函数,它们也不会同时访问和修改共享数据,从而避免了数据竞争和错误的结果。
可重入函数强调函数的中断处理能力,而线程安全函数则强调在多线程环境下对共享数据的正确处理。
5. 常见锁的概念
5.1 常见锁
互斥锁(Mutex):
- 概念:互斥锁是一种特殊的二值性信号量,用于实现对临界资源的独占式处理。任意时刻互斥锁只有两种状态:开锁或闭锁。当任务持有时,该任务获得互斥锁的所有权,并处于闭锁状态;当任务释放锁后,失去所有权,互斥锁处于开锁状态。
- 用法:在多线程环境中,当某个线程需要访问共享资源时,它会尝试获取互斥锁。如果锁已经被其他线程持有,则该线程会等待直到锁被释放。一旦线程获得锁,它可以安全地访问共享资源,并在完成访问后释放锁,允许其他线程访问。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 全局互斥锁
int shared_data = 0;
void increment() {
mtx.lock(); // 锁定互斥锁
++shared_data;
mtx.unlock(); // 解锁互斥锁
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Shared data: " << shared_data << std::endl; // 输出应为2
return 0;
}
信号量(Semaphore):
- 概念:信号量是一种特殊的变量,用于控制对临界资源的使用。它允许多个线程或进程访问共享资源,但限制了同时访问的数量。信号量有一个整数值,表示可用的资源数量。
- 用法:线程在访问共享资源前会先尝试减少信号量的值(通常称为P操作)。如果信号量的值大于0,线程可以继续执行;如果值为0,线程将被阻塞,直到信号量的值增加。当线程完成对共享资源的访问后,它会增加信号量的值(通常称为V操作),从而可能唤醒其他等待的线程。
C++标准库本身并不直接提供信号量的实现,但可以使用条件变量和互斥锁来模拟。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
class Semaphore {
private:
std::mutex mtx;
std::condition_variable cv;
int count;
public:
Semaphore(int initialCount) : count(initialCount) {}
void acquire() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this] { return count > 0; });
--count;
}
void release() {
std::unique_lock<std::mutex> lock(mtx);
++count;
cv.notify_one();
}
};
Semaphore sem(2); // 初始化信号量为2
void worker() {
sem.acquire(); // 获取信号量
// 执行需要同步的代码
std::cout << "Worker thread is running." << std::endl;
sem.release(); // 释放信号量
}
int main() {
std::thread t1(worker);
std::thread t2(worker);
std::thread t3(worker);
t1.join();
t2.join();
t3.join();
return 0;
}
条件变量(Condition Variable):
- 概念:条件变量是用于线程间同步的一种机制,它允许线程等待某个条件成立。条件变量通常与互斥锁一起使用,以确保对共享状态的正确访问。
- 用法:一个线程可以使用条件变量来等待某个条件为真。当条件不满足时,线程会将自己加入等待队列,并释放持有的互斥锁。另一个线程在改变条件并确认它为真后,可以唤醒等待的线程。这样,被唤醒的线程可以重新获取互斥锁并检查条件,然后继续执行。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
std::unique_lock<std::mutex> lck(mtx);
while (!ready) { // 等待条件成立
cv.wait(lck); // 阻塞当前线程
}
// 条件成立,继续执行
std::cout << "thread " << id << '\n';
}
void go() {
std::unique_lock<std::mutex> lck(mtx);
ready = true; // 改变条件
cv.notify_all(); // 唤醒所有等待的线程
}
int main() {
std::thread threads[10];
// spawn 10 threads:
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(print_id, i);
std::cout << "10 threads ready to race...\n";
go(); // go!
for (auto& th : threads) th.join();
return 0;
}
读写锁(Read-Write Lock):
- 概念:读写锁是一种特殊的锁,它将访问共享资源的线程划分为读者和写者。多个读者可以同时访问共享资源,但只允许一个写者进行写操作。
- 用法:当线程需要读取共享资源时,它会尝试获取读锁。如果有其他线程持有写锁或正在等待写锁,则读线程会等待。当线程需要写入共享资源时,它会尝试获取写锁。此时,如果有其他线程持有读锁或写锁,写线程也会等待。读写锁可以提高并发性能,特别是在多读少写的场景中。
#include <iostream>
#include <thread>
#include <vector>
#include <shared_mutex> // C++17 引入的读写锁
#include <random>
#include <chrono>
std::shared_timed_mutex rw_mtx; // 读写锁
std::vector<int> data{1, 2, 3, 4, 5};
void read_data() {
std::shared_lock<std::shared_timed_mutex> lock(rw_mtx); // 读者使用共享锁
for (const auto& value : data) {
std::cout << value << ' ';
}
std::cout << '\n';
}
void write_data(int new_value) {
std::unique_lock<std::shared_timed_mutex> lock(rw_mtx); // 写者使用独占锁
data.push_back(new_value);
std::cout << "Wrote " << new_value << " to data.\n";
}
int main() {
// 创建多个读线程
std::vector<std::thread> readers;
for (int i = 0; i < 5; ++i) {
readers.emplace_back(read_data);
}
// 等待所有读线程启动
for (auto& th : readers) {
th.detach(); // 分离线程,让它们在后台运行
}
// 创建写线程,并写入一些新数据
std::thread writer([&]() {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待读线程开始
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(10, 20);
for (int i = 0; i < 3; ++i) {
int new_value = dis(gen);
write_data(new_value);
std::this_thread::sleep_for(std::chrono::seconds(1)); // 休眠以观察效果
}
});
writer.join(); // 等待写线程完成
return 0;
}
在这个示例中,我们创建了5个读线程和一个写线程。读线程使用std::shared_lock来访问data向量,这允许多个线程同时读取。写线程使用std::unique_lock来确保它在修改data向量时具有独占访问权。
注意
,我们使用了detach方法来让读线程在后台运行,这样主线程(写线程)可以继续执行而不会被阻塞。写线程休眠了1秒以确保读线程有机会开始执行。然后,它生成了一些随机数,并将它们写入data向量。每次写入后,它都会休眠1秒,以便我们可以看到读写操作是如何交替进行的。这个示例展示了读写锁在允许多个读者同时访问资源,同时确保写操作的互斥性方面的用途。在实际应用中,您可能需要根据具体需求调整线程的数量、休眠时间以及读写操作的逻辑。
加锁解决上述抢票问题
问题
#include <iostream>
#include <thread>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
using namespace std;
// 如果多线程访问同一个全局变量,并对它进行数据计算,多线程会互相影响吗?
int tickets = 1000; // 在并发访问的时候,导致了我们数据不一致的问题!
void *getTickets(void *args)
{
(void)args;
while(true)
{
if(tickets > 0)
{
usleep(1000);
printf("%p: %d\n", pthread_self(), tickets);
tickets--;
}
else{
break;
}
}
return nullptr;
}
int main()
{
pthread_t t1,t2,t3;
// 多线程抢票的逻辑
pthread_create(&t1, nullptr, getTickets, nullptr);
pthread_create(&t2, nullptr, getTickets, nullptr);
pthread_create(&t3, nullptr, getTickets, nullptr);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
}
上述代码存在多线程并发访问同一个全局变量tickets的问题,
多个线程同时对tickets进行读写操作,可能会导致数据不一致的情况
。例如,当多个线程同时读取tickets的值并判断是否大于0时,都发现tickets大于0,然后同时进行减1操作,导致多个线程都执行了tickets–操作,但实际上只有一个线程能够成功减1,其他线程的减1操作会被覆盖掉,造成数据丢失。
解决方案:
为了避免多线程的竞争条件,可以采用互斥锁(mutex)机制来保护共享资源,确保每个线程对tickets的操作是原子的。
#include <iostream>
#include <thread>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <cassert>
#include <cstdio>
using namespace std;
int tickets = 10000; // 在并发访问的时候,导致了我们数据不一致的问题!临界资源
#define THREAD_NUM 800
class ThreadData
{
public:
ThreadData(const std::string &n,pthread_mutex_t *pm):tname(n), pmtx(pm)
{}
public:
std::string tname;
pthread_mutex_t *pmtx;
};
void *getTickets(void *args)
{
// int myerrno = errno;
ThreadData *td = (ThreadData*)args;
while(true)
{
// 抢票逻辑
int n = pthread_mutex_lock(td->pmtx);
assert(n == 0);
// 临界区
if(tickets > 0) // 1. 判断的本质也是计算的一种
{
usleep(rand()%1500);
printf("%s: %d\n", td->tname.c_str(), tickets);
tickets--; // 2. 也可能出现问题
n = pthread_mutex_unlock(td->pmtx);
assert(n == 0);
}
else{
n = pthread_mutex_unlock(td->pmtx);
assert(n == 0);
break;
}
// 抢完票,其实还需要后续的动作
usleep(rand()%2000);
// errno = myerrno;
}
delete td;
return nullptr;
}
int main()
{
time_t start = time(nullptr);
pthread_mutex_t mtx;
pthread_mutex_init(&mtx, nullptr);
srand((unsigned long)time(nullptr) ^ getpid() ^ 0x147);
pthread_t t[THREAD_NUM];
// 多线程抢票的逻辑
for(int i = 0; i < THREAD_NUM; i++)
{
std::string name = "thread ";
name += std::to_string(i+1);
ThreadData *td = new ThreadData(name, &mtx);
pthread_create(t + i, nullptr, getTickets, (void*)td);
}
for(int i = 0; i < THREAD_NUM; i++)
{
pthread_join(t[i], nullptr);
}
pthread_mutex_destroy(&mtx);
time_t end = time(nullptr);
cout << "cast: " << (int)(end - start) << "S" << endl;
}
-
在全局范围内定义了一个整型变量tickets,表示票数,它是临界资源,需要被多个线程竞争访问。
-
定义了ThreadData类,用于传递线程数据,包括线程名称和互斥锁指针。
3. 实现了getTickets函数作为线程的入口函数。线程通过传入ThreadData指针获取线程名称和互斥锁。在循环中,每个线程通过互斥锁对临界区进行保护。
4. 在临界区内,线程首先判断票数是否大于0,如果大于0,则模拟抢票过程,打印线程名称和当前剩余票数,然后将票数减少1,并释放互斥锁。如果票数为0,则表示票已抢完,线程释放互斥锁并退出循环。
-
抢完票后,线程休眠一段时间,模拟后续的其他操作。
-
在主函数中,初始化互斥锁并创建多个线程。每个线程都传入一个ThreadData对象,其中包含该线程的名称和互斥锁指针。然后使用pthread_create函数创建线程。
7. 主线程等待所有子线程结束后,销毁互斥锁。
- 最后,计算整个程序的执行时间,并输出。
改进后的代码通过使用互斥锁保护临界资源tickets,确保每个线程对tickets的访问是原子的,避免了数据不一致的问题。每个线程在进行操作时都会首先获得互斥锁,进入临界区,在临界区操作完成后释放互斥锁,确保其他线程能够访问临界资源。通过这种方式,多线程之间不会相互影响,保证了数据的一致性。
问题总结
如果多线程访问同一个全局变量,并对它进行数据计算,多线程会互相影响吗?
- 当多个线程同时访问同一个全局变量并进行数据计算时,如果没有加锁保护,这些线程会互相影响可能导致数据不一致的问题。例如,在代码中的tickets变量,如果多个线程同时减少票数,可能会导致票数减少的结果不正确。
- 解决:加锁保护:加锁的时候,一定要保证加锁的粒度,越小越好!!
- pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; // pthread_mutex_t 就是原生线程库提供的一个数据类型
加锁就是串行执行了吗?加锁了之后,线程在临界区中,是否会切换,会有问题吗?
-
加锁机制确保了在同一时间只有一个线程可以访问临界区内的代码,这被称为串行执行。当一个线程获得了锁并进入临界区时,其他线程必须等待该线程释放锁才能进入临界区执行。
-
在临界区内,线程之间可能会发生切换,这取决于操作系统的调度策略。当一个线程执行完临界区内的代码后,它会释放锁,然后等待系统的调度,其他线程在获得锁后可以进入临界区执行。
-
临界区的切换并不会引起问题,但是在多线程编程中,需要注意避免一些潜在的问题,例如:
-
竞态条件(Race condition):如果多个线程同时访问共享数据,并且在临界区内对这些数据进行修改操作,可能会导致数据不一致或不可预期的结果。
-
死锁(Deadlock):不正确的加锁顺序或资源争用可能导致死锁,即线程相互等待对方释放资源,造成程序无法继续执行。
-
饥饿(Starvation):某些线程可能会长时间等待锁的释放,无法进入临界区,造成饥饿现象。
- 为了避免这些问题,需要正确使用锁的机制,考虑并发访问共享数据的安全性,并尽量避免长时间的临界区,以及合理设置锁的粒度等。
加锁了之后,线程在临界区中,是否会切换,会有问题吗?
-
当一个线程获得了锁并进入临界区时,其他线程在申请同一个锁时会被阻塞,等待锁的释放。这意味着在临界区中只有一个线程在执行代码,其他线程会暂时停止执行。
-
在这种情况下,如果临界区内的代码不涉及阻塞操作或者长时间运算,那么问题通常不会出现。只有当临界区内的代码执行时间过长或者发生阻塞操作时,可能会导致一些问题。
-
长时间运行的临界区代码可能会导致其他线程长时间等待,降低程序的并发性能。此外,如果临界区内发生阻塞操作(如I/O操作),可能会导致整个程序的性能下降。
-
因此,为了避免这些问题,尽量将
临界区内的代码保持简洁,尽量避免耗时操作和阻塞操作
。这样可以减少其他线程的等待时间,提高程序的性能和响应能力。同时,合理使用锁的粒度,尽量减小加锁的范围,可以增加程序的并发度
。
- 会切换,不会!第一次理解:虽然被切换了,但是你是持有锁被切换的, 所以其他抢票线程要执行临界区代码,也必须先申请锁,锁它是无法申请成功的,所以,也不会让其他线程进入临界区,就保证了临界区中数据一致性!我是一个线程,我不申请锁,就是单纯的访问临界资源!-- 错误的编码方式在没有持有锁的线程看来,对我最有意义的情况只有两种:1. 线程1没有持有锁(什么都没做) 2. 线程1释放锁(做完),此时我可以申请锁!
加锁就是串行执行了吗?
是的,执行临界区代码一定是串行的!
要访问临界资源,每一个线程都必须现申请锁,每一个线程都必须先看到同一把锁&&访问它,锁本身是不是就是一种共享资源?
-
是的,锁本身可以被视为一种共享资源。当多个线程需要访问临界资源时,它们必须先申请同一把锁。只有一个线程能够成功地获取到锁,并进入临界区执行,其他的线程则需要等待。
-
锁通常是一种系统提供的机制,用于协调多个线程对共享资源的访问。它可以被看作一个信号,表示当前资源正在被占用,其他线程需要等待一段时间才能获取锁并访问资源。
-
因为锁是被多个线程共享和竞争的对象,所以它本身也需要进行同步和协调,以确保线程之间的顺序和公平性。这样,每个线程都有机会获得锁,并访问临界资源,从而保证多线程程序的正确性和可靠性。
谁来保证锁的安全呢??如何保证??锁是如何实现的?
所以,为了保证锁的安全,申请和释放锁,必须是 原子的!!!
锁的安全由操作系统或线程库来保证。
具体实现方式可能因操作系统和线程库的不同而有所差异,但通常都遵循一些常见的原则和机制。
- 主要的锁实现机制包括:互斥锁(Mutex)、读写锁(Read-Write Lock)、自旋锁(Spin Lock)、信号量(Semaphore)等。这些锁的安全性由操作系统或线程库内部的实现来保证。它们使用底层的同步原语(如原子操作、内核级同步等)来实现对锁的加锁和解锁操作的原子性,并确保在多线程环境下的正确性。
- 在实际使用中,开发者在编写多线程代码时要遵循正确的锁使用规则来保证锁的安全性,
避免死锁、竞态条件
等问题的出现。这包括正确的加锁和解锁顺序,合理的锁粒度,以及避免长时间的临界区操作等。
实际开发过程中,运用锁是需要非常小心的,那么我们应该怎么去避免,尽量让使用锁变得更安全呢?(请持续关注从0-1为您解释多线程(下))