线程同步与线程通信

多线程是有趣的事情,它很容易突然出现”错误情况”,这是由于系统的线程调度具有一定的随机性造成的.不过,即使程序偶然出现问题,那么是由于编程不当所引起的.当使用多个线程来访问同一个数据时,很容易”偶然”出现线程安全问题.

线程安全问题

关于线程安全问题,有一个经典的问题:银行取钱的问题.银行取钱的基本流程基本可以分为如下几个步骤.

  1. 用户输入账户、密码、系统判断用户的账户、密码是否匹配。
  2. 用户输入取款金额.
  3. 系统判断账户余额是否大于取款金额.
  4. 如果余额大于取款金额,则取款成功;如果余额小于曲矿金额,则取款失败.

我们不管检查账户和密码的操作,仅仅模拟了后面3步骤操作.

下面定义一个账户类,该账户类封装了账户编号和账户余额两个属性.

  1 LCAccount.h
  2
  3 @interface LCAccount : NSObject
  4
  5 // 封装账户编号、账户余额两个属性
  6
  7 @property (nonatomic, copy)NSString* accountNO;// 账户编号
  8
  9 @property (nonatomic, readonly)CGFloat balance;// 账户余额
 10
 11 - (id)initWithAccountNo:(NSString*)accountNo  balance:(CGFloat)balance;
 12
 13 - (void)draw:(CGFloat)drawAmount;
 14
 15 @end
 16
 17 该LCAccount类还需要提供一个draw:方法,该方法用于从该账号中取钱。
 18
 19  LCAccount.m
 20
 21 @implementation LCAccount
 22
 23 - (id)initWithAccountNo:(NSString *)aAccount balance:(CGFloat)aBalance
 24
 25 {
 26
 27   self = [super init];
 28
 29   if(self)
 30
 31 {
 32
 33   _accountNo = aAccount;
 34
 35   _balance = aBalance;
 36
 37 }
 38
 39 return self;
 40
 41 }
 42
 43 // 提供了一个draw方法来完成取钱操作
 44
 45 - (void)draw:(CGFloat)drawAmount
 46
 47 {
 48
 49    // 账户余额大于取钱数目
 50
 51   if(self.balance >= drawAmount)
 52
 53   {
 54
 55 // 吐出钞票
 56
 57 NSLog(@”%@取钱成功!吐出钱票:%g”, [NSThread currentThread].name , drawAmount);// ①
 58
 59 //  [NSThread sleepForTimeInterval:0.001 ];
 60
 61 // 修改余额
 62
 63 _balance = _balance – drawAmount;
 64
 65 NSLog(@”\t 余额为:%g”, self.balance);
 66
 67 }
 68
 69 else
 70
 71 {
 72
 73   NSLog(@”%@取钱失败!余额不足!”,  [NSThread currentThread].name);// ②
 74
 75 }
 76
 77 }
 78
 79 - (NSUInteger) hash
 80
 81 {
 82
 83   return [self.accountNo hash];
 84
 85 }
 86
 87 - (BOOL)isEqual:(id)anObject
 88
 89 {
 90
 91   if(self == anObject)
 92
 93    return YES;
 94
 95   if(anObject != nil
 96
 97 && [anObject class] == [LCAccount class])
 98
 99 {
100
101   LCAccount* target = (LCAccount*)anObject;
102
103   return [target.accountNo isEqualToString:self.accountNo];
104
105 }
106
107 return NO;
108
109 }
110
111 @end
112
113
114
115 LCViewController.m
116
117 @implementation LCViewController
118
119 LCAccount* account;
120
121 - (void)viewDidLoad
122
123 {
124
125   [super viewDidLoad];
126
127   // 创建一个账号
128
129   account = [[LCAccount alloc] initWithAccountNo:@”321231” balance: 1000.0 ];
130
131 }
132
133 - (IBAction)draw:(id)sender
134
135 {
136
137    // 创建第1个线程对象
138
139   NSThread* thread1 = [[NSThread alloc] initWithTarget:self
140
141 selector:@selector(drawMethod:)
142
143 object:[NSNumber numberWithInt:800]];
144
145 // 创建第2个线程对象
146
147   NSThread* thread2 = [[NSThread alloc] initWithTarget:self
148
149 selector:@selector(drawMethod:)
150
151 object:[NSNumber numberWithInt:800]];
152
153 // 启动两条线程
154
155 [thread1 start];
156
157 [thread2 start];
158
159
160
161 }
162
163 - (void)drawMethod:(NSNumber *)drawAmount
164
165 {
166
167    // 直接调用accont对象的draw方法来执行取钱操作
168
169   [account draw:drawAmount.doubleValue];
170
171 }
172
173 @end

