多线程随笔
1 =================== 2 3 多线程 4 5 ================== 6 7 8 9 多线程 10 11 一、概念: 12 13 程序:(Program)(App)是一个可以运行的文件(我们写的代码) 14 15 进程:(Process)是程序执行的一个操作实体 16 17 即在系统中正在运行的一个应用程序 18 19 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内 20 21 比如同时打开QQ、Xcode,系统就会分别启动2个进程 22 23 通过“活动监视器”可以查看Mac系统中所开启的进程 24 25 01_iOS进程.png 26 27 02_进程和系统.png 28 29 线程:(Thread)线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行 30 31 一个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程) 32 33 比如使用酷狗播放音乐、使用迅雷下载电影,都需要在线程中执行 34 35 03_进程和线程.png 36 37 二、背景: 38 39 线程的串行 40 41 1个线程中任务的执行是串行的 42 43 如果要在1个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务 44 45 也就是说,在同一时间内,1个线程只能执行1个任务 46 47 比如在1个线程中下载3个文件(分别是文件A、文件B、文件C) 48 49 50 51 当用户播放音频、下载资源、进行图像处理时往往希望做这些事情的时候其他操作不会被中断或者希望这些操作过程中更加顺畅。在单线程中一个线程只能做一件事情,一件事情处理不完另一件事就不能开始,这样势必影响用户体验。 52 53 耗时操作 54 55 不依赖逻辑部分的东西 56 57 58 59 意义: 60 61 随着移动设备的更新换代,移动设备的性能也不断提高,现在流行的CPU已经进入双核、甚至四核时代。如何充分发挥这些CPU的性能,会变得越来越重要。在iOS中如果想要充分利用多核心CPU的优势,就要采用并发编程,提高CPU的利用率。 62 63 64 65 所以这节课学好了,会是面试的亮点 66 67 68 69 三、多线程 70 71 72 73 1.多线程原理 74 75 串行并行 76 77 04_线程的串并行.png 78 79 80 81 同一时间,CPU只能处理1条线程,只有1条线程在工作(执行) 82 多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换) 83 如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象 84 思考:如果线程非常非常多,会发生什么情况? 85 CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源 86 每条线程被调度执行的频次会降低(线程的执行效率降低) 87 88 89 90 2.多线程的优缺点 91 92 93 94 多线程的优点 95 96 能适当提高程序的执行效率 97 98 能适当提高资源利用率(CPU、内存利用率) 99 100 101 102 多线程的缺点 103 104 开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能 105 106 线程越多,CPU在调度线程上的开销就越大 107 108 程序设计更加复杂:比如线程之间的通信、多线程的数据共享 109 110 111 112 3.多线程在iOS开发中的应用 113 114 115 116 主线程:一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程” 117 118 主线程的主要作用 119 120 显示\刷新UI界面 121 122 处理UI事件(比如点击事件、滚动事件、拖拽事件等) 123 124 主线程的使用注意:别将比较耗时的操作放到主线程中。 125 126 耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验 127 128 129 130 4.iOS的线程模型 131 132 pthread(底层C线程库) 133 134 NSThread(OC线程库) 135 136 NSOperationQueue(线程池/线程队列) 137 138 GCD(Blocks模式的线程池) 139 140 05_iOS线程模型.png 141 142 143 144 四、整体认知结束,详见代码 145 146 147 148 149 150 01_TimeConsumingAndPhread 151 152 153 154 这个工程告诉我们: 155 156 //1.谁来响应我们的UI操作? 都是主线程响应 157 158 //2.主线程处理任务,都是按照自然顺序处理,谁先来先处理谁,在一件事没有处理完成之前,这个家伙不会抽身去处理其他的事儿 159 160 //3.currentThread 获得当前逻辑代码是运行在那一个线程中 161 162 //4.主线程的number = 1 永远是 1 163 164 165 166 //请问我们应该在主线程上执行什么样的操作呢? 167 168 //主线程上执行的操作最好都是及时响应的,不能让用户觉得我们的程序像windows中的程序一样卡死了,这是一个很严重的用户体验问题 169 170 171 172 //如果我真的需要完成一个非常耗时间的操作怎么办? 173 174 //1.一定不要在主线程做这件事 175 176 //2.开启一条新的线程(子线程),来完成耗时操作 177 178 //3.常见耗时操作 179 180 //大量的开模运算(数学类运算),图形运算,OpenGLES2.0(一般接触不到) 181 182 //网络操作(1.网络环境影响(2G,2.5G,3G,4G相应速度都不同,大数据下载操作)) 183 184 185 186 pthread:C方法创建子线程 187 188 pthread_t cThread; 189 190 int b = pthread_create(&cThread, NULL, working, NULL); 191 192 缺点:不好写,不好控 193 194 优点:只是开个线程而已 195 196 197 198 199 200 201 202 02_NSThreadAndNSLock 203 204 205 206 这个工程告诉我们: 207 208 一、 209 210 //NSThread 是 cocoa(MacOS,iOS)中一个轻量级的多线程对象 211 212 //NSThread 傻瓜式的操作,简单 213 214 //1.如何创建一个子线程 215 216 //2.把耗时操作的逻辑转移到子线程中去 217 218 //object 给 @selector中要执行的方法传递参数 219 220 221 222 // //方法一 223 224 // NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(doWCing) object:nil]; 225 226 // //可以给子线程命名 227 228 // thread.name = @"子线程1"; 229 230 // //使用alloc创建的线程,一定要显示的调用start方法,才能够运行 231 232 // [thread start]; 233 234 235 236 //方法二 237 238 // 创建一个线程 只是创建就好 不亲自执行这个线程 只是创建 239 240 // 线程创建后 就会自动运行 241 242 //为了更加简化我们创建一个子线程的操作,NSObject对创建线程封装了一些方法 243 244 //内部会自动的创建一个子线程,并且把@selector中的方法交给子线程去做 245 246 [NSThread detachNewThreadSelector:@selector(doWCing) toTarget:self withObject:nil]; 247 248 249 250 //线程工作完程,如果以后不再使用这个线程 251 252 //应当做如下操作 253 254 // [thread cancel]; 255 256 [NSThread exit]; 257 258 259 260 二、 261 262 //线程锁 类比上厕所,多个线程有一把锁就够了。谁锁,谁才能打开。 263 264 NSLock * _threadLock; 265 266 /** 267 268 * (atomic)。所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。 269 270 * 271 272 //通过加锁和解锁,使一段代码成为原子操作。 273 274 //使用线程锁,不是服务于线程,而是服务于代码。当我们希望一段代码进行原子操作,如数据库的写入,就可以进行加锁。 275 276 //比如从判断缓存是否存在,到写入新的缓存。应当加一把线程锁。即使我们没有创建NSThread。 277 278 //它和@Syncronized关键字起到同样效果。但是关键字不能在一个函数中加锁,另一个函数中解锁,NSLock可以。 279 280 */ 281 282 //上锁 283 284 [_threadLock lock]; 285 286 //解锁 287 288 [_threadLock unlock]; 289 290 291 292 三、 293 294 //NSThread 很难人为的去控制创建子线程的数量 295 296 //子线程创建过多,会严重的影响程序的性能,因为每一个线程都会占用一定cpu的资源,导致cpu很忙碌,特别是多人开发的时候 297 298 299 300 301 302 03_GCDDemo 303 304 305 306 这个工程告诉我们: 307 308 309 310 1.什么是GCD? 311 312 为并发代码在多核硬件(ios(iphone4s),mac os)上高效执行多线程代码而生的技术 313 314 弥补了NSThread难于管理的问题 315 316 //Grand Central Dispatch(多线程优化技术)或称GCD,是一套底层API,提供了新的模式编写并发执行的程序。允许将一个程序切分为多个单一任务,然后提交到工作队列并并发地或者串行地执行。 317 318 319 320 2.什么是Queue队列? 321 322 GCD使用了队列概念,已经很好的解决了NSThread难于管理的问题,队列实际上就是一个数组的概念,通常我们把要执行的任务,放到一个队列中去管理 323 324 //GCD采用队列的方式管理线程,不仅可以管理多线程,并发的任务,设置主线程,也通过任务队列的方式进行管理。 325 326 //所以说GCD的队列是任务队列,而不是线程队列。 327 328 329 330 3.什么是串行队列? 331 332 依次完成每一个任务 333 334 335 336 4.什么是并行队列? 337 338 好像所有的任务都是在同一时间执行的 339 340 341 342 5.mainqueue(主队列)串行队列,全局队列(Global Queue),自己创建队列 343 344 345 346 347 348 CGD存在的问题 349 350 1.纯c的代码,让很多面向对象程序员头疼 351 352 2.虽然已经很好的解决了NSThread难于管理的问题,但是还不是很好,当程序在某一个时间点上如果需要执行很多个好时操作,也会出现创建很多个线程的问题,导致系统性能出现问题 353 354 355 356 优点: 357 358 通过GCD,开发者不用再直接跟线程打交道了,只需要向队列中添加block代码即可,GCD在后端管理着一个线程池。GCD不仅决定着哪个线程(block)将被执行,它还根据可用的系统资源对线程池中的线程进行管理——这样可以不通过开发者来集中管理线程,缓解大量线程的创建,做到了让开发者远离线程的管理。 359 360 缺点: 361 362 虽然GCD是稍微偏底层的一个API,但是使用起来非常的简单。不过这也容易使开发者忘记并发编程中的许多注意事项和陷阱。 363 364 365 366 367 368 369 370 一、Dispatch Queues 371 372 373 374 1.用户队列(用户队列,串行执行): 375 376 用户队列(GCD中对这种队列没有特定的名词来描述,姑且如此称之)可以使用dispatch_queue_create函数创建队列。这些队列是串行执行的。 377 378 //怎么使用呢? 379 380 //想要执行GCD的方法dispatch_async 必须把它交个一个队列管理,否则报错 381 382 //dispatch_async 开一个子线程,不会阻塞主线程的操作,操作对象是线程,而不是方法 383 384 //向队列中添加block任务 385 386 //这是可以选择任务是同步执行还是异步执行 387 388 //dispatch_async 函数会将传入的block块放入指定的queue里运行。这个函数是异步的,这就意味着它会立即返回而不管block是否运行结束。 389 390 //同步,失去了多线程的意义,只不过是拿出来放在自己的队列里而已 391 392 393 394 //把任务都添加在了同一队列(线程)queue上 395 396 //所以自己创建的队列是串行的 397 398 399 400 401 402 403 404 2.Global queues(全局队列,并行执行): 405 406 全局队列是并发队列,并由整个进程共享。进程中存在三个全局队列,可以调用dispatch_get_global_queue函数,传入优先级来访问队列。 407 408 409 410 抓取全局的队列 411 412 global_queue并行队列 413 414 可以同时运行多个任务,每个任务的启动时间是按照加入queue的顺序,即任务开始执行的顺序和其加入队列的顺序相同,结束的顺序依赖各自的任务.我们自己不能去创建并行调度队列。只有三个可用的global concurrent queues。使用dispatch_get_global_queue获得 415 416 //由于GCD中同一个队列中的任务是串行执行的。所以只要将那些需要线程保护的任务添加到同一个队列当中,就能实现串行执行。 417 418 419 420 421 422 3.The main queue(主队列,串行执行): 423 424 提交到main queue的任务将在主线程执行。main queue可以调用dispatch_get_main_queue()来获得。因为main queue是与主线程相关的,所以这是一个串行队列。 425 426 //至少两个线程会调用这个函数,如果这个函数中有需要原子操作的代码,应当加线程锁,或者使用GCD进行保护 427 428 429 430 //使用GCD保护 431 432 //将需要保护的代码交由主线程执行。是主线程,也是主队列,一个队列的任务,是串行的。 433 434 PS: 435 436 还有一些其他的东西不需要了解的,工作基本不用,面试就说知道这个东西,但是不常用,如果需要可以细查查 437 438 讲这些只会加大我们的负担,影响学习心情和进度,意义不大 439 440 441 442 比如GCD的内存管理 443 444 比如GCD的线程阻碍等。。。 445 446 447 448 449 450 451 452 04_GCDMakeSingleton 453 454 455 456 GCD具有以下优点: 457 458 459 460 GCD 能通过推迟昂贵计算任务并在后台运行它们来改善你的应用的响应性能。 461 462 GCD 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱。 463 464 GCD 具有在常见模式(例如单例)上用更高性能的原语优化你的代码的潜在能力。 465 466 467 468 469 470 单例模式是开发中最常用的写法之一,创建一个单例很多办法,iOS的单例模式有两种官方写法,如下: 471 472 473 474 不使用GCD 475 476 #import "ServiceManager.h" 477 478 479 480 static ServiceManager *defaultManager; 481 482 483 484 @implementation ServiceManager 485 486 487 488 +(ServiceManager *)defaultManager{ 489 490 if(!defaultManager) 491 492 defaultManager=[[self allocWithZone:NULL] init]; 493 494 return defaultManager; 495 496 } 497 498 499 500 @end 501 502 503 504 某些特殊情况下,if执行后面执行的慢了,然后另一个线程用了他,就会创建,if并不安全 505 506 507 508 使用GCD 509 510 #import "ServiceManager.h" 511 512 513 514 @implementation ServiceManager 515 516 517 518 +(ServiceManager *)sharedManager{ 519 520 static dispatch_once_t predicate; 521 522 static ServiceManager * sharedManager; 523 524 dispatch_once(&predicate, ^{ 525 526 sharedManager=[[ServiceManager alloc] init]; 527 528 }); 529 530 return sharedManager; 531 532 } 533 534 535 536 @end 537 538 539 540 dispatch_once这个函数,它可以保证整个应用程序生命周期中某段代码只被执行一次! 541 542 543 544 1. 线程安全。 545 546 2. 满足静态分析器的要求。 547 548 3. 兼容了ARC 549 550 551 552 我们看到,该方法的作用就是执行且在整个程序的声明周期中,仅执行一次某一个block对象。简直就是为单例而生的嘛。而且,有些我们需要在程序开头初始化的动作,如果为了保证其,仅执行一次,也可以放到这个dispatch_once来执行。 553 554 总结:1.这个方法可以在创建单例或者某些初始化动作时使用,以保证其唯一性。2.该方法是线程安全的,所以请放心大胆的在子线程中使用。(前提是你的dispatch_once_t *predicate对象必须是全局或者静态对象。这一点很重要,如果不能保证这一点,也就不能保证该方法只会被执行一次。) 555 556 557 558 559 560 //单例创建的写法 561 562 http://blog.csdn.net/lovefqing/article/details/8516536 563 564 //单例创建写法的原因 565 566 http://blog.sina.com.cn/s/blog_4cd8dd130101mi37.html 567 568 569 570 ios 中如何实现一个真正的单利模式 571 572 一个常见的担忧是它们常常不是线程安全的。这个担忧十分合理,基于它们的用途:单例常常被多个控制器同时访问。 573 574 575 576 1.首先要保证allocWithZone是线程安全,当中调用super方法的时候使用dispatch_once方法锁住 577 578 2.还要保证单利实现方法中也使用了dispatch_once方法锁住创建对象过程 579 580 581 582 //dispatch_once的作用正如其名:对于某个任务执行一次,且只执行一次。 dispatch_once函数有两个参数,第一个参数predicate用来保证执行一次,第二个参数是要执行一次的任务block。 583 584 //dispatch_once被广泛使用在单例、缓存等代码中,用以保证在初始化时执行一次某任务。 585 586 //dispatch_once在单线程程序中毫无意义,但在多线程程序中,其低负载、高可依赖性、接口简单等特性,赢得了广大消费者的一致五星好评。 587 588 589 590 591 592 //(mutableCopy深 与Copy浅,字面理解,能变的是深拷贝) 593 594 //浅拷贝,就是只创建一个同类型对象返回 595 596 //深拷贝就是,不但创建一个同类型对象回去,并且这个对象中所有的属性值一并都赋值过来 597 598 //深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝 599 600 //copy是创建一个新对象(copy 是内容拷贝),retain是创建一个指针,引用对象计数加1(指针拷贝) 601 602 //使用区别 603 604 //mutableCopy得到一个新的可变对象,可以看到它的地址和原来对象的地址是不同的,也就是新对象的retainCount从0-1。 605 606 //而copy得到的是一个不可变对象,这里分为两种情况:1、如果原来的对象也是一个不可变的,那么他们的地址指向同一个地址,也就是说它们同一个对象,只是把retainCount加1了而已,2、原来的对象是一个可变对象,那么它会新生成一个不可变对象,地址不同,也是retainCount从0-1。 607 608 609 610 611 612 05_NSOperationQueue 613 614 NSOperation使用方法 615 616 iOS中有多种多线程实现方式,苹果公司建议我们都使用NSOperation技术 617 618 1.GCD是纯c的对于面向对象程序员来说非常不友好 619 620 2.GCD对线程的管理还不是很强大 621 622 623 624 NSOperation底层实现就是基于GCD来做的 625 626 627 628 dispatch_queue == NSOperationQueue 629 630 dispatch_asyn() == NSOperation 631 632 dispatch_syn() == NSOperation 633 634 635 636 637 638 iOS并发编程中,把每个并发任务定义为一个Operation,对应的类名是NSOperation。NSOperation是一个抽象类,无法直接使用,它只定义了Operation的一些基本方法。我们需要创建一个继承于它的子类或者使用系统预定义的子类。目前系统预定义了两个子类:NSInvocationOperation和NSBlockOperation。 639 640 641 642 643 644 NSInvocationOperation 645 646 NSInvoationOperation是一个基于对象和selector的Operation,使用这个你只需要指定对象以及任务的selector,如果必要,你还可以设定传递的对象参数。 647 648 NSInvocationOperation *invacationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doSomethingWithObj:) object:obj]; 649 650 651 652 同时当这个Operation完成后,你还可以获取Operation中Invation执行后返回的结果对象。 653 654 id result = [invacationOperation result]; 655 656 657 658 NSBlockOperation 659 660 在一个Block中执行一个任务,这时我们就需要用到NSBlockOperation。可以通过blockOperationWithBlock:方法来方便地创建一个NSBlockOperation: 661 662 NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{ 663 664 //Do something here. 665 666 }]; 667 668 669 670 运行一个Operation 671 672 调用Operation的start方法就可以直接运行一个Operation。 673 674 [operation start]; 675 676 取消一个Operation 677 678 要取消一个Operation,要向Operation对象发送cancel消息: 679 680 [operation cancel]; 681 682 683 684 685 686 06_ThreadsCommunication 687 688 多种主线程的获取方式(即线程之间的通信) 689 690 691 692 //通过NSThread 693 694 //假设是子线程执行该方法,如何回调主线程,修改UI 695 696 [self performSelectorOnMainThread:@selector(finished) withObject:nil waitUntilDone:NO]; 697 698 //这个方法是NSObject的类别方法,所有对象都能调用。 699 700 //当前线程回调主线程完成工作,第三个参数是如果传YES,则当前线程等待主线程完成这一工作后,继续执行,否则阻塞。如果传NO,则当前线程不阻塞。 701 702 703 704 705 706 //通过operationQueue 707 708 //线程队列 709 710 //通过这个方法找到主队列,将任务添加给主队列去完成,即可交付给主线程完成。 711 712 NSOperationQueue * mainQueue = [NSOperationQueue mainQueue]; 713 714 715 716 [mainQueue addOperationWithBlock:^(void){ 717 718 NSLog(@"判断执行当前block的是否是主线程 %d",[NSThread isMainThread]); 719 720 }]; 721 722 723 724 //通过GCD 725 726 dispatch_async(dispatch_get_main_queue(), ^(void){ 727 728 NSLog(@"主线程执行这里的语句"); 729 730 }); 731 732