扯扯线程并发和同步的那些事

扯扯线程并发和同步的那些事 2015.6.26

线程基础的那些事——李仙鹏

线程

线程俗称为轻量级进程。在现代OS中,通常以线程作为基本的调度单位。线程会共享进程范围内的资源,例如内存句柄和文件句柄,但每个线程又有各自的程序计数器、栈以及局部变量等。因此,再配合多核CPU,多个线程方可被并发执行。

线程的上下文切换

如果当前运行线程数与CPU核数相同,那么这些线程将不会被系统调度出去。

但是,如果可运行的线程数量大于CPU核数,那么系统会通过上下文切换,将某个正在运行的线程调度出来,从而使其他线程能够获得CPU的时间片,从而得到运行。

上下文切换需要一定的开销:

  1. 系统和应用程序都使用一组相同的CPU,线程调度需要访问系统资源。系统代码消耗越多的CPU时间,分配到应用程序的可用CPU时间就越少。
  2. 上下文切换会导致处理器的一些缓存缺失

线程的以上特性,促使了现代编程的并发和同步问题:

  • 安全性问题。安全性的含义是“永远不会发生糟糕的事情”
  • 活跃性问题。活跃性关注的目标为“某件正确的事情最终会发生”
  • 性能问题。性能关注的点事“正确的事情尽快发生”

超线程(Hyper-Threading)

为何我们会经常听到宣传说:四核八线程并行(如I5处理器)、八核十六线程并行(如I7)。原因是,这些CPU使用了超线程技术。超线程最早由因特尔研发,并在奔腾四处理器将技术主流化。

超线程技术是在CPU内部仅复制必要的资源、让CPU模拟成两个线程;也就是一个实体核心,两个逻辑线程,在一单位时间内处理两个线程的工作,模拟实体双核心、双线程运作。

虽然采用超线程技术能同时执行两个线程,但它并不象两个真正的CPU那样,每个CPU都具有独立的资源。当两个线程都同时需要某一个资源时,其中一个要暂时停止,并让出资源,直到这些资源闲置后才能继续。因此超线程的性能并不等于两颗CPU的性能。

GUI为什么都是单线程

许多人曾经尝试过编写多线程的GUI来处理事件,但最终都由于竞态条件和死锁导致的稳定性而重回到单线程的事件队列模型:使用UI线程从队列中抽取事件,并将事件分发给事件处理器(消费者)。

另一个重要原因是MVC会导致多线程的GUI因为不一致的锁定顺序而发生死锁。
MVC模式图

推荐两本书

值得一看:

JAVA多线程中的单例——贾学涛

常见单利模写法

public class Singleton {  

    private static Singleton mInstance;

    private Singleton(){
    }

    public static Singleton getInstance(){
        if (mInstance == null) {
            mInstance = new Singleton();
        }
        return mInstance;
    }
}
  • 缺点:非线程安全的,在多线程并发的情况下容易出现多个实例存在的情况

改为线程安全的单例模式

通过添加synchronized关键字

public class Singleton {  

    private static Singleton mInstance;

    private Singleton(){
    }

    public static synchronized Singleton getInstance(){
        if (mInstance == null) {
            mInstance = new Singleton();
        }
        return mInstance;
    }
}
  • 缺点:每次都要进行同步检查,实际上需要检查的时机是在首次创建实例的时候。

改为双重检查锁单例模式

public class Singleton {  

    private static Singleton mInstance;

    private Singleton(){
    }

    public static Singleton getInstance(){
        if (mInstance == null) {          //Single Checked
            synchronized(Singleton.class) {
                if (mInstance == null) {        //Double Checked
                    mInstance = new Singleton();
                }
            }
        }
        return mInstance;
    }
}
  • instance = new Singleton()这句,并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情

    1.给 instance 分配内存
    2.调用 Singleton 的构造函数来初始化成员变量
    3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

    但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的.可能会出现第一次检测是mInstance为非null时,有可能实例还未创建。所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

为实例变量增加volatile关键字

public class Singleton {  

    private volatile static Singleton mInstance;    // 增加volatile关键字

    private Singleton(){
    }

    public static Singleton getInstance(){
        if (mInstance == null) {          //Single Checked
            synchronized(Singleton.class) {
                if (mInstance == null) {        //Double Checked
                    mInstance = new Singleton();
                }
            }
        }
        return mInstance;
    }
}
  • 使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障,读操作不会被重排序到内存屏障之前

  • Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序