按照正常的执行逻辑,应该是第1个线程可以取到钱,第2线程显示”余额不足”.但上图所示的运行结果并不是期望的结(不过也有可能看到运行正确的结果),这正是多线程编程突然出现的”偶然”错误----因为线程调度的不确定性.

使用@synchronized实现同步

为了解决”线程执行体的方法不具备同步安全性”的问题,Objective—C的多线程支持引入了同步,使用同步的通用方法就是@synchronized修饰代码块,被@synchornized修饰的代码块可简称为同步代码块.

同步代码块的语法如下:

@synchronized(obj)

{

// 此处的代码就是同步代码块

}

上面语法格式中,@synchronized后面括号里的obj就是同步监视器.上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定.

注意:任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对同步监视器的锁定.虽然Objective-C允许使用任何对象作为同步监视器,但想一下同步监视器的目的----阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器.

对于上面的取钱模拟程序,我们应该考虑使用账户(LCAccount对象)作为同步监视器.只要我们把LCAccount类的draw:方法修改如下形式即可.

 1 // 提供一个线程安全的draw方法来完成取钱操作
 2
 3 - (void) draw:(CGFloat)drawAmount
 4
 5 {
 6
 7   // 使用self作为同步监视器,任何线程进入下面的同步代码块之前
 8
 9   // 必须先获得self账户的锁定----其他线程无法获得锁,也就无法修改它
10
11   // 这种做法符合”加锁 →修改→释放锁”的逻辑
12
13   @synchronized(self)
14
15 {
16
17    // 账户余额大于取钱数目
18
19    if(self.balance >= drawAmount)
20
21    {
22
23      // 吐出钞票
24
25      NSLog(@”%@取钱成功! 吐出钞票:%g”, [NSThread currentThread].name , drawAmount);
26
27 [NSThread  sleepForTimeInterval:0.001];
28
29 // 修改余额
30
31 _balance = _balance – drawAmount;
32
33 NSLog(@”\tyue为: %g”, self.balance);
34
35 }
36
37 else
38
39 {
40
41   NSLog(@”%@取钱失败!余额不足!”, [NSThread currentThread].name);
42
43 }
44
45 }// 同步代码块结束,该线程释放同步锁
46
47 }

说明

上面程序使用@synchronized将draw:方法的方法体修改成同步代码块,该同步代码块的同步监视器是LCAccount对象本声,这样做法符合”加锁→修改→释放锁”的逻辑,任何线程在修改制定资源之前,首先都要对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成后,释放对该资源的锁定.通过这种方式就可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(也称为临界区),所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全性.

特征

通过这种方式可以非常方便地实现线程安全的类,线程安全的类具有如下特征.

该类的对象可以被多个线程安全地访问.

每个线程调用该对象的任意方法之后都将得到正确结果.

每个线程调用该对象的任意方法之后,该对象依然保持合理状态.

减少线程安全的负面

  1. 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。例如上面的LCAccount类中的accountNo属性就无须同步,所以程序只对draw方法进行同步控制。
  2. 如果可变类有两种运行环境:单线程和多线程环境,则应该为该可变类型提供两种版本---线程不安全版本和线程安全版本,在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。

释放对同步监视器的锁定

任何线程在进入同步代码块之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定。

当前线程的同步代码执行结束,当前线程即释放同步监视器。

当线程在同步代码块中遇到goto、return终止了该代码块、该方法的继续执行时,当前线程将会释放同步监视器。

当线程在同步代码块中出现错误,导致该代码块异常结束时,将会释放同步监视器。

典型地,当程序调用NSThread的sleepXxx方法暂停线程时,线程不会释放同步监视器。

