- (instancetype)initWithTarget:方法所属的对象 selector:需要执行的方法 object:方法需要的参数;
- (instancetype)initWithBlock:无参数无返回值的块;
+ (void)detachNewThreadSelector:需要执行的方法 toTarget:方法所属的对象 withObject:方法需要的参数;
+ (void)detachNewThreadWithBlock:无参数无返回值的块;
通过这个方法,可以直接添加一个线程添加到可执行线程池中。- (void)performSelectorInBackground:需要执行的方法 withObject:方法需要的参数;
@property (nullable, copy) NSString *name;
@property double threadPriority;
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isCancelled) BOOL cancelled;
+ (void)exit;
调用完这个方法以后,线程的 cannelled 属性为 YES。+ (void)sleepForTimeInterval:休眠时间;
+ (void)sleepUntilDate:(NSDate *)date;
- (void)performSelectorOnMainThread:在主线程需要执行的方法 withObject:方法当中的参数 waitUntilDone:是否等待当前线程执行完毕子再执行该方法 modes:NSRunLoop当中的模式;
- (void)performSelectorOnMainThread:在主线程需要执行的方法 withObject:方法当中的参数 waitUntilDone:如果为 YES,则需要执行完当前线程以后,才执行主线程当中的方法。如果为 NO,则无需执行完当前线程就调用主线程执行该方法;
- (void)performSelector:需要执行的方法 onThread:在哪个线程当中执行该方法 withObject:方法当中需要的参数 waitUntilDone:是否等待当前线程执行完毕子再执行该方法 modes:NSRunLoop当中的模式
- (void)performSelector:需要执行的方法 onThread:在哪个线程执行该方法 withObject:方法当中的参数 waitUntilDone:是否等待当前线程执行完毕子再执行该方法
- (id)performSelector:需要执行的方法;
- (id)performSelector:需要执行的方法 withObject:方法当中的参数;
- (id)performSelector:需要执行的方法 withObject:方法需要的参数1 withObject:方法需要的参数2;
样例1:异步下载图片 — 通过方法来控制线程通信
当我们需要从网络上加载一张图片的时候,如果我们直接在主线程上进行下载,会出现图片下载完成之前,手机点击无任何反应。只有图片下载好并且加载完成以后才能使用手机。如果这个时候网络突然出现问题。则主线程一直处于阻塞状态。影响用户体验。这个时候需要将下载任务分担给子线程,主线程只负责图片的下载完成的刷新。
实现效果:当我们点击按钮的时候,图片通过子线程进行加载。
实现步骤
1. 从子线程当中下载图片。
2. 当子线程下载好图片时,从主线程刷新图片
实现代码
// 当点击按钮以后,执行以下事件
- (IBAction)showImage:(UIButton *)sender {
[NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];
}
- (void)downloadImage {
NSURL *url = [NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1556552942866&di=24c024e198e7cb243247413e19fb50a8&imgtype=jpg&src=http%3A%2F%2Fupload.ikanchai.com%2F2016%2F0704%2F1467601486394.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
// 当图片下载完成以后,才在主线程上执行刷新动作
[self performSelectorOnMainThread:@selector(setImageInImageView:) withObject:image waitUntilDone:YES];
}
- (void)setImageInImageView:(UIImage *)image {
self.imageView.image = image;
}
样例2:存取钱 — 利用 NSCondition 来进行控制线程通信
模拟银行取钱存钱的过程。
假设卡里有 1000 块,小张和小王分别从两个窗口进行存取钱。现在要求必须先取钱再存钱。每次小张往里面存 800 块,小王每次从里面取 800 块。两人做 100 次。
首先需要创建一个账户
import UIKit
class ITAccount: NSObject {
// 创建一个 NSCondition 对象,负责管理线程的状态
var condition:NSCondition!
// 创建一个标记,看是否在里面存钱了
var hasMoney = true
var accountNO:String
var balance:Double
init(accountNO:String, balance:Double) {
self.accountNO = accountNO
self.balance = balance
self.condition = NSCondition()
}
// 取钱方法
func draw(drawAmount:Double) {
// 先将条件上锁,保证其他线程不影响该线程的执行
condition.lock()
// 判断有没有人向里面存钱,如果没人,让当期线程进入等待状态。等待有人先把钱存进账户,再执行当前线程。
if !hasMoney {
condition.wait()
} else {
print("\(Thread.current.name ?? "") 取了\(drawAmount)元")
Thread.sleep(forTimeInterval: 0.01)
balance -= drawAmount
print("\t 卡里还剩\(balance)元")
// 当取钱成功以后,需要将账户状态设置成已取钱。不能再取钱。
hasMoney = false
// 这个时候通知其他线程,进入就绪状态(检查一下有没有其他线程等待访问)
condition.broadcast()
}
// 当边界区代码执行完毕以后,解除条件锁
condition.unlock()
}
// 存钱的方法
func deposit(drawAmount:Double) {
condition.lock()
// 判断有人往里面存钱了,如果有人往里面存钱了,则当期线程进入阻塞状态,等待其他线程唤醒
if hasMoney {
condition.wait()
} else {
print("从\(Thread.current.name ?? "")存款成功")
balance += drawAmount
print("现在卡里有\(balance)元")
hasMoney = true
condition.broadcast()
}
condition.unlock()
}
}
转换到控制器当中,创建一个按钮,当点击按钮以后开始执行这个存取钱操作
import UIKit
class ViewController: UIViewController {
var account:ITAccount!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
account = ITAccount(accountNO: "123456", balance: 1000)
let button = UIButton(type: .system)
button.frame = CGRect(x: 10, y: 50, width: view.bounds.width - 20, height: 30)
button.setTitle("并发取钱", for: .normal)
button.addTarget(self, action: #selector(drawMoney(sender:)), for: .touchUpInside)
view.addSubview(button)
}
@objc func drawMoney(sender:UIButton) {
// 这个线程负责取钱
let thread1 = Thread(target: self, selector: #selector(draw(moneyAmount:)), object: 800)
thread1.name = "中国银行"
// 这个线程负责存钱
let thread2 = Thread(target: self, selector: #selector(deposit(moneyAmount:)), object: 800)
thread2.name = thread1.name
thread1.start()
thread2.start()
}
@objc func draw(moneyAmount:NSNumber) {
for _ in 0...100 {
account.draw(drawAmount: moneyAmount.doubleValue)
}
}
@objc func deposit(moneyAmount:NSNumber) {
for _ in 0...100 {
account.deposit(drawAmount: moneyAmount.doubleValue)
}
}
}
关于线程同步的案例:
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
[button setTitle:@"非线程安全卖票" forState:UIControlStateNormal];
button.frame = CGRectMake(10, 50, CGRectGetWidth(self.view.bounds - 30), 30);
[button addTarget:self action:@selector(unsafeBuyTicketButton:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
- (void)unsafeBuyTicketButton:(UIButton *)sender {
self.leftTickets = 50;
self.beijingStation = [[NSThread alloc] initWithTarget:self selector:@selector(unsafeBuyTicket) object:nil];
self.beijingStation.name = @"北京站";
self.shanghaiStation = [[NSThread alloc] initWithTarget:self selector:@selector(unsafeBuyTicket) object:nil];
self.shanghaiStation.name = @"上海站";
[self.beijingStation start];
[self.shanghaiStation start];
}
- (void)unsafeBuyTicket {
while (self.leftTickets) {
self.leftTickets--;
NSLog(@"现在%@卖出一张票,还剩%ld张票", NSThread.currentThread.name, self.leftTickets);
[NSThread sleepForTimeInterval:0.1];
}
}
执行结果如下(这个地方只是截取部分结果,更加突出非线程安全)
2019-04-29 00:20:36.213789+0800 买票[3525:338766] 现在上海站卖出一张票,还剩48张票
2019-04-29 00:20:36.213796+0800 买票[3525:338765] 现在北京站卖出一张票,还剩49张票
2019-04-29 00:20:36.317764+0800 买票[3525:338766] 现在上海站卖出一张票,还剩47张票
2019-04-29 00:20:36.317764+0800 买票[3525:338765] 现在北京站卖出一张票,还剩47张票
2019-04-29 00:20:36.422135+0800 买票[3525:338765] 现在北京站卖出一张票,还剩45张票
2019-04-29 00:20:36.422135+0800 买票[3525:338766] 现在上海站卖出一张票,还剩46张票
2019-04-29 00:20:36.525773+0800 买票[3525:338765] 现在北京站卖出一张票,还剩44张票
2019-04-29 00:20:36.525775+0800 买票[3525:338766] 现在上海站卖出一张票,还剩44张票
2019-04-29 00:20:36.630788+0800 买票[3525:338766] 现在上海站卖出一张票,还剩43张票
2019-04-29 00:20:36.630788+0800 买票[3525:338765] 现在北京站卖出一张票,还剩43张票
2019-04-29 00:20:36.733148+0800 买票[3525:338766] 现在上海站卖出一张票,还剩41张票
2019-04-29 00:20:36.733148+0800 买票[3525:338765] 现在北京站卖出一张票,还剩41张票
2019-04-29 00:20:36.836007+0800 买票[3525:338766] 现在上海站卖出一张票,还剩40张票
2019-04-29 00:20:36.836008+0800 买票[3525:338765] 现在北京站卖出一张票,还剩40张票
2019-04-29 00:20:36.940403+0800 买票[3525:338765] 现在北京站卖出一张票,还剩38张票
2019-04-29 00:20:36.940403+0800 买票[3525:338766] 现在上海站卖出一张票,还剩38张票
2019-04-29 00:20:37.045444+0800 买票[3525:338765] 现在北京站卖出一张票,还剩37张票
2019-04-29 00:20:37.045469+0800 买票[3525:338766] 现在上海站卖出一张票,还剩37张票
......
2019-04-29 00:20:37.460871+0800 买票[3525:338765] 现在北京站卖出一张票,还剩29张票
2019-04-29 00:20:37.460871+0800 买票[3525:338766] 现在上海站卖出一张票,还剩29张票
2019-04-29 00:20:37.565210+0800 买票[3525:338765] 现在北京站卖出一张票,还剩28张票
2019-04-29 00:20:37.565210+0800 买票[3525:338766] 现在上海站卖出一张票,还剩28张票
......
2019-04-29 00:20:38.393183+0800 买票[3525:338765] 现在北京站卖出一张票,还剩12张票
2019-04-29 00:20:38.393183+0800 买票[3525:338766] 现在上海站卖出一张票,还剩12张票
......
2019-04-29 00:20:38.598336+0800 买票[3525:338766] 现在上海站卖出一张票,还剩9张票
2019-04-29 00:20:38.598336+0800 买票[3525:338765] 现在北京站卖出一张票,还剩9张票
2019-04-29 00:20:38.700762+0800 买票[3525:338766] 现在上海站卖出一张票,还剩7张票
2019-04-29 00:20:38.700762+0800 买票[3525:338765] 现在北京站卖出一张票,还剩7张票
2019-04-29 00:20:38.804648+0800 买票[3525:338766] 现在上海站卖出一张票,还剩5张票
2019-04-29 00:20:38.804648+0800 买票[3525:338765] 现在北京站卖出一张票,还剩5张票
这里面的有些票数是没按照倒序往下排,有些是票卖重了。究其原因是由于有些情况下两个线程同时在购买车票(车票是两者的共享变量)。而在现实生活当中,如果一个人在买票的时候,另外一个人应该是无法购买到相同位置的票。所以需要将买票的那段代码添加同步锁。保证在买票的时候只有一个窗口可以买票。
所以为了保证线程安全,需要在取钱的部分进行加锁
- (void)safeBuyTicket {
// 由于需要修改车票数,所以需要先将对象自身进行加锁。
@synchronized (self) {
while (self.leftTickets) {
self.leftTickets--;
NSLog(@"现在%@卖出一张票,还剩%ld张票", NSThread.currentThread.name, self.leftTickets);
[NSThread sleepForTimeInterval:0.2];
}
NSLog(@"对不起,%@没票了", NSThread.currentThread.name);
}
// 当卖票过程结束以后,锁解除
}
这个时候再去执行。得到以下结果
2019-04-29 09:01:36.569563+0800 买票[3894:363026] 现在北京站卖出一张票,还剩49张票
2019-04-29 09:01:36.774933+0800 买票[3894:363026] 现在北京站卖出一张票,还剩48张票
2019-04-29 09:01:36.977431+0800 买票[3894:363026] 现在北京站卖出一张票,还剩47张票
2019-04-29 09:01:37.182415+0800 买票[3894:363026] 现在北京站卖出一张票,还剩46张票
2019-04-29 09:01:37.387306+0800 买票[3894:363026] 现在北京站卖出一张票,还剩45张票
2019-04-29 09:01:37.587751+0800 买票[3894:363026] 现在北京站卖出一张票,还剩44张票
2019-04-29 09:01:37.791243+0800 买票[3894:363026] 现在北京站卖出一张票,还剩43张票
2019-04-29 09:01:37.993895+0800 买票[3894:363026] 现在北京站卖出一张票,还剩42张票
2019-04-29 09:01:38.199246+0800 买票[3894:363026] 现在北京站卖出一张票,还剩41张票
2019-04-29 09:01:38.400462+0800 买票[3894:363026] 现在北京站卖出一张票,还剩40张票
2019-04-29 09:01:38.602914+0800 买票[3894:363026] 现在北京站卖出一张票,还剩39张票
2019-04-29 09:01:38.808258+0800 买票[3894:363026] 现在北京站卖出一张票,还剩38张票
2019-04-29 09:01:39.013592+0800 买票[3894:363026] 现在北京站卖出一张票,还剩37张票
2019-04-29 09:01:39.215573+0800 买票[3894:363026] 现在北京站卖出一张票,还剩36张票
2019-04-29 09:01:39.417993+0800 买票[3894:363026] 现在北京站卖出一张票,还剩35张票
2019-04-29 09:01:39.623340+0800 买票[3894:363026] 现在北京站卖出一张票,还剩34张票
2019-04-29 09:01:39.827254+0800 买票[3894:363026] 现在北京站卖出一张票,还剩33张票
2019-04-29 09:01:40.027850+0800 买票[3894:363026] 现在北京站卖出一张票,还剩32张票
2019-04-29 09:01:40.229999+0800 买票[3894:363026] 现在北京站卖出一张票,还剩31张票
2019-04-29 09:01:40.431019+0800 买票[3894:363026] 现在北京站卖出一张票,还剩30张票
2019-04-29 09:01:40.633772+0800 买票[3894:363026] 现在北京站卖出一张票,还剩29张票
2019-04-29 09:01:40.839112+0800 买票[3894:363026] 现在北京站卖出一张票,还剩28张票
2019-04-29 09:01:41.044466+0800 买票[3894:363026] 现在北京站卖出一张票,还剩27张票
2019-04-29 09:01:41.245871+0800 买票[3894:363026] 现在北京站卖出一张票,还剩26张票
2019-04-29 09:01:41.451108+0800 买票[3894:363026] 现在北京站卖出一张票,还剩25张票
2019-04-29 09:01:41.653174+0800 买票[3894:363026] 现在北京站卖出一张票,还剩24张票
2019-04-29 09:01:41.853563+0800 买票[3894:363026] 现在北京站卖出一张票,还剩23张票
2019-04-29 09:01:42.058787+0800 买票[3894:363026] 现在北京站卖出一张票,还剩22张票
2019-04-29 09:01:42.264138+0800 买票[3894:363026] 现在北京站卖出一张票,还剩21张票
2019-04-29 09:01:42.469487+0800 买票[3894:363026] 现在北京站卖出一张票,还剩20张票
2019-04-29 09:01:42.670215+0800 买票[3894:363026] 现在北京站卖出一张票,还剩19张票
2019-04-29 09:01:42.871871+0800 买票[3894:363026] 现在北京站卖出一张票,还剩18张票
2019-04-29 09:01:43.077231+0800 买票[3894:363026] 现在北京站卖出一张票,还剩17张票
2019-04-29 09:01:43.282586+0800 买票[3894:363026] 现在北京站卖出一张票,还剩16张票
2019-04-29 09:01:43.484710+0800 买票[3894:363026] 现在北京站卖出一张票,还剩15张票
2019-04-29 09:01:43.690056+0800 买票[3894:363026] 现在北京站卖出一张票,还剩14张票
2019-04-29 09:01:43.895408+0800 买票[3894:363026] 现在北京站卖出一张票,还剩13张票
2019-04-29 09:01:44.098013+0800 买票[3894:363026] 现在北京站卖出一张票,还剩12张票
2019-04-29 09:01:44.300308+0800 买票[3894:363026] 现在北京站卖出一张票,还剩11张票
2019-04-29 09:01:44.501114+0800 买票[3894:363026] 现在北京站卖出一张票,还剩10张票
2019-04-29 09:01:44.706466+0800 买票[3894:363026] 现在北京站卖出一张票,还剩9张票
2019-04-29 09:01:44.911829+0800 买票[3894:363026] 现在北京站卖出一张票,还剩8张票
2019-04-29 09:01:45.117033+0800 买票[3894:363026] 现在北京站卖出一张票,还剩7张票
2019-04-29 09:01:45.322381+0800 买票[3894:363026] 现在北京站卖出一张票,还剩6张票
2019-04-29 09:01:45.527740+0800 买票[3894:363026] 现在北京站卖出一张票,还剩5张票
2019-04-29 09:01:45.733103+0800 买票[3894:363026] 现在北京站卖出一张票,还剩4张票
2019-04-29 09:01:45.938453+0800 买票[3894:363026] 现在北京站卖出一张票,还剩3张票
2019-04-29 09:01:46.143808+0800 买票[3894:363026] 现在北京站卖出一张票,还剩2张票
2019-04-29 09:01:46.349174+0800 买票[3894:363026] 现在北京站卖出一张票,还剩1张票
2019-04-29 09:01:46.550451+0800 买票[3894:363026] 现在北京站卖出一张票,还剩0张票
2019-04-29 09:01:46.751257+0800 买票[3894:363026] 对不起,北京站没票了
2019-04-29 09:01:46.751632+0800 买票[3894:363027] 对不起,上海站没票了
这个时候结果正常。可以按照顺序进行售票。
2. 模拟取钱的过程
到银行去取钱的时候,有多个窗口可以办理业务。现在假设有 2 个人用同一个账户进行取钱。那么将会创建 2 个线程,分别进行取钱。假设现在卡里有 1000 块。每个人同时从卡里取 800 块。
先声明账户类(ITAccount)
NS_ASSUME_NONNULL_BEGIN
@interface ITAccount : NSObject
@property (nonatomic, copy) NSString *accountNO; 银行卡号
@property (nonatomic, assign) double balance; 卡内余额
- (instancetype)initWithAccount:(NSString *)acccountNO balance:(CGFloat)balance;
/**
从卡里取钱
@param amount 需要取多少钱
*/
- (void)drawAmount:(double)amount;
@end
NS_ASSUME_NONNULL_END
实现账户类(ITAccount)
@implementation ITAccount
- (instancetype)initWithAccount:(NSString *)acccountNO balance:(double)balance {
self = [super init];
if (self) {
_accountNO = acccountNO;
_balance = balance;
}
return self;
}
// 判断卡里面是
- (void)drawAmount:(CGFloat)amount {
// 通过判断卡里面的余额,来决定能否取钱
if (self.balance >= amount) {
NSLog(@"%@取钱成功!取出现金%g元", NSThread.currentThread.name, amount);
self.balance -= amount;
[NSThread sleepForTimeInterval:0.2];
NSLog(@"现在还剩%g", self.balance);
} else {
NSLog(@"对不起,从%@取钱失败,无法取钱", NSThread.currentThread.name);
}
}
@end
再从控制器当中创建一个按钮,点击按钮,模拟 2 个人取钱。
@interface ViewController ()
@property (nonatomic, strong) ITAccount *account;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.account = [[ITAccount alloc] initWithAccount:@"123456" balance:1000];
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectMake(10, 50, CGRectGetWidth(self.view.bounds) - 20, 30);
[button setTitle:@"并发编程" forState:UIControlStateNormal];
[button addTarget:self action:@selector(draw:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
}
// 当点击按钮的时候,开始模拟两个人取钱
- (void)draw:(UIButton *)sender{
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(drawMethod:) object:@800];
thread1.name = @"中国银行";
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(drawMethod:) object:@800];
thread2.name = @"中国工商银行";
[thread1 start];
[thread2 start];
}
// 如何去取钱
- (void)drawMethod:(NSNumber *)drawAmount {
[self.account drawAmount:drawAmount.doubleValue];
}
@end
最后结果如下
2019-04-28 23:41:01.168169+0800 取钱[3001:290346] 中国银行取钱成功!取出现金800元
2019-04-28 23:41:01.168207+0800 取钱[3001:290347] 中国工商银行取钱成功!取出现金800元
2019-04-28 23:41:01.370037+0800 取钱[3001:290346] 现在还剩-600
2019-04-28 23:41:01.370037+0800 取钱[3001:290347] 现在还剩-600
出现这个结果的原因是:两个线程对共享变量进行并发访问(相当于两个人同时取钱),导致钱数为负数。
如何解决:将需要共享的部分添加同步锁。当有线程访问该共享变量的时候,为该变量添加一把锁。使得其他线程无法访问到该变量。直到该线程对该变量操作完毕以后,即可解锁。
在取钱的时候,账户相当于共享变量。需要将取钱的这个过程上锁。不能让一个线程取钱的时候另外的线程同样也开始取钱。于是需要将取钱的方法进行上锁。
需要修改 ITAccount 的取钱方法
- (void)drawAmount:(CGFloat)amount {
// 将当前账户作为同步锁对象,如果哪个人需要取钱,需要先将当前账户锁定,防止其它账户能够并发访问到当前账户。
@synchronized (self) {
// 通过判断卡里面的余额,来决定能否取钱
if (self.balance >= amount) {
NSLog(@"%@取钱成功!取出现金%g元", NSThread.currentThread.name, amount);
self.balance -= amount;
[NSThread sleepForTimeInterval:0.2];
NSLog(@"现在还剩%g", self.balance);
} else {
NSLog(@"对不起,从%@取钱失败,无法取钱", NSThread.currentThread.name);
}
}
// 当取钱结束以后,即解除同步锁。
}
修改好以后再运行该程序
2019-04-29 00:09:04.758388+0800 取钱[3314:329183] 中国银行取钱成功!取出现金800元
2019-04-29 00:09:04.963739+0800 取钱[3314:329183] 现在还剩200
2019-04-29 00:09:04.964052+0800 取钱[3314:329184] 对不起,从中国工商银行取钱失败,无法取钱
一个完整的多线程取钱步骤就完成了。
通过线程同步,可以保证线程共享区当中的内容能够安全的访问。
参考链接:
iOS 多线程:『pthread、NSThread』详尽总结 - 简书
iOS开发之多线程编程总结(一) - 简书
十分感谢上面两位大神的帮助,如此博文当中有错误还请大神雅正。