饿汉加载单例模式

public class Singleton {  

    private static Singleton mInstance = new Singleton();

    private Singleton(){
    }

    public static Singleton getInstance(){
        return mInstance;
    }
}

变种的饿汉加载单例模式

public class Singleton {

    private static Singleton mInstance;

    static {
        mInstance = new Singleton();
    }

    private Singleton(){
    }

    public static Singleton getInstance(){
        return mInstance;
    }
}
  • 缺点:在类被加载的时候就会去创建实例,牺牲空间来保证时间,与之前的单例模式相反,懒汉加载是牺牲时间,来保证空间,在需要的时候再去创建实例。

通过静态内部类来创建单例

public class Singleton {

    private Singleton(){
    }

    public static Singleton getInstance(){
        return InnerClass.mInstance;
    }

    private staitc class InnerClass{
        public static Singleton mInstance = new Singleton();
    }
}
  • 与前者一样,通过classloader机制来保证线程安全,区别是,前者当Singleton类被加载时,就会创建实例,而后者是在需要调用getInstance的时候去加载内部类的时候,来创建实例。

通过枚举来创建单例

public enum Singleton {
    INSTANCE;
}

访问实例对象 Singleton.INSTANCE

*默认枚举实例的创建是线程安全的,但是在枚举中的其他任何方法由程序员自己负责。

为什么要有线程同步之喂金鱼问题——曾铭

喂金鱼问题

  • 金鱼一天不吃会饿死,一天吃两次会撑死;
  • 张三、李四,每天每人分别会去执行这件事一次;

方案 1

两人执行一致