同步琐(NSLock)

Foundation还提供了NSLock,它通过显式定义同步锁对象来实现同步,在这种机制下,同步锁使用NSLock对象充当。

NSLock是控制多个线程对共享资源进行访问的工具,通常锁定提供了对共享资源的独占访问,每次只能有一个线程对NSLock对象加锁,线程开始访问共享资源之前应先获得NSLock对象。

在实现线程安全的控制中,使用该NSLock对象可以显式地加锁、释放锁。通常使用NSLock的代码格式如下:

 1   @implementation X
 2
 3   NSLock *lock;
 4
 5   - (id)init
 6
 7 {
 8
 9    self = [super init];
10
11    if(self)
12
13 {
14
15   lock = [[NSLock alloc] init];
16
17 }
18
19 return self;
20
21 }
22
23 // 定义需要保证线程安全的方法
24
25 - (void) m
26
27 {
28
29    [lock lock];
30
31    // 需要保证线程安全的代码
32
33    // … method body
34
35   [lock unlock];
36
37 }
38
39 …
40
41 @end

通过使用NSLock对象,我们可以把LCAccount类改为如下形式,它依然是线程安全的.

 1 LCAccount.m
 2
 3 @implementation LCAccount
 4
 5 NSLock *lock;
 6
 7 - (id)init
 8
 9 {
10
11   self = [super init];
12
13   if(self)
14
15   {
16
17      lock = [[NSLock alloc] init];
18
19 }
20
21 return self;
22
23 }
24
25 - (id)initWithAccountNo:(NSString*)aAccount balance:(CGFloat)aBalance
26
27 {
28
29   self = [super init];
30
31   if(self)
32
33   {
34
35     lock = [[NSLock alloc] init];
36
37     _accountNo = aAccount;
38
39     _balance = aBalance;
40
41 }
42
43 return self;
44
45 }
46
47 // 提供一个线程安全的draw方法来完成取钱操作
48
49 - (void)draw:(CGFloat)drawAmount
50
51 {
52
53    // 显式锁定lock对象
54
55    [lock lock];
56
57    // 账户余额大于取钱数目
58
59    if(self.balance > = drawAmount)
60
61   {
62
63     // 吐出钞票
64
65     NSLog(@”%@取钱成功!吐出钱票:%g”, [NSThread currentThread].name, drawAmount);
66
67     [NSThread sleepForTimeInterval:0.001];
68
69     // 修改余额
70
71    _balance = _balance – drawAmount;
72
73    NSLog(@”\t余额为:%g”, self.balance);
74
75 }
76
77 else
78
79 {
80
81    NSLog(@”%@取钱失败!余额不足!”, [NSThread  currentThread].name);
82
83 }
84
85  // 释放lock的锁定
86
87 [lock unlock];
88
89 }
90
91 // 省略hash和isEqual:方法
92
93 …
94
95 @end

定义了一个NSLock对象,程序中实现draw:方法时,进入方法开始执行后立即请求对NSLock对象进行加锁,当执行完draw:方法的取钱逻辑之后,程序释放对NSLock对象的锁定.

提示:使用NSLock与使用同步方式有点相似,只是使用NSLock时显式使用NSLock对象作为同步锁,而使用同步代码块时系统显式使用某个对象作为同步监视器,同样都符合”加锁->修改->释放锁”的操作模式,而且使用NSLock对象时每个NSLock对象对应一个LCAccount对象,一样可以保证对于同一个LCAccount对象,同一时刻只能有一个线程进入临界区.

使用NSCondition控制线程通信

当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,但我们可以通过一些机制来保证线程协调运行,也就是处理线程之间的通信。

Foundation提供了一个NSCondition类来处理线程通信,NSCondition实现了NSLocking协议,因此也可以调用lock、unlock来实现线程同步。NSCondition可以让那些已经锁定NSCondition对象却无法继续执行的线程释放NSCondition对象,NSCondition对象也可以唤醒其他处于等待状态的线程。

NSCondition类提供了如下3个方法

- wait:

