|
1 | 1 | # ThreadLocal |
2 | 2 |
|
| 3 | +我们知道,同一进程的多个线程之间是内存共享的,这意味着,当一个线程对全局变量做了修改,将会影响到其他所有线程,这是很危险的。为了避免多个线程同时修改全局变量,我们就需要对全局变量的修改加锁。 |
| 4 | + |
| 5 | +除了对全局变量的修改进行加锁,你可能也想到了可以使用线程自己的局部变量,因为局部变量只有线程自己能看见,对同一进程的其他线程是不可访问的。确实如此,让我们先看一个例子: |
| 6 | + |
| 7 | +```python |
| 8 | +from threading import Thread, current_thread |
| 9 | + |
| 10 | +def echo(num): |
| 11 | + print current_thread().name, num |
| 12 | + |
| 13 | +def calc(): |
| 14 | + print 'thread %s is running...' % current_thread().name |
| 15 | + local_num = 0 |
| 16 | + for _ in xrange(10000): |
| 17 | + local_num += 1 |
| 18 | + echo(local_num) |
| 19 | + print 'thread %s ended.' % current_thread().name |
| 20 | + |
| 21 | +if __name__ == '__main__': |
| 22 | + print 'thread %s is running...' % current_thread().name |
| 23 | + |
| 24 | + threads = [] |
| 25 | + for i in range(5): |
| 26 | + threads.append(Thread(target=calc)) |
| 27 | + threads[i].start() |
| 28 | + for i in range(5): |
| 29 | + threads[i].join() |
| 30 | + |
| 31 | + print 'thread %s ended.' % current_thread().name |
| 32 | +``` |
| 33 | + |
| 34 | +在上面的代码中,我们创建了 5 个线程,每个线程都对自己的局部变量 local_num 进行 10000 次的加 1 操作。由于对线程局部变量的修改不会影响到其他线程,因此,我们可以看到,每个线程结束时打印的 local_num 的值都为 10000,执行结果如下: |
| 35 | + |
| 36 | +```python |
| 37 | +thread MainThread is running... |
| 38 | +thread Thread-4 is running... |
| 39 | +Thread-4 10000 |
| 40 | +thread Thread-4 ended. |
| 41 | +thread Thread-5 is running... |
| 42 | +Thread-5 10000 |
| 43 | +thread Thread-5 ended. |
| 44 | +thread Thread-6 is running... |
| 45 | +Thread-6 10000 |
| 46 | +thread Thread-6 ended. |
| 47 | +thread Thread-7 is running... |
| 48 | +Thread-7 10000 |
| 49 | +thread Thread-7 ended. |
| 50 | +thread Thread-8 is running... |
| 51 | +Thread-8 10000 |
| 52 | +thread Thread-8 ended. |
| 53 | +thread MainThread ended. |
| 54 | +``` |
| 55 | + |
| 56 | +上面这种**线程使用自己的局部变量**的方法虽然可以避免多线程对同一变量的访问冲突,但还是有一些问题。在实际的开发中,我们会调用很多函数,每个函数又有很多个局部变量,这时每个函数都这么传参数显然是不可取的。 |
| 57 | + |
| 58 | +为了解决这个问题,一个比较容易想到的做法就是创建一个全局字典,以线程的 ID 作为 key,线程的局部数据作为 value,这样就可以消除函数传参的问题,代码如下: |
| 59 | + |
| 60 | +```python |
| 61 | +from threading import Thread, current_thread |
| 62 | + |
| 63 | +global_dict = {} |
| 64 | + |
| 65 | +def echo(): |
| 66 | + num = global_dict[current_thread()] # 线程根据自己的 ID 获取数据 |
| 67 | + print current_thread().name, num |
| 68 | + |
| 69 | +def calc(): |
| 70 | + print 'thread %s is running...' % current_thread().name |
| 71 | + |
| 72 | + global_dict[current_thread()] = 0 |
| 73 | + for _ in xrange(10000): |
| 74 | + global_dict[current_thread()] += 1 |
| 75 | + echo() |
| 76 | + |
| 77 | + print 'thread %s ended.' % current_thread().name |
| 78 | + |
| 79 | +if __name__ == '__main__': |
| 80 | + print 'thread %s is running...' % current_thread().name |
| 81 | + |
| 82 | + threads = [] |
| 83 | + for i in range(5): |
| 84 | + threads.append(Thread(target=calc)) |
| 85 | + threads[i].start() |
| 86 | + for i in range(5): |
| 87 | + threads[i].join() |
| 88 | + |
| 89 | + print 'thread %s ended.' % current_thread().name |
| 90 | +``` |
| 91 | + |
| 92 | +看下执行结果: |
| 93 | + |
| 94 | +``` |
| 95 | +thread MainThread is running... |
| 96 | +thread Thread-64 is running... |
| 97 | +thread Thread-65 is running... |
| 98 | +thread Thread-66 is running... |
| 99 | +thread Thread-67 is running... |
| 100 | +thread Thread-68 is running... |
| 101 | +Thread-67 10000 |
| 102 | +thread Thread-67 ended. |
| 103 | +Thread-65 10000 |
| 104 | +thread Thread-65 ended. |
| 105 | +Thread-68 10000 |
| 106 | +thread Thread-68 ended. |
| 107 | +Thread-66 10000 |
| 108 | +thread Thread-66 ended. |
| 109 | +Thread-64 10000 |
| 110 | +thread Thread-64 ended. |
| 111 | +thread MainThread ended. |
| 112 | +``` |
| 113 | + |
| 114 | +上面的做法虽然消除了函数传参的问题,但是还是有些不完美,为了获取线程的局部数据,我们需要先获取线程 ID,另外,global_dict 是个全局变量,所有线程都可以对它进行修改,还是有些危险。 |
| 115 | + |
| 116 | +那到底如何是好? |
| 117 | + |
| 118 | +事实上,Python 提供了 ThreadLocal 对象,它真正做到了线程之间的数据隔离,而且不用查找 dict,代码如下: |
| 119 | + |
| 120 | +```python |
| 121 | +from threading import Thread, current_thread, local |
| 122 | + |
| 123 | +global_data = local() |
| 124 | + |
| 125 | +def echo(): |
| 126 | + num = global_data.num |
| 127 | + print current_thread().name, num |
| 128 | + |
| 129 | +def calc(): |
| 130 | + print 'thread %s is running...' % current_thread().name |
| 131 | + |
| 132 | + global_data.num = 0 |
| 133 | + for _ in xrange(10000): |
| 134 | + global_data.num += 1 |
| 135 | + echo() |
| 136 | + |
| 137 | + print 'thread %s ended.' % current_thread().name |
| 138 | + |
| 139 | +if __name__ == '__main__': |
| 140 | + print 'thread %s is running...' % current_thread().name |
| 141 | + |
| 142 | + threads = [] |
| 143 | + for i in range(5): |
| 144 | + threads.append(Thread(target=calc)) |
| 145 | + threads[i].start() |
| 146 | + for i in range(5): |
| 147 | + threads[i].join() |
| 148 | + |
| 149 | + print 'thread %s ended.' % current_thread().name |
| 150 | +``` |
| 151 | + |
| 152 | +在上面的代码中,global_data 就是 ThreadLocal 对象,你可以把它当作一个全局变量,但它的每个属性,比如 `global_data.num` 都是线程的局部变量,没有访问冲突的问题。 |
| 153 | + |
| 154 | +让我们看下执行结果: |
| 155 | + |
| 156 | +``` |
| 157 | +thread MainThread is running... |
| 158 | +thread Thread-94 is running... |
| 159 | +thread Thread-95 is running... |
| 160 | +thread Thread-96 is running... |
| 161 | +thread Thread-97 is running... |
| 162 | +thread Thread-98 is running... |
| 163 | +Thread-96 10000 |
| 164 | +thread Thread-96 ended. |
| 165 | +Thread-97 10000 |
| 166 | +thread Thread-97 ended. |
| 167 | +Thread-95 10000 |
| 168 | +thread Thread-95 ended. |
| 169 | +Thread-98 10000 |
| 170 | +thread Thread-98 ended. |
| 171 | +Thread-94 10000 |
| 172 | +thread Thread-94 ended. |
| 173 | +thread MainThread ended. |
| 174 | +``` |
| 175 | + |
| 176 | +# 小结 |
| 177 | + |
| 178 | +- 使用 ThreadLocal 对象来线程绑定自己独有的数据。 |
| 179 | + |
| 180 | +# 参考资料 |
| 181 | + |
| 182 | +- [ThreadLocal - 廖雪峰的官方网站](http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001386832845200f6513494f0c64bd882f25818a0281e80000) |
| 183 | +- [深入理解Python中的ThreadLocal变量(上) | Just For Fun](http://selfboot.cn/2016/08/22/threadlocal_overview/) |
| 184 | +- [Python线程同步机制 | Python见闻志](https://harveyqing.gitbooks.io/python-read-and-write/content/python_advance/python_thread_sync.html) |
| 185 | + |
| 186 | + |
0 commit comments