译文 · 原文: Friday Q&A 2017-10-27: Locks, Thread Safety, and Swift: 2017 Edition · 作者 Mike Ash
原文:https://www.mikeash.com/pyblog/friday-qa-2017-10-27-locks-thread-safety-and-swift-2017-edition.html 发布:2017-10-27 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样
回到 Swift 1 的 “黑暗时代”,我曾写过一篇关于 Swift 中锁和线程安全的文章。随着时间的推移,那篇文章已相当过时,读者 Seth Willits 建议我为现代时代更新它,于是就有了这篇文章!
本文将重复旧文中的一些内容,但做了更新以跟上时代,并讨论了事物的变化。在阅读本文之前,无需先阅读前一篇文章。
锁的简要回顾
锁(或称互斥量,mutex)是一种确保在任何时刻只有一个线程处于代码特定区域内的机制。它们通常用于确保多个访问可变数据结构的线程都能看到其一致的状态。锁有几种类型:
-
阻塞锁(Blocking locks)会让等待其他线程释放锁的线程进入睡眠状态。这是常见行为。
-
自旋锁(Spinlocks)通过忙循环不断检查锁是否已释放。如果等待情况罕见,这种方式更高效;但如果等待常见,则会浪费 CPU 时间。
-
读写锁(reader / writer locks)允许多个” 读取” 线程同时进入临界区,但当一个” 写入” 线程获取锁时,会排斥所有其他线程(包括读取线程)。这种机制很有用,因为许多数据结构在多个线程同时读取时是安全的,但在其他线程读取或写入时进行写入操作则不安全。
-
递归锁(recursive locks)允许单个线程多次获取同一把锁。非递归锁在同一个线程重入时,可能会导致死锁、崩溃或其他异常行为。
APIs
苹果的 API 提供了一系列不同的互斥锁机制。以下是一个较长但不完全的列表:
- pthread_mutex_t。
- pthread_rwlock_t。
- DispatchQueue。
- OperationQueue(配置为串行时)。
- NSLock。
- os_unfair_lock(译注:现代系统底层锁,用于替代已废弃的 OSSpinLock)。
除此之外,Objective-C 还提供了 @synchronized 语言结构,目前该机制基于 pthread_mutex_t 实现。与其他锁不同,@synchronized 不使用显式的锁对象,而是将任意 Objective-C 对象当作锁来使用。一个 @synchronized(someObject) 代码段会阻止任何其他使用相同对象指针的 @synchronized 代码段的访问。这些不同的工具各有不同的行为和特性:
pthread_mutex_t是一种阻塞式锁,可选配置为递归锁。pthread_rwlock_t是一种阻塞式读写锁。DispatchQueue可用作阻塞式锁。通过将其配置为并发队列并使用屏障块(barrier blocks),它也可用作读写锁。它还支持对锁定区域进行异步执行。OperationQueue可用作阻塞式锁。与dispatch_queue_t类似,它支持对锁定区域进行异步执行。NSLock是一个以 Objective-C 类形式实现的阻塞式锁。其伴随类NSRecursiveLock(如其名)是一个递归锁。os_unfair_lock是一种较为底层、相对不复杂的阻塞式锁。
最后,@synchronized 是一个阻塞递归锁。
自旋锁的缺失,我提到自旋锁(spinlock)作为一种锁类型,但这里列出的 API 都不是自旋锁。这是与之前文章相比的一个重大变化,也是我写这篇更新的主要原因。
- H 变为可运行状态,并抢占了 L。
- H 试图获取自旋锁(spinlock),但失败了,因为 L 仍然持有它。
- H 开始愤怒地在自旋锁上自旋,反复尝试获取它,并独占 CPU。
- H 无法继续,直到 L 完成其工作。L 无法完成其工作,除非 H 停止愤怒地在自旋锁上自旋。
- 悲伤。
有办法解决这个问题。例如,在第 4 步中,H 可能会将其优先级捐赠给 L,允许 L 及时完成其工作。可以制作一种能解决这个问题的自旋锁,但苹果的旧自旋锁 API,OSSpinLock,并没有这样做。(译注:OSSpinLock 在现代系统中已被弃用,推荐使用 os_unfair_lock 等替代方案。)这在很长时间内都没问题,因为线程优先级在苹果平台上没有被广泛使用,而且优先级系统使用了动态优先级,防止死锁场景持续太久。最近,服务质量类使得不同优先级更加常见,并使得死锁场景更可能持续。
OSSpinLock(自旋锁)(译注:自 iOS 8 和 macOS 10.10 起已被废弃)长期以来表现良好,但随着 iOS 8 和 macOS 10.10 的发布,它不再是一个好主意。它现在已被正式废弃。替代品是 os_unfair_lock(不公平锁),它作为低级、简单、廉价的锁实现了相同的整体目的,但足够复杂以避免优先级问题。
Value Types 注意,pthread_mutex_t(POSIX 线程互斥锁类型)、pthread_rwlock_t(POSIX 线程读写锁类型)和 os_unfair_lock 是 value types(值类型),不是 reference types(引用类型)。这意味着如果你对它们使用 =,你会创建一个副本。这很重要,因为这些类型不能被复制!如果你复制了一个 pthread 类型,副本将不可用,并且当你尝试使用它时可能会崩溃。处理这些类型的 pthread 函数假设值位于它们初始化时的相同内存地址,之后将它们放在其他地方是个坏主意。os_unfair_lock 不会崩溃,但你会得到一个完全独立的锁,这绝不是你想要的。
如果你使用这些类型,你必须小心永远不要复制它们,无论是显式地使用 = 操作符,还是隐式地,例如,将它们嵌入一个结构体或在一个闭包中捕获它们。
此外,由于锁(locks)本质上是可变对象(mutable objects),这意味着你需要使用 var 而不是 let 来声明它们。
其他的是引用类型(reference types),这意味着它们可以随意传递,并且可以使用 let 来声明。
初始化时,你必须小心 pthread 锁(pthread locks),因为空的 () 初始化器可以创建一个值,但该值不会是一个有效的锁。这些锁必须使用 pthread_mutex_init 或 pthread_rwlock_init 进行单独初始化:
var mutex = pthread_mutex_t() pthread_mutex_init(&mutex, nil)在这些类型上编写扩展来封装初始化很诱人。然而,无法保证初始化器直接作用于变量本身而非其副本。由于这些类型无法安全复制,除非该扩展返回指针或包装器类,否则无法安全编写。
如果使用这些 API,切记在需要销毁锁时调用对应的销毁函数。
DispatchQueue 提供了回调式 API,使其能够自然地安全使用。根据需要受保护代码同步或异步执行,调用 sync 或 async 并传入待执行代码:
queue.sync(execute: { ... }) queue.async(execute: { ... })对于同步情况,该 API 会贴心地捕获受保护代码的返回值,并将其作为同步方法的返回值提供:
let value = queue.sync(execute: { return self.protectedProperty })你甚至可以在保护块内抛出错误,这些错误会传播出去。
OperationQueue 的情况类似,尽管它没有内置方式来传播返回值或错误。你必须自己构建这部分逻辑,或者改用 DispatchQueue。
其他 API 则要求分离的锁定和解锁调用,当你忘记其中一个时可能会很「刺激」。这些调用的形式如下:
pthread_mutex_lock(&mutex) ... pthread_mutex_unlock(&mutex)
nslock.lock() ... nslock.unlock()
os_unfair_lock_lock(&lock) ... os_unfair_lock_unlock(&lock)既然这些 API 几乎相同,我将在后续示例中使用NSLock。其他锁只是名称不同。
当受保护的代码很简单时,这种做法效果很好。但如果代码更复杂呢?例如:
nslock.lock() if earlyExitCondition { return nil } let value = compute() nslock.unlock() return value哎呀,有时你会忘记解锁!这正是制造难以发现的 bug 的好方法。也许你总是严格遵守 return 语句,从不会犯这种错误。但如果你抛出一个错误(error)怎么办?
nslock.lock() guard something else { throw error } let value = compute() nslock.unlock() return value同样的问题!也许你真的很有自律性,也绝不会这样做。那么你就安全了,但即便如此,代码还是有点难看:
nslock.lock() let value = compute() nslock.unlock() return value对此,一个显而易见的修复方案是使用 Swift 的 defer(延迟执行)机制。当你加锁后,立即使用 defer 来安排解锁操作。这样,无论你以何种方式退出代码段,锁都一定会被释放。
nslock.lock() defer { nslock.unlock() } return compute()这段写法适用于提前返回(early returns)、抛出错误(throwing errors)或普通代码场景。
不过每次都要写两行确实有点烦人,因此我们可以把所有逻辑封装到一个基于回调的函数中,就像 DispatchQueue 那样:
func withLocked<T>(_ lock: NSLock, _ f: () throws -> T) rethrows -> T { lock.lock() defer { lock.unlock() } return try f() }
let value = withLocked(lock, { return self.protectedProperty })实现值类型的锁机制时,你必须确保获取的是锁的指针而非锁本身。记住,你不应复制这些对象!使用 pthread 的版本如下所示:
func withLocked<T>(_ mutexPtr: UnsafeMutablePointer<pthread_mutex_t>, _ f: () throws -> T) rethrows -> T { pthread_mutex_lock(mutexPtr) defer { pthread_mutex_unlock(mutexPtr) } return try f() }
let value = withLocked(&mutex, { return self.protectedProperty })选择你的锁 API
DispatchQueue(调度队列)是一个明显受欢迎的选择。它有一个漂亮的 Swift 风格的 API,使用起来令人愉快。Dispatch 库得到了苹果公司的大量关注,这意味着它可以被指望表现良好、工作可靠,并且获得许多酷炫的新功能。
DispatchQueue 允许许多巧妙的高级用途,例如调度定时器或事件源直接在你用作锁的队列上触发,确保处理程序与使用该队列的其他东西同步。设置目标队列的能力允许表达复杂的锁层次结构。自定义并发队列可以轻松用作读写锁(reader-writer locks)。你只需要改变一个字母,就可以在后台线程上异步执行受保护的代码,而不是同步执行。而且这个 API 易于使用且难以误用。这是一个全面的胜利。GCD(Grand Central Dispatch)迅速成为我最喜欢的 API 之一,并且至今仍然是,这是有原因的。
和大多数事物一样,它并非完美无缺。调度队列(dispatch queue)在内存中以对象形式存在,因此存在一些开销。它们缺少某些特定功能,比如条件变量(condition variables)或可递归性(recursiveness)。偶尔也会遇到需要直接调用锁和解锁操作,而非被迫使用基于回调的 API(callback-based API)的情况。通常情况下,DispatchQueue 是正确的选择,如果你不确定该选什么,它是一个出色的默认选项,但偶尔也有使用其他方案的理由。
当单个锁的开销至关重要(因为某些原因你需要管理大量锁)且不需要高级功能时,os_unfair_lock 可能是个好选择。它实现为一个 32 位整数,你可以将其放置在任何需要的位置,因此开销很小。
顾名思义,os_unfair_lock 缺失的特性之一就是公平性(fairness)。锁公平性意味着至少会尝试确保等待锁的不同线程都有机会获取它。缺乏公平性时,一个快速释放并重新获取锁的线程可能会垄断该锁,而其他线程则在等待。
是否会产生问题取决于你的具体应用场景。在某些用例中公平性是必需的,而在另一些场景中则完全无关紧要。os_unfair_lock(不公平锁)因缺乏公平性获得了更好的性能,因此在不需要公平性的场景中能带来优势。
pthread_mutex(POSIX 互斥锁)处于中间位置。它的体积相当大,达到 64 字节,但仍可控制其存储位置。该锁实现了公平性(虽然这是苹果实现的细节而非 API 规范的一部分)。它还提供了多种高级特性,例如使互斥锁可递归的机制,以及复杂的线程优先级控制功能。
pthread_rwlock(POSIX 读写锁)提供读写锁机制。它占用高达 200 字节空间,且未提供太多有趣的特性,因此相比并发派发队列(DispatchQueue)似乎没有太多使用理由。
NSLock 是 pthread_mutex 的封装类。很难为其构思具体用例,但如果你需要显式的加锁 / 解锁调用,又不愿手动初始化和销毁 pthread_mutex 带来的麻烦时,它可能发挥作用。
OperationQueue(操作队列)提供了类似 DispatchQueue 的基于回调的 API,并具备操作间依赖管理等高级功能,但缺少 DispatchQueue 提供的许多其他特性。几乎没有理由将 OperationQueue 用作锁定 API,尽管它在其他方面可能有用。
简而言之:DispatchQueue 可能是正确的选择。在某些特定情况下,os_unfair_lock(非公平锁)可能更优。其余选项通常不是首选。
结论 Swift 没有语言内置的线程同步机制,但相应的 API 弥补了这一不足。GCD 仍是苹果的瑰宝之一,其 Swift API 表现出色。对于极少数不适用的场景,还有许多其他选项可供选择。我们没有 @synchronized 或 atomic 属性,但我们拥有更好的替代方案。
本次讨论到此结束。欢迎下次再来探索更多有趣内容。如果等待期间感到无聊,可以购买我的一本著作!周五 Q & A 栏目由读者创意驱动,若您希望看到某主题被探讨,请将其发送给我们!
Original (English)
Source: https://www.mikeash.com/pyblog/friday-qa-2017-10-27-locks-thread-safety-and-swift-2017-edition.html
Back in the dark ages of Swift 1, I wrote an article about locks and thread safety in Swift. The march of time has made it fairly obsolete, and reader Seth Willits suggested I update it for the modern age, so here it is!
This article will repeat some material from the old one, with changes to bring it up to date, and some discussion of how things have changed. Reading the previous article is not necessary before you read this one.
A Quick Recap on LocksA lock, or mutex, is a construct that ensures only one thread is active in a given region of code at any time. They’re typically used to ensure that multiple threads accessing a mutable data structure all see a consistent view of it. There are several kinds of locks:
-
Blocking locks sleep a thread while it waits for another thread to release the lock. This is the usual behavior.
-
Spinlocks use a busy loop to constantly check to see if a lock has been released. This is more efficient if waiting is rare, but wastes CPU time if waiting is common.
-
Reader/writer locks allow multiple “reader” threads to enter a region simultaneously, but exclude all other threads (including readers) when a “writer” thread acquires the lock. This can be useful as many data structures are safe to read from multiple threads simultaneously, but unsafe to write while other threads are either reading or writing.
-
Recursive locks allow a single thread to acquire the same lock multiple times. Non-recursive locks can deadlock, crash, or otherwise misbehave when re-entered from the same thread.
APIsApple’s APIs have a bunch of different mutex facilities. This is a long but not exhaustive list:
-
pthread_mutex_t.
-
pthread_rwlock_t.
-
DispatchQueue.
-
OperationQueue when configured to be serial.
-
NSLock.
-
os_unfair_lock.
In addition to this, Objective-C provides the @synchronized language construct, which at the moment is implemented on top of pthread_mutex_t. Unlike the others, @synchronized doesn’t use an explicit lock object, but rather treats an arbitrary Objective-C object as if it were a lock. A @synchronized(someObject) section will block access to any other @synchronized sections that use the same object pointer. These different facilities all have different behaviors and capabilities:
-
pthread_mutex_t is a blocking lock that can optionally be configured as a recursive lock.
-
pthread_rwlock_t is a blocking reader/writer lock.
-
DispatchQueue can be used as a blocking lock. It can be used as a reader/writer lock by configuring it as a concurrent queue and using barrier blocks. It also supports asynchronous execution of the locked region.
-
OperationQueue can be used as a blocking lock. Like dispatch_queue_t it supports asynchronous execution of the locked region.
-
NSLock is blocking lock as an Objective-C class. Its companion class NSRecursiveLock is a recursive lock, as the name indicates.
-
os_unfair_lock is a less sophisticated, lower-level blocking lock.
Finally, @synchronized is a blocking recursive lock.
Spinlocks, Lack ofI mentioned spinlocks as one type of lock, but none of the APIs listed here are spinlocks. This is a big change from the previous article, and is the main reason I’m writing this update.
Spinlocks are really simple, and are efficient in the right circumstances. Unfortunately, they’re a little too simple for the complexities of the modern world.
The problem is thread priorities. When there are more runnable threads than CPU cores, higher priority threads get preference. This is a useful notion, because CPU cores are always a limited resource, and you don’t want some time-insensitive background network operation stealing time from your UI while the user is trying to use it.
When a high-priority thread gets stuck and has to wait for a low-priority thread to finish some work, but the high-priority thread prevents the low-priority thread from actually performing that work, it can result in long hangs or even a permanent deadlock.
The deadlock scenario looks like this, where H is a high-priority thread and L is a low-priority thread:
-
L acquires the spinlock.
-
L starts doing some work.
-
H becomes ready to run, and preempts L.
-
H attempts to acquire the spinlock, but fails, because L still holds it.
-
H begins angrily spinning on the spinlock, repeatedly trying to acquire it, and monopolizing the CPU.
-
H can’t proceed until L finishes its work. L can’t finish its work unless H stops angrily spinning on the spinlock.
-
Sadness.
There are ways to solve this problem. For example, H might donate its priority to L in step 4, allowing L to complete its work in a timely fashion. It’s possible to make a spinlock that solves this problem, but Apple’s old spinlock API, OSSpinLock, doesn’t.
This was fine for a long time, because thread priorities didn’t get much use on Apple’s platforms, and the priority system used dynamic priorities that kept the deadlock scenario from persisting too long. More recently, quality of service classes made different priorities more common, and made the deadlock scenario more likely to persist.
OSSpinLock, which did a fine job for so long, stopped being a good idea with the release of iOS 8 and macOS 10.10. It’s now been formally deprecated. The replacement is os_unfair_lock, which fills the same overall purpose as a low-level, unsophisticated, cheap lock, but is sufficiently sophisticated to avoid problems with priorities.
Value TypesNote that pthread_mutex_t, pthread_rwlock_t, and os_unfair_lock are value types, not reference types. That means that if you use = on them, you make a copy. This is important, because these types can’t be copied! If you copy one of the pthread types, the copy will be unusable and may crash when you try to use it. The pthread functions that work with these types assume that the values are at the same memory addresses as where they were initialized, and putting them somewhere else afterwards is a bad idea. os_unfair_lock won’t crash, but you get a completely separate lock out of it which is never what you want.
If you use these types, you must be careful never to copy them, whether explicitly with a = operator, or implicitly by, for example, embedding them in a struct or capturing them in a closure.
Additionally, since locks are inherently mutable objects, this means you need to declare them with var instead of let.
The others are reference types, meaning they can be passed around at will, and can be declared with let.
InitializationYou must be careful with the pthread locks, because you can create a value using the empty () initializer, but that value won’t be a valid lock. These locks must be separately initialized using pthread_mutex_init or pthread_rwlock_init:
var mutex = pthread_mutex_t() pthread_mutex_init(&mutex, nil)It’s tempting to write an extension on these types which wraps up the initialization. However, there’s no guarantee that initializers work on the variable directly, rather than on a copy. Since these types can’t be safely copied, such an extension can’t be safely written unless you have it return a pointer or a wrapper class.
If you use these APIs, don’t forget to call the corresponding destroy function when it’s time to dispose of the lock.
UseDispatchQueue has a callback-based API which makes it natural to use it safely. Depending on whether you need the protected code to run synchronously or asynchronously, call sync or async and pass it the code to run:
queue.sync(execute: { ... }) queue.async(execute: { ... })For the sync case, the API is nice enough to capture the return value from the protected code and provide it as the return value of the sync method:
let value = queue.sync(execute: { return self.protectedProperty })You can even throw errors inside the protected block and they’ll propagate out.
OperationQueue is similar, although it doesn’t have a built-in way to propogate return values or errors. You’ll have to build that yourself, or use DispatchQueue instead.
The other APIs require separate locking and unlocking calls, which can be exciting when you forget one of them. The calls look like this:
pthread_mutex_lock(&mutex) ... pthread_mutex_unlock(&mutex)
nslock.lock() ... nslock.unlock()
os_unfair_lock_lock(&lock) ... os_unfair_lock_unlock(&lock)Since the APIs are virtually identical, I’ll use nslock for further examples. The others are the same, but with different names.
When the protected code is simple, this works well. But what if it’s more complicated? For example:
nslock.lock() if earlyExitCondition { return nil } let value = compute() nslock.unlock() return valueOops, sometimes you don’t unlock the lock! This is a good way to make hard-to-find bugs. Maybe you’re always disciplined with your return statements and never do this. What if you throw an error?
nslock.lock() guard something else { throw error } let value = compute() nslock.unlock() return valueSame problem! Maybe you’re really disciplined and would never do this either. Then you’re safe, but even then the code is a bit ugly:
nslock.lock() let value = compute() nslock.unlock() return valueThe obvious fix for this is to use Swift’s defer mechanism. The moment you lock, defer the unlock. Then no matter how you exit the code, the lock will be released:
nslock.lock() defer { nslock.unlock() } return compute()This works for early returns, throwing errors, or just normal code.
It’s still annoying to have to write two lines, so we can wrap everything up in a callback-based function like DispatchQueue has:
func withLocked<T>(_ lock: NSLock, _ f: () throws -> T) rethrows -> T { lock.lock() defer { lock.unlock() } return try f() }
let value = withLocked(lock, { return self.protectedProperty })When implementing this for value types, you’ll need to be sure to take a pointer to the lock rather than the lock itself. Remember, you don’t want to copy these things! The pthread version would look like this:
func withLocked<T>(_ mutexPtr: UnsafeMutablePointer<pthread_mutex_t>, _ f: () throws -> T) rethrows -> T { pthread_mutex_lock(mutexPtr) defer { pthread_mutex_unlock(mutexPtr) } return try f() }
let value = withLocked(&mutex, { return self.protectedProperty })Choosing Your Lock APIDispatchQueue is an obvious favorite. It has a nice Swifty API and is pleasant to use. The Dispatch library gets a huge amount of attention from Apple, and that means that it can be counted on to perform well, work reliably, and get lots of cool new features.
DispatchQueue allows for a lot of nifty advanced uses, such as scheduling timers or event sources to fire directly on the queue you’re using as a lock, ensuring that the handlers are synchronized with other things using the queue. The ability to set target queues allows expressing complex lock hierarchies. Custom concurrent queues can be easily used as reader-writer locks. You only have to change a single letter to execute protected code asynchronously on a background thread rather than synchronously. And the API is easy to use and hard to misuse. It’s a win all around. There’s a reason GCD quickly became one of my favorite APIs, and remains one to this day.
Like most things, it’s not perfect. A dispatch queue is represented by an object in memory, so there’s a bit of overhead. They’re missing some niche features, like condition variables or recursiveness. Every once in a great while, it’s useful to be able to make individual lock and unlock calls rather than be forced to use a callback-based API. DispatchQueue is usually the right choice, and is a great default if you don’t know what to pick, but there are occasionally reasons to use others.
os_unfair_lock can be a good choice when per-lock overhead is important (because for some reason you have a huge number of them) and you don’t need fancy features. It’s implemented as a single 32-bit integer which you can place wherever you need it, so overhead is small.
As the name hints, one of the features that os_unfair_lock is missing is fairness. Lock fairness means that there’s at least some attempt to ensure that different threads waiting on a lock all get a chance to acquire it. Without fairness, it’s possible for a thread that rapidly releases and re-acquires the lock to monopolize it while other threads are waiting.
Whether or not this is a problem depends on what you’re doing. There are some use cases where fairness is necessary, and some where it doesn’t matter at all. The lack of fairness allows os_unfair_lock to have better performance, so it can provide an edge in cases where fairness isn’t needed.
pthread_mutex is somewhere in the middle. It’s considerably larger than os_unfair_lock, at 64 bytes, but you can still control where it’s stored. It implements fairness, although this is a detail of Apple’s implementation, not part of the API spec. It also provides various other advanced features, such as the ability to make the mutex recursive, and fancy thread priority stuff.
pthread_rwlock provides a reader/writer lock. It takes up a whopping 200 bytes and doesn’t provide much in the way of interesting features, so there doesn’t seem to be much reason to use it over a concurrent DispatchQueue.
NSLock is a wrapper around pthread_mutex. It’s hard to come up with a use case for this, but it could be useful if you need explicit lock/unlock calls but don’t want the hassle of manually initializing and destroying a pthread_mutex.
OperationQueue offers callback-based API like DispatchQueue, with some advanced features for things like dependency management between operations, but without many of the other features offered by DispatchQueue. There is little reason to use OperationQueue as a locking API, although it can be useful for other things.
In short: DispatchQueue is probably the right choice. In certain circumstances, os_unfair_lock may be better. The others are usually not the ones to use.
ConclusionSwift has no language facilities for thread synchronization, but the APIs make up for it. GCD remains one of Apple’s crown jewels, and the Swift API for it is great. For the rare occasions where it’s not suitable, there are many other options to choose from. We don’t have @synchronized or atomic properties, but we have things that are better.
That wraps it up for this time. Check back again for more fun stuff. If you get bored in the meantime, buy one of my books! Friday Q&A is driven by reader ideas, so if you have a topic you’d like to see covered here, please send it in!