该方法导致当前线程一直等待,直到其他线程调用该NSCondition的signal方法或broadcast方法来唤醒该线程。wait方法有一个变体:- (BOOL)waitUntilDate:(NSDate *)limiteout,用于控制等待到指定时间点,如果到了该时间点,该线程将会被自动唤醒。

- signal:

唤醒在此NSCondition对象上等待的单个线程。如果所有线程都在该NSCondition对象上等待,则会选择唤醒其中一个线程,选择是任意性的。只有当前线程放弃对该NSCondition对象的锁定后(使用wait方法),才可以执行被唤醒的线程。

- broadcast:

唤醒在此NSCondition对象上等待的所有线程,只有当前线程放弃对该NSCondition对象的锁定后,才可以执行被唤醒的线程。

/*

本程序中LCACcount使用NSCondition对象来控制同步,并使用NSCondition对象来控制线程的通信。程序通过一个旗标来标识账户中是否已有存款,当旗标为“NO”时,表明账户中没有存款,存款者线程可以向下执行,当存款者把钱存入账户后,将旗标设为“YES”,并调用signal或broadcast方法来唤醒其他线程;当存款者线程进入线程体后,如果旗标为“YES”,就调用wait方法让该线程等待。

当旗标为“YES”时,表明账户中已经存入了钱,则取钱者线程可以向下执行,当取钱者把钱从账户中取出后,将旗标设为“NO”,并调用signal或broadcast方法来唤醒其他线程;当存款者线程进入线程体后,如果旗标为“YES”,就调用wait方法让该线程等待。

上面这种模式可以推而广之,存钱的线程可以称为生产者,而取钱的线程则可以称为消费者,生产者与消费者之间通过NSCondition进行通信,从而实现良好的协调运行。

本程序通过为LACcount类提供draw:和deposit:两个方法,分别对应该账户的取钱、存钱等操作,因为这两个方法可能需要并发修改LCAccount类的balance成员变量,所以这两个方法都使用NSCondition来控制线程同步。除此之外,这两个方法还使用了wait、broadcast来控制线程的通信。

*/

  1 LCAccount.m
  2
  3 @implementation LCAccount
  4
  5 NSCondition* cond;
  6
  7 BOOL flag;
  8
  9 - (id)init
 10
 11 {
 12
 13 self = [super init];
 14
 15 if(self)
 16
 17 {
 18
 19 cond = [[NSCondition alloc] init];
 20
 21 }
 22
 23 return self;
 24
 25 }
 26
 27 -(id)initWithAccountNo:(NSString*)aAccount balance:(CGFloat)aBalance
 28
 29 {
 30
 31 self = [super init];
 32
 33 if(self)
 34
 35 {
 36
 37 cond = [[NSCondition alloc] init];
 38
 39 _accountNo = aAccount;
 40
 41 _balance = aBalance;
 42
 43 }
 44
 45 return self;
 46
 47 }
 48
 49 // 提供一个线程安全的draw方法来完成取钱操作
 50
 51 - (void)draw:(CGFloat)drawAmount
 52
 53 {
 54
 55 // 加锁
 56
 57 [cond lock];
 58
 59 // 如果flag为NO,则表明账户中还没有人存钱进去,取钱方法阻塞
 60
 61 if(!flag)
 62
 63 {
 64
 65 [cond wait];
 66
 67 }
 68
 69 else
 70
 71 {
 72
 73 // 执行取钱操作
 74
 75 NSLog(”%@ 取钱:%g”, [NSThread currentThread].name,    drawAmount);
 76
 77 _balance -= drawAmount;
 78
 79 NSLog(@”账户余额为:”%g”, self.balance);
 80
 81 // 将标识账户是否已有存款的旗标设为NO
 82
 83 flag = NO;
 84
 85 // 唤醒其他线程
 86
 87 [cond broadcast];
 88
 89 }
 90
 91 [cond unlock];
 92
 93 }
 94
 95 - (void)deposit:(CGFloat)depositAmount
 96
 97 {
 98
 99 [cond lock];
100
101 // 如果flag为YES,则表明账户中已有人存钱进去了,存钱方法阻塞
102
103 if(flag)// 1
104
105 {
106
107 [cond wait];
108
109 }
110
111 else
112
113 {
114
115 // 执行存款操作
116
117 NSLog(@”%@ 存款:%g”, [NSThread currentThread].name, depositAmount);
118
119 _balance += depositAmount;
120
121 NSLog(@”账户余额为:%g”, self.balance);
122
123 // 将标识账户是否已有存款的旗标设为YES
124
125 flag = YES;
126
127 // 唤醒其他线程
128
129 [cond broadcast];
130
131 }
132
133 [cond unlock];
134
135 }
136
137 // 此处省略了hash和isEqual:方法
138
139 …
140
141 @end