1
2
3
if (noFeed) {
feed fish
}
张三 李四
if (noFeed) { .
. if (noFeed) {
. feed fish
feed fish .
fish died .
结论
  • feed fish 时间越长,鱼被撑死可能性越大
  • 没解决问题

方案 2

两人执行一致

1
2
3
4
5
6
7
8
9
if (noNote) {
leave note

if (noFeed) {
feed fish
}

remove note
}
张三 李四
if (noNote) { .
. if (noNote) {
. leave note
leave note .
if (noFeed) { .
. if (noFeed) {
. feed fish
feed fish .
fish died .
结论
  • noNote, noFeed 按特定顺序执行才会出问题
  • noNote 多了一层保护,如果 leave note 时间很短,出问题可能性很小
  • 没解决问题

方案 3

张三执行

1
2
3
4
5
6
7
8
9
leave note3

if (noNote4) {
if (noFeed) {
feed fish
}
}

remove note3

李四执行

1
2
3
4
5
6
7
8
9
leave note4

if (noNote3) {
if (noFeed) {
feed fish
}
}

remove note4
张三 李四
leave note3 .
. leave note4
. if (noNote3) {
if (noNote4) { .
remove note3 .
. remove note4
fish died .
结论
  • 不会被撑死了,可能会被饿死……
  • 没解决问题

方案 4

张三执行

1
2
3
4
5
6
7
8
9
10
11
12
leave note3

while (noNote4)
{
sleep (1)
}

if (noFeed) {
feed fish
}

remove note3

李四执行

1
2
3
4
5
6
7
8
9
leave note4

if (noNote3) {
if (noFeed) {
feed fish
}
}

remove note4
结论
  • 的确解决了问题
  • 能优化么?
    • 程序不不对称
    • 循环等待的浪费

方案 5

两人执行一致

1
2
3
4
5
6
7
lock()

if (noFeed) {
feed fish
}

unlock()
总结
  • 能解决问题
  • 程序对称
  • 持有锁需要等待,未解决浪费问题
    • 生产者与消费者问题

END

全部内容来自《计算机的心智·操作系统之哲学原理》——邹恒明 第七章

iOS并发相关的概念介绍——潘君

@(归纳中)[iOS]

基础概念

竞态条件

竞态条件(race condition),从多进程间通信的角度来讲,是指两个或多个进程对共享的数据进行读或写的操作时,最终的结果取决于这些进程的执行顺序。
竞态条件(race condition)是指设备或系统出现不恰当的执行时序,而得到不正确的结果。

注: atomic 可以解决竞态竞争 但是无法保证类是线程安全

iOS中的相关概念

atomic属性

property 修饰符

加了atomic后生成的setter,类似如下代码:

1
2
3
4
5
- (void)setProp:(NSString *)newValue {
[_prop lock];
_prop = newValue;
[_prop unlock];
}
@synchronized指令

引用自@synchronized(id anObject) {}定义和使用
1.作用:创建了一个互斥锁,它的作用和其他语言中的互斥锁作用一样

2.解释:这个是OBC中的一个锁定令牌,防止{}里的内容在同一时间内被其他线程访问,起到了线程保护的作用

3.使用范围:一般在单例模式或者操作类的static变量的时候使用,即共用的变量的时候

4.外延:这个令牌隐式的包含了异常处理,如果你不想使用的话,就使用锁吧

5.它的参数是id类型,如果用
@synchronized(1) {
}
编译器提示
@synchronzied requires an Objective-C object type.
也就是说需要一个objective C的对象类型。

1
2
3
@synchronized(id anObject){
// test code
}
NSLock

使用样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//主线程中
TestObj *obj = [[TestObj alloc] init];
NSLock *lock = [[NSLock alloc] init];

//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
[obj method1];
sleep(10);
[lock unlock];
});

//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);//以保证让线程2的代码后执行
[lock lock];
[obj method2];
[lock unlock];
});

iOS多线程 - 杨志平

简介

iOS有三种多线程编程的技术,分别是:

  • NSThread

  • Cocoa NSOperation

  • GCD (全称:Grand Central Dispatch)

这三种编程方式从上到下,抽象度层次是从低到高的,抽象度越高的使用越简单,也是Apple最推荐使用的

三种方式的介绍:

NSThread - 文档

优点:NSThread 比其他两个轻量级
缺点:需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销

NSOperation - 文档

优点:不需要关心线程管理,数据同步的事情,可以把精力放在自己需要执行的操作上。
创建NSOperation子类的对象,把对象添加到NSOperationQueue队列里执行。

GCD - 文档

Grand Central Dispatch (GCD)是Apple开发的一个多核编程的解决方法。在iOS4.0开始之后才能使用。GCD是一个替代诸如NSThread, NSOperationQueue, NSInvocationOperation等技术的很高效和强大的技术。

GCD的底层依然是用线程实现,不过这样可以让程序员不用关注实现的细节。

创建线程的开销 - 查看文档

Item Approximate cost Notes
Kernel data structures Approximately 1 KB This memory is used to store the thread data structures and attributes, much of which is allocated as wired memory and therefore cannot be paged to disk.
Stack space 512 KB (secondary threads) 8 MB (OS X main thread) 1 MB (iOS main thread) The minimum allowed stack size for secondary threads is 16 KB and the stack size must be a multiple of 4 KB. The space for this memory is set aside in your process space at thread creation time, but the actual pages associated with that memory are not created until they are needed.
Creation time Approximately 90 microseconds This value reflects the time between the initial call to create the thread and the time at which the thread’s entry point routine began executing. The figures were determined by analyzing the mean and median values generated during thread creation on an Intel-based iMac with a 2 GHz Core Duo processor and 1 GB of RAM running OS X v10.5.

替代线程的一些技术

Item Approximate cost
Operation objects Introduced in OS X v10.5, an operation object is a wrapper for a task that would normally be executed on a secondary thread. This wrapper hides the thread management aspects of performing the task, leaving you free to focus on the task itself. You typically use these objects in conjunction with an operation queue object, which actually manages the execution of the operation objects on one or more threads.For more information on how to use operation objects, see Concurrency Programming Guide.
Grand Central Dispatch (GCD) Introduced in Mac OS x v10.6, Grand Central Dispatch is another alternative to threads that lets you focus on the tasks you need to perform rather than on thread management. With GCD, you define the task you want to perform and add it to a work queue, which handles the scheduling of your task on an appropriate thread. Work queues take into account the number of available cores and the current load to execute your tasks more efficiently than you could do yourself using threads.For information on how to use GCD and work queues, see Concurrency Programming Guide
Idle-time notifications For tasks that are relatively short and very low priority, idle time notifications let you perform the task at a time when your application is not as busy. Cocoa provides support for idle-time notifications using the NSNotificationQueue object. To request an idle-time notification, post a notification to the default NSNotificationQueue object using the NSPostWhenIdle option. The queue delays the delivery of your notification object until the run loop becomes idle. For more information, see Notification Programming Topics.
Asynchronous functions The system interfaces include many asynchronous functions that provide automatic concurrency for you. These APIs may use system daemons and processes or create custom threads to perform their task and return the results to you. (The actual implementation is irrelevant because it is separated from your code.) As you design your application, look for functions that offer asynchronous behavior and consider using them instead of using the equivalent synchronous function on a custom thread
Timers You can use timers on your application’s main thread to perform periodic tasks that are too trivial to require a thread, but which still require servicing at regular intervals. For information on timers, see Timer Sources
Separate processes Although more heavyweight than threads, creating a separate process might be useful in cases where the task is only tangentially related to your application. You might use a process if a task requires a significant amount of memory or must be executed using root privileges. For example, you might use a 64-bit server process to compute a large data set while your 32-bit application displays the results to the user

线程安全 - 文档

原则
  • Immutable objects are generally thread-safe. Once you create them, you can safely pass these objects to and from threads. On the other hand, mutable objects are generally not thread-safe. To use mutable objects in a threaded application, the application must synchronize appropriately.
  • Many objects deemed “thread-unsafe” are only unsafe to use from multiple threads. Many of these objects can be used from any thread as long as it is only one thread at a time. Objects that are specifically restricted to the main thread of an application are called out as such
  • The main thread of the application is responsible for handling events. Although the Application Kit continues to work if other threads are involved in the event path, operations can occur out of sequence
  • If you want to use a thread to draw to a view, bracket all drawing code between the lockFocusIfCanDraw and unlockFocus methods of NSView
Thread-Safe Classes and Functions

The following classes and functions are generally considered to be thread-safe. You can use the same instance from multiple threads without first acquiring a lock.

NSArray
NSAssertionHandler
NSAttributedString
NSCalendarDate
NSCharacterSet
NSConditionLock
NSConnection
NSData
NSDate
NSDecimal functions
NSDecimalNumber
NSDecimalNumberHandler
NSDeserializer
NSDictionary
NSDistantObject
NSDistributedLock
NSDistributedNotificationCenter
NSException
NSFileManager (in OS X v10.5 and later)
NSHost
NSLock
NSLog/NSLogv
NSMethodSignature
NSNotification
NSNotificationCenter
NSNumber
NSObject
NSPortCoder
NSPortMessage
NSPortNameServer
NSProtocolChecker
NSProxy
NSRecursiveLock
NSSet
NSString
NSThread
NSTimer
NSTimeZone
NSUserDefaults
NSValue

Thread-Unsafe Classes

The following classes and functions are generally not thread-safe. In most cases, you can use these classes from any thread as long as you use them from only one thread at a time.

NSArchiver
NSAutoreleasePool
NSBundle
NSCalendar
NSCoder
NSCountedSet
NSDateFormatter
NSEnumerator
NSFileHandle
NSFormatter
NSHashTable functions
NSInvocation
NSJavaSetup functions
NSMapTable functions
NSMutableArray
NSMutableAttributedString
NSMutableCharacterSet
NSMutableData
NSMutableDictionary
NSMutableSet
NSMutableString
NSNotificationQueue
NSNumberFormatter
NSPipe
NSPort
NSProcessInfo
NSRunLoop
NSScanner
NSSerializer
NSTask
NSUnarchiver
NSUndoManager

Main Thread Only Classes

The following class must be used only from the main thread of an application.

NSAppleScript

多线程的死锁 - 张超耀

  • 俗话说,人多好办事!在程序里也是这样,如果是同一个应用程序需要并行处理多件任务,那就可以创建多条线程。但是人多了,往往会出现冲突,使得这个工作无法再进行下去了(正所谓三个和尚没水喝),这就是“死锁”。

死锁的产生

那么我们如何来消除“死锁”呢?首先,让我们来看看产生“死锁”的必要条件:

  • 互斥:就是说多个线程不能同时使用同一资源

  • 请求和保持:就是某线程必须同时拥有多个资源才能完成任务,否则它将占用已经拥有的资源直到拥有他所需的所有资源为止

  • 不剥夺:就是说所有线程的优先级都相同,不能在别的线程没有释放资源的情况下,夺走其已占有的资源

  • 循环等待,就是没有资源满足的线程无限期地等待

有的朋友可能已经明白了,只要打破这这几个必要条件,就能打破“死锁”!

  • 互斥:就是要让多个线程能共享资源

  • 请求和保持:只要当检测到自己所需的资源仍被别的线程占用,即释放自己已占有的资源(毫不利己,专门利人),或者在经过一段时间的等待后,还未得到所需资源,才释放,这都能打破请求和保持

  • 不剥夺:只要给线程制定一个优先级即可

  • 最后的循环等待的解决方法其实和请求和保持是一样的,都是等待一段时间后释放资源。

好了,希望通过这个例子能让不了解死锁的朋友对“死锁”能有一定的认识

集合的并行操作 - 王胜

用for循环操作一个集合,即读取集合元素,同时又删除集合中的元素,会发生什么事情呢?

上代码【号外,号外,此示例来源于我们项目中的真实代码哦~】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Target class
class Target {
int id;// ID
String type;// 类型
int count;// 未读消息数

public Target(int id, String type, int count) {
this.id = id;
this.type = type;
this.count = count;
}

@Override
public String toString() {
return "Target [id=" + id + ", type=" + type + ", count=" + count + "]";
}

}

// 以下是模拟一组target集合,target包含group、room、user类型。
List<Target> sessions = new ArrayList<Target>();
sessions.add(new Target(1, "group", 1));
sessions.add(new Target(2, "group", 1));
sessions.add(new Target(3, "group", 1));
sessions.add(new Target(4, "room", 1));
sessions.add(new Target(5, "group", 1));
sessions.add(new Target(6, "group", 1));
sessions.add(new Target(7, "group", 1));
sessions.add(new Target(8, "user", 1));
sessions.add(new Target(9, "user", 1));
sessions.add(new Target(10, "group", 1));
System.out.println("before size:"+sessions.size()+", sessions:"+humanPrintList(sessions));
int totalUnread = 0;// 统计集合中类型为group的target未读消息数量
for (int i=0;i<sessions.size();i++) {
System.out.println("read index:" + i + ", session:"+sessions.get(i));
if (sessions.get(i).type.equals("group")) {// 如果是group类型,则累加未读消息数
System.out.println("==> add totalUnread");
totalUnread += sessions.get(i).count;
} else {// 否则,将target移除集合
System.out.println("==> remove");
sessions.remove(i);
}
}
System.out.println("after size:"+sessions.size()+", totalUnread:"+totalUnread+",sessions:"+humanPrintList(sessions));

至此,示例代码结束。大家可以猜测下最后的输出中sessions的长度,内容以及总共的未读消息数据。

项目中的代码,期望结果是剩余集合只包含group类型的target,而且totalUnread的值是类型为group的target的未读消息数之和。可是,运行的结果却是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
before size:10, sessions:
========== List content: ===========
Target [id=1, type=group, count=1]
Target [id=2, type=group, count=1]
Target [id=3, type=group, count=1]
Target [id=4, type=room, count=1]
Target [id=5, type=group, count=1]
Target [id=6, type=group, count=1]
Target [id=7, type=group, count=1]
Target [id=8, type=user, count=1]
Target [id=9, type=user, count=1]
Target [id=10, type=group, count=1]
====================================
read index:0, session:Target [id=1, type=group, count=1]
==> add totalUnread
read index:1, session:Target [id=2, type=group, count=1]
==> add totalUnread
read index:2, session:Target [id=3, type=group, count=1]
==> add totalUnread
read index:3, session:Target [id=4, type=room, count=1]
==> remove
read index:4, session:Target [id=6, type=group, count=1]
==> add totalUnread
read index:5, session:Target [id=7, type=group, count=1]
==> add totalUnread
read index:6, session:Target [id=8, type=user, count=1]
==> remove
read index:7, session:Target [id=10, type=group, count=1]
==> add totalUnread
after size:8, totalUnread:6,sessions:
========== List content: ===========
Target [id=1, type=group, count=1]
Target [id=2, type=group, count=1]
Target [id=3, type=group, count=1]
Target [id=5, type=group, count=1]
Target [id=6, type=group, count=1]
Target [id=7, type=group, count=1]
Target [id=9, type=user, count=1]
Target [id=10, type=group, count=1]
====================================

为什么结果完全不是预期的呢?原因是程序走到else时,将元素移除,后面的元素自动往前移动,所以继续取下一个下标时,被移除的后一个元素悄悄溜走了,成了漏网之鱼。

解决方法 : 最简单的就是在remove后执行 i--;