Skip to content

Commit

Permalink
RxSwift 利用线程特有数据(TSD)解决循环调用的问题
Browse files Browse the repository at this point in the history
  • Loading branch information
fuyoufang committed Apr 4, 2018
1 parent eabd2e7 commit 7f55dfd
Show file tree
Hide file tree
Showing 6 changed files with 326 additions and 3 deletions.
102 changes: 100 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,70 @@
# Created by https://www.gitignore.io/api/macos

# Created by https://www.gitignore.io/api/xcode,macos,appcode

### AppCode ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries

# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml

# Gradle:
.idea/**/gradle.xml
.idea/**/libraries

# CMake
cmake-build-debug/

# Mongo Explorer plugin:
.idea/**/mongoSettings.xml

## File-based project format:
*.iws

## Plugin-specific files:

# IntelliJ
/out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# Ruby plugin and RubyMine
/.rakeTasks

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

### AppCode Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721

# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr

# Sonarlint plugin
.idea/sonarlint

### macOS ###
*.DS_Store
Expand Down Expand Up @@ -27,5 +93,37 @@ Network Trash Folder
Temporary Items
.apdisk

### Xcode ###
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## Build generated
build/
DerivedData/

## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/

## Other
*.moved-aside
*.xccheckout
*.xcscmblueprint