上面程序中的代码使用了wait和broadcast进行控制,对存款线程而言,当程序进入deposit:方法后,如果flag为”YES”,则表明账户中已有存款,程序调用wait方法阻塞;否则,程序向下执行存款操作,当存款操作执行完成后,系统将flag设为“YES”,然后调用broadcast来唤醒其他被阻塞的线程----如果系统中有存款者线程,存款者线程也会被唤醒,但该存款者线程执行到“1”号代码处时再次进入阻塞状态,只有执行draw:方法的取钱者线程才可以向下执行,同理,取钱者线程的执行流程也是如此。

/*

程序中的存款者线程循环100次重复村矿,而取钱者线程则循环100次重复取钱,存款者线程和取钱者线程分别调用LCAccount对象的deposit:、draw:方法来实现。

*/

 1 ViewController.m
 2
 3 @implementation ViewController
 4
 5 LCAccount* account;
 6
 7 - (void)viewDidLoad
 8
 9 {
10
11    [super viewDidLoad];
12
13    // 创建一个账号
14
15   account = [[LCAccount alloc] initWithAccountNo:@”321321” balance:1000.0];
16
17 }
18
19 - (IBAction)depositDraw:(id)sender
20
21 {
22
23    // 创建 启动3个存钱者线程
24
25    [NSThread detachNewThreadSelector:@selector(drawMethod:) toTarget:self withObject:[NSNumber numberWithDouble:800.0]];
26
27    [NSThread detachNewThreadSelector:@selector(drawMethod:) toTarget:self withObject:[NSNumber numberWithDouble:800.0]];
28
29 [NSThread detachNewThreadSelector:@selector(drawMethod:) toTarget:self withObject:[NSNumber numberWithDouble:800.0]];
30
31    // 创建,启动取钱者线程
32
33    [NSThread detachNewThreadSelector:@selector(depositMethod:) toTarget:self withObject:[NSNumber numberWithDouble:800.0]];
34
35 }
36
37 - (void)drawMethod:(NSNumber*)drawAmount
38
39 {
40
41    [NSThread currentThread].name = @”甲”;
42
43    // 重复100次执行取钱操作
44
45    for(int i = 0;i < 100; i++)
46
47    {
48
49      [account draw:drawAmount.doubleValue];
50
51 }
52
53 }
54
55 - (void)depositMethod:(NSNumber*) depositAmount
56
57 {
58
59   [NSThread currentThread].name = @”乙”;
60
61   // 重复100次执行存款操作
62    for(int i = 0; i < 100; i++)
63   {
64      [account deposit:depositAmount.doubleValue];
65    }
66 }
67 @end

转载于:https://www.cnblogs.com/congli0220/p/5008083.html

