如何使用 HAL 锁定和解锁功能?为什么?
How are the HAL lock and unlock functions used and why?
我正在尝试理解另一位程序员编写的代码。它使用I²C通信将数据写入STM32微控制器的EEPROM。
大体上我理解他的代码是如何工作的,但我不明白他为什么使用 HAL_LOCK
和 HAL_UNCLOCK
函数。
这些是这些方法的代码:
typedef enum
{
HAL_UNLOCKED = 0x00U,
HAL_LOCKED = 0x01U
} HAL_LockTypeDef;
#if (USE_RTOS == 1)
/* Reserved for future use */
#error "USE_RTOS should be 0 in the current HAL release"
#else
#define __HAL_LOCK(__HANDLE__) \
do{ \
if((__HANDLE__)->Lock == HAL_LOCKED) \
{ \
return HAL_BUSY; \
} \
else \
{ \
(__HANDLE__)->Lock = HAL_LOCKED; \
} \
} while (0)
#define __HAL_UNLOCK(__HANDLE__) \
do{ \
(__HANDLE__)->Lock = HAL_UNLOCKED; \
} while (0)
在哪些情况下可以使用这些方法?
有人尝试使用这些宏(有时称为 "lock" 或 "critical section",但这些术语有其他含义;"mutex" 是明确的)。这个想法是你写这样的代码:
int frob_handle(handle_t hdl)
{
__HAL_LOCK(hdl);
hdl->frob_counter += 1;
__HAL_UNLOCK(hdl);
return 0;
}
然后一次只有一个执行线程可以执行语句hdl->frob_counter += 1
。 (在一个真实的程序中,那里可能会有更多的代码。)它是 "advisory" 因为没有什么可以阻止你在需要时忘记使用互斥锁,它是 "non-recursive" 因为你可以'如果您已经锁定它,请不要再次调用 __HAL_LOCK
。这些都是互斥量相对正常的属性。
在问题的评论中,我说这些宏是"catastrophically buggy"和"I believe you are better off not using them at all."最重要的问题是__HAL_LOCK
不是atomic。
// This code is incorrect.
if ((__HANDLE__)->Lock == HAL_LOCKED)
return HAL_BUSY;
else
(__HANDLE__)->Lock = HAL_LOCKED;
假设两个执行线程正试图同时获取锁。它们会同时从内存中获取 __HANDLE__->Lock
,因此它们都会观察到它的值为 HAL_UNLOCKED
,并且它们都会继续处理本应仅由一个线程执行的代码一次。把可能生成的汇编语言写出来可能更容易看出问题:
; This code is incorrect.
; r1 contains the __HANDLE__ pointer
load.b r0, Lock(r1)
test.b r0
bnz .already_locked
inc.b r0
store.b Lock(r1), r0
...
.already_locked:
mov.b r0, #HAL_BUSY
ret
没有什么可以阻止两个线程同时执行加载指令,因此两个线程都观察到要解锁的互斥体。即使只有一个 CPU,当线程 1 在加载和存储之间时,也可能会触发中断,从而导致上下文切换并允许线程 2 在线程 1 执行存储之前执行加载。
为了使互斥锁发挥作用,您必须以某种方式确保两个并发线程 不可能 从 __HANDLE__->Lock
加载 HAL_UNLOCKED
,而这用普通的C是做不到的。事实上,用普通的机器语言是做不到的;您需要使用特殊指令,例如 compare-and-swap.
如果您的编译器实现了 C2011, then you can get at those special instructions using the new feature of atomic types,但我不知道如何做到这一点,我不会写出可能有问题的东西。否则,您需要使用编译器扩展或 hand-written 程序集。
第二个问题是 __HAL_LOCK
没有实现通常称为 "lock" 的操作。如果 "Lock" 不能立即获取锁,它应该 wait,但是 __HAL_LOCK
所做的是 fail。该操作的名称是 "try-lock",宏应该相应地命名。此外,可能导致调用函数 return 的宏被认为是不好的做法。
我正在尝试理解另一位程序员编写的代码。它使用I²C通信将数据写入STM32微控制器的EEPROM。
大体上我理解他的代码是如何工作的,但我不明白他为什么使用 HAL_LOCK
和 HAL_UNCLOCK
函数。
这些是这些方法的代码:
typedef enum
{
HAL_UNLOCKED = 0x00U,
HAL_LOCKED = 0x01U
} HAL_LockTypeDef;
#if (USE_RTOS == 1)
/* Reserved for future use */
#error "USE_RTOS should be 0 in the current HAL release"
#else
#define __HAL_LOCK(__HANDLE__) \
do{ \
if((__HANDLE__)->Lock == HAL_LOCKED) \
{ \
return HAL_BUSY; \
} \
else \
{ \
(__HANDLE__)->Lock = HAL_LOCKED; \
} \
} while (0)
#define __HAL_UNLOCK(__HANDLE__) \
do{ \
(__HANDLE__)->Lock = HAL_UNLOCKED; \
} while (0)
在哪些情况下可以使用这些方法?
有人尝试使用这些宏(有时称为 "lock" 或 "critical section",但这些术语有其他含义;"mutex" 是明确的)。这个想法是你写这样的代码:
int frob_handle(handle_t hdl)
{
__HAL_LOCK(hdl);
hdl->frob_counter += 1;
__HAL_UNLOCK(hdl);
return 0;
}
然后一次只有一个执行线程可以执行语句hdl->frob_counter += 1
。 (在一个真实的程序中,那里可能会有更多的代码。)它是 "advisory" 因为没有什么可以阻止你在需要时忘记使用互斥锁,它是 "non-recursive" 因为你可以'如果您已经锁定它,请不要再次调用 __HAL_LOCK
。这些都是互斥量相对正常的属性。
在问题的评论中,我说这些宏是"catastrophically buggy"和"I believe you are better off not using them at all."最重要的问题是__HAL_LOCK
不是atomic。
// This code is incorrect.
if ((__HANDLE__)->Lock == HAL_LOCKED)
return HAL_BUSY;
else
(__HANDLE__)->Lock = HAL_LOCKED;
假设两个执行线程正试图同时获取锁。它们会同时从内存中获取 __HANDLE__->Lock
,因此它们都会观察到它的值为 HAL_UNLOCKED
,并且它们都会继续处理本应仅由一个线程执行的代码一次。把可能生成的汇编语言写出来可能更容易看出问题:
; This code is incorrect.
; r1 contains the __HANDLE__ pointer
load.b r0, Lock(r1)
test.b r0
bnz .already_locked
inc.b r0
store.b Lock(r1), r0
...
.already_locked:
mov.b r0, #HAL_BUSY
ret
没有什么可以阻止两个线程同时执行加载指令,因此两个线程都观察到要解锁的互斥体。即使只有一个 CPU,当线程 1 在加载和存储之间时,也可能会触发中断,从而导致上下文切换并允许线程 2 在线程 1 执行存储之前执行加载。
为了使互斥锁发挥作用,您必须以某种方式确保两个并发线程 不可能 从 __HANDLE__->Lock
加载 HAL_UNLOCKED
,而这用普通的C是做不到的。事实上,用普通的机器语言是做不到的;您需要使用特殊指令,例如 compare-and-swap.
如果您的编译器实现了 C2011, then you can get at those special instructions using the new feature of atomic types,但我不知道如何做到这一点,我不会写出可能有问题的东西。否则,您需要使用编译器扩展或 hand-written 程序集。
第二个问题是 __HAL_LOCK
没有实现通常称为 "lock" 的操作。如果 "Lock" 不能立即获取锁,它应该 wait,但是 __HAL_LOCK
所做的是 fail。该操作的名称是 "try-lock",宏应该相应地命名。此外,可能导致调用函数 return 的宏被认为是不好的做法。