### Xcode Patch ###
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcworkspace/contents.xcworkspacedata
/*.gcno


# End of https://www.gitignore.io/api/macos
# End of https://www.gitignore.io/api/xcode,macos,appcode
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

| 主题 | 文章 | 备注 |
|:-------:|:------|:----:|
|RxSwift 源码阅读|[RxSwift Queue 队列的实现](./articles/RxSwift-Queue.md)<br>[RxSwift PriorityQueue 优先级队列的实现](./articles/RxSwift-PriorityQueue.md)||
|RxSwift 源码阅读|[RxSwift Queue 队列的实现](./articles/RxSwift-Queue.md)<br>[RxSwift PriorityQueue 优先级队列的实现](./articles/RxSwift-PriorityQueue.md)<br>[RxSwift 利用线程特有数据(TSD)解决循环调用的问题](./articles/RxSwift_TSD.md)||


## 操作系统
Expand Down
160 changes: 160 additions & 0 deletions articles/RxSwift_TSD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# RxSwift 利用线程特有数据(TSD)解决循环调用的问题

`RxSwift` 框架的 `CurrentThreadScheduler.swift` 文件中定义了 `CurrentThreadScheduler` 类,因为需要符合 `ImmediateSchedulerType` 协议,所有实现了下面的方法:
```
public func schedule<StateType>(_ state: StateType, action: @escaping (StateType) -> Disposable) -> Disposable
```
这个方法就是在当前线程中派发一个需要被立即执行的任务,其中

- 参数 state: 任务将被执行的状态
- 参数 action: 被执行的任务
- 返回值:可以取消调度操作的 disposable 类

在需要被执行的 `action` 中,我们依然可以继续调用 `schedule` 方法,继续向当前线程派发一个需要被立即执行的任务。

我们思考一个问题,`action``schedule` 方法中被执行,如果再在 `action` 中调用 `schedule` 方法,这样会不会造成**循环调用**呢?答案显而易见,这样势必会造成**循环调用**

为了**避免**循环引用 `RxSwift` 的解决方法是:在 `schedule` 方法中,在执行 `action` 之前,首先判断当前线程中之前是否有调用过 `schedule` 方法,并且还没有执行结束,如果有则先将 `action` 保存到和线程关联的队列中,如果没有则直接执行 `action`,执行结束后查看和线程关联的队列中是否有未执行的 `action`

那么怎样判读一个线程是否正在执行一个方法呢?这里需要引入**线程特有数据**`Thread Specific Data``TSD`)。

在具体分析 RxSwift 的实现之前,可以先了解一下[Swift 中的指针使用](https://onevcat.com/2015/01/swift-pointer/)[线程特有数据(Thread Specific Data)](https://github.com/FuYouFang/fuyoufangBlog/blob/master/articles/Thread_Specific_Data.md)

### RxSWift 中的具体实现
#### 获取线程特有数据的 key
在保存线程特有数据(TSD)之前,需要获取线程特有数据的 key。
```
private static var isScheduleRequiredKey: pthread_key_t = { () -> pthread_key_t in
// 1.分配一个 pthread_key_t 的内存空间,返回指向 pthread_key_t 的指针
// 这是 key 是一个指向 pthread_key_t 类型的指针
let key = UnsafeMutablePointer<pthread_key_t>.allocate(capacity: 1)
defer {
// 4. 释放保存 pthread_key_t 的空间
key.deallocate(capacity: 1)
}
// 2. 创建线程特有数据的 key
guard pthread_key_create(key, nil) == 0 else {
rxFatalError("isScheduleRequired key creation failed")
}
// 3. 返回线程特有数据的 key
return key.pointee
}()
```
**注意:**
1. `pthread_key_create` 并不会为线程特有数据的 `key` 申请内存空间,所以我们需要自己申请一个 `pthread_key_t` 的内存空间。
2. 这是一个静态(`static`)的属性,它会保存创建后的线程特有数据的 `key`,所以可以将申请的内存空间进行释放,而不会丢失线程特有数据的 `key`
> 这里说的线程特有数据的 `key` 和代码中的 `key` 不是一个东西,代码中的 `key` 是一个指针,指针指向的内容为线程特有数据的 `key`
总结一下获取一个线程特有数据的 `key` 的过程:
1. 分配一个 `pthread_key_t` 的内存空间(线程特有数据的 `key` 的类型);
2. 调用 `pthread_key_create` 创建线程特有数据的 key,
3. 保存线程特有数据的 key,并释放之前分配的 `pthread_key_t` 的空间释放。

#### 设置线程特有数据
获取到线程特有数据 key 之后,我们就可以设置和读取 key 对应的线程特有数据。
```
/// 返回一个值,用来表示调用者是否必须调用 `schedule` 方法
public static fileprivate(set) var isScheduleRequired: Bool {
get {
// 获取线程特有信息
return pthread_getspecific(CurrentThreadScheduler.isScheduleRequiredKey) == nil
}
set(isScheduleRequired) {
// 设置线程特有信息
if pthread_setspecific(CurrentThreadScheduler.isScheduleRequiredKey, isScheduleRequired ? nil : scheduleInProgressSentinel) != 0 {
rxFatalError("pthread_setspecific failed")
}
}
}
private static var scheduleInProgressSentinel: UnsafeRawPointer = { () -> UnsafeRawPointer in
return UnsafeRawPointer(UnsafeMutablePointer<Int>.allocate(capacity: 1))
}()
```
`isScheduleRequired` 就是通过在线程特有数据的 key 设置或清除一个 Int 指针来保存 true 或 false。

#### schedule 方法的实现
下面看 `schedule` 的具体实现:
```
public func schedule<StateType>(_ state: StateType, action: @escaping (StateType) -> Disposable) -> Disposable {
// 本次调用 schedule 是否需要派发 action
// 也就是当前线程之前有没有调用过 schedule,并且没有执行完。
if CurrentThreadScheduler.isScheduleRequired {
// 修改状态
CurrentThreadScheduler.isScheduleRequired = false
// 派发 action
let disposable = action(state)
defer {
CurrentThreadScheduler.isScheduleRequired = true
CurrentThreadScheduler.queue = nil
}
// 查看和当前线程关联的队列 queue 中是否有未派发的 action,如果有则执行
guard let queue = CurrentThreadScheduler.queue else {
return disposable
}
while let latest = queue.value.dequeue() {
if latest.isDisposed {
continue
}
latest.invoke()
}
return disposable
}
// 将 action 先保存到和当前线程关联的队列 queue 中
let existingQueue = CurrentThreadScheduler.queue
let queue: RxMutableBox<Queue<ScheduledItemType>>
if let existingQueue = existingQueue {
queue = existingQueue
} else {
queue = RxMutableBox(Queue<ScheduledItemType>(capacity: 1))
CurrentThreadScheduler.queue = queue
}
let scheduledItem = ScheduledItem(action: action, state: state)
queue.value.enqueue(scheduledItem)
return scheduledItem
}
```
#### 最终的效果
下面是 `CurrentThreadSchedulerTest` 测试用例当中对 `CurrentThreadScheduler` 的测试,我们看 `schedule` 达到的效果:

```
func testCurrentThreadScheduler_basicScenario() {
XCTAssertTrue(CurrentThreadScheduler.isScheduleRequired)
var messages = [Int]()
_ = CurrentThreadScheduler.instance.schedule(()) { s in
messages.append(1)
_ = CurrentThreadScheduler.instance.schedule(()) { s in
messages.append(3)
_ = CurrentThreadScheduler.instance.schedule(()) {
messages.append(5)
return Disposables.create()
}
messages.append(4)
return Disposables.create()
}
messages.append(2)
return Disposables.create()
}
XCTAssertEqual(messages, [1, 2, 3, 4, 5])
}
```
可见在 `schedule` 方法中执行的 `action` 依然可以调用 `schedule` 而不会造成循环调用。





54 changes: 54 additions & 0 deletions code/timer.playground/Contents.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//: Playground - noun: a place where people can play

import UIKit

enum Type {
case tick
case dispatch
}

class Timer {
typealias TimerAction = ()->()
private var action: TimerAction
private var index: Int = 0

init(action: @escaping TimerAction) {
self.action = action
}

func start(interval: TimeInterval) {
self.dispatch(type: .tick, interval: interval)
}

func dispatch(type: Type, interval: TimeInterval) {
switch type {
case .tick:
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + interval, execute: {
self.dispatch(type: .tick, interval: interval)
})
index += 1
if index == 1 {
dispatch(type: .dispatch, interval: interval)
}
case .dispatch:
action()
index -= 1
if index > 0 {
dispatch(type: .dispatch, interval: 0)
}
}
}
}


var timer = Timer.init {
let now = NSDate()
print(now)
}
timer.start(interval: 1)

func test() {

}

test()
4 changes: 4 additions & 0 deletions code/timer.playground/contents.xcplayground
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='5.0' target-platform='ios'>
<timeline fileName='timeline.xctimeline'/>
</playground>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 7f55dfd

Please sign in to comment.