iOS-----线程同步与线程通信相关推荐

  1. iOS开发——高级篇——线程同步、线程依赖、线程组

    前言 对于iOS开发中的网络请求模块,AFNet的使用应该是最熟悉不过了,但你是否把握了网络请求正确的完成时机?本篇文章涉及线程同步.线程依赖.线程组等专用名词的含义,若对上述名词认识模糊,可先进行查 ...

  2. 多线程——线程实现、线程状态、线程同步、线程通信、线程池

    多线程 一.线程 1.普通方法调用和多线程 2.程序.进行.线程 二.线程创建 1.继承Thread类 2.实现Runable接口 3.实现Callable接口 4.静态代理模式 5.Lamda表达式 ...

  3. python 测试 多线程 _thread和threading模块 线程同步,线程优先级队列

    文章目录 python 多线程简介 Python中使用线程的两种方式 1.函数式 示例 2.线程模块 示例 线程同步 示例 线程优先级队列( Queue)[暂时没用到,没仔细看] 示例 其他 thre ...

  4. 什么是线程同步和线程异步?

    1.什么是线程同步和线程异步 线程同步:是多个线程同时访问同一资源,等待资源访问结束,浪费时间,效率不高 线程异步:访问资源时,如果有空闲时间,则可在空闲等待同时访问其他资源,实现多线程机制 异步处理 ...

  5. IOS多线程系统学习之线程同步与线程通信

    多线程编程是有趣的事情,它很容易突然出现"错误情况",这是由于系统的线程调度具有一定的随机性造成的.不过,即使程序偶然出现"错误情况",这是由于系统的线程调度具 ...

  6. educoder 使用线程锁(lock)实现线程同步_线程间的通信(一)

    这篇文章主要从4个角度来讲多线程间的通信: 使用wait/notify实现线程间的通信 生产者/消费者模式的实现 方法join的使用 ThreadLocal类的使用 等待/通知机制的实现: (1)wa ...

  7. 采用信号量机制实现消费者与生产者的线程同步_Java线程通信

    线程通信是Java线程部分的重点,我们介绍一下常见的几种线程通信方式. 线程锁与同步 锁机制是线程通信的一种重要方式.当多个线程竞争某一个对象时,一旦某个线程获得对象就会立刻将其上锁,其他线程只能等待 ...

  8. 线程安全、线程同步、线程间通信

    一.线程安全 多个线程在执行同一段代码的时候,每次的执行结果和单线程执行的结果都是一样的,不存在执行结果的二义性,就可以称作是线程安全的. 讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会 ...

  9. java 多线程编程(包括创建线程的三种方式、线程的生命周期、线程的调度策略、线程同步、线程通信、线程池、死锁等)

    1 多线程的基础知识 1.1 单核CPU和多核CPU 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务.微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那 ...

  10. 线程(线程基本概念、java实现多线程、使用多线程、线程的生命周期、线程同步、线程死锁)

    (一)线程基本概念 一. 程序, 进程, 线程的概念 程序: 使用某种语言编写一组指令(代码)的集合,静态的 进程: 运行的程序,表示程序一次完整的执行, 当程序运行完成, 进程也就结束了 个人电脑: ...

最新文章

  1. 使用joda-time工具类 计算时间相差多少 天,小时,分钟,秒
  2. spring boot 修改 jackson string的null为空字符串
  3. 图解SQL Server 安全函数的使用
  4. Jquery-core.holdReady()
  5. dubbo使用遇到的问题
  6. 【Python数据分析】四级成绩分布 -matplotlib,xlrd 应用
  7. matlab过滤,matlab过滤问题
  8. Java多线程:线程间通信之volatile与sychronized
  9. 图像对齐(image alignment)
  10. Sort方法根据T类某个字段对泛型集合ListT排序
  11. ssh相互访问不用密码
  12. 【贪心 和 DP + 卖股票】LeetCode 121. Best Time to Buy and Sell Stock
  13. PHP 中数组获取不到元素
  14. 需求分析,我们应当怎样做
  15. csgo 机器人模式_csgo怎么加机器人
  16. scuba 报表_是否想了解JavaScript的for循环? 这个动画的SCUBA潜水员可以提供帮助!...
  17. 哈罗单车弯道超车,摩拜危险了!
  18. 癸卯年新春贺文 --孤羽江绎
  19. shadow acne(阴影失真)和peter panning(阴影悬浮)
  20. 乐观锁 VS 悲观锁

热门文章

  1. werkzeug routing.MapAdapter
  2. Basic concepts behind Web Audio API
  3. TensorFlow models/research
  4. hdfs shell
  5. C语言size_t类型
  6. golang |问题代码报go并发死锁
  7. mysql的orm框架_Mysql-Sqlalchemy-ORM框架
  8. VMware vSphere 6.7之vSAN配置全程图解
  9. 5月22日晚间,阿里巴巴集团发布2020财年业绩……
  10. App后台开发运维和架构实践学习总结(5)——App产品从需求到研发到开发到上线到产品迭代全过程