pthread & mutex(互斥锁)*

pthread 是POSIX thread的简称,而POSIX是Unix系统下的接口标准。

通过pthread,我们可以创建一个新的线程。pthread的使用方法非常简单,我们只需要调用pthread_create函数,并传入pthread_t类型的线程ID指针以及回调函数指针即可:

pthread_t tid;
if (pthread_create(&tid, NULL, thread_func, NULL) != 0){
    printf("error creating thread.");
    return EXIT_FAILURE;
}

回调函数调用后会在一个新的线程上运行,当该函数结束时(return),线程终止。除此之外,你也可以通过另外两种方式来终止一个线程:

  1. 通过同一进程的另外线程cancel掉该线程。

  2. 调用pthread_exit退出线程。

互斥锁

线程之间可以共享内存空间,这意味着不同线程可以访问内存中的同一个变量。然而不同线程在同一时间修改一个内存对象会造成一些不可预知的结果。

为了避免意外,我们需要用到pthread里一个非常重要的数据结构 —— 互斥对象(mutex)。互斥对象在使用时结合互斥锁使用,pthread_mutex_lock 和 pthread_mutex_unlock。

互斥锁是这样工作的:线程B锁定了一个互斥对象(mymutex),如果线程A也试图锁定该互斥对象(mymutex)时,线程A就进入休眠状态。一旦线程B释放了互斥对象(调用 pthread_mutex_unlock()) ,线程A 就能够锁定这个互斥对象(换句话说,线程A就将从 pthread_mutex_lock() 函数调用中返回,同时互斥对象被重新锁定)。同样地,当线程A正锁定互斥对象时,如果线程C试图锁定互斥对象的话,线程C也将临时进入睡眠状态。

pthread_mutex_lock(&mymutex);
 /*
   直至解锁后,mymutex会阻止另一个试图访问此区域的线程
 */
pthread_mutex_unlock(&mymutex);

对已锁定的互斥对象上调用 pthread_mutex_lock() 的所有线程都将进入睡眠状态,这些睡眠的线程将“排队”访问这个互斥对象。

从上述可知,mutex会帮助我们锁定一段逻辑区域的访问。但是,如果一个数据对象有多处调用的情况,我们需要根据实际情况,设计统一的接口。

互斥锁的使用

1)互斥对象有两种初始化方式,分为别静态初始化和动态初始化.

静态初始化:

pthread_mutex_t myLock = PTHREAD_MUTEX_INITIALIZER;

动态初始化:

pthread_mutex_t myLock;
pthread_mutex_init(&myLock,NULL);

2)销毁

一旦初始化完成之后,我们就可以根据实际情况对操作实施加锁和解锁,但需要留意的是如果是动态初始化(使用pthread_mutex_init()的方式)的互斥对象,在释放或废弃条件变量之前,需要清理它:

/*
 * 使用pthread_mutex_init()初始化的互斥对象,
 * 应当使用 pthread_mutex_destroy() 清理它。
 * pthread_mutex_destroy() 接受一个指向 pthread_mutext_t 的指针作为参数
 * 并释放创建互斥对象时分配给它的任何资源。
 */
pthread_mutex_destroy(myLock);   

3)尝试锁定

假如,我们不知道互斥对象是否被锁定。当我们使用 pthread_mutex_lock 对互斥对象加锁时,可能互斥对象已经锁定,这时pthread_mutex_lock操作就会进入休眠。为了避免这种情况,我们可以使用 pthread_mutex_trylock 去尝试锁定互斥对象。如果互斥对象当前处于解锁状态,那么您将获得该锁并且函数将返回零。然而,如果互斥对象已锁定,这个调用也不会阻塞。

条件变量

如果线程在等待某个特定条件发生,按照上述方式就需要不停地对数据加锁、解锁进行检测。这样不但浪费了时间和资源,而且繁忙的查询效率也非常低。这时,我们可以利用pthread_cond_wait.

以查询特殊条件为例:
首先,线程A执行对数据的检测并没有找到满足条件的数据。此时,通过pthread_cond_wait()函数将线程A设置为等待休眠状态:

pthread_mutex_lock(&mymutex);
pthread_cond_wait(&mycond, &mymutex);

pthread_cond_wait()被调用后所做的第一件事就是对互斥对象(mymutex)进行解锁,并等待条件mycond发生。

此时,pthread_cond_wait() 调用还未返回,等待条件mycond通常是一个阻塞操作,这意味着线程将睡眠,在它苏醒之前不会消耗 CPU 周期。这正是我们期待发生的情况,线程将一直睡眠,直到特定条件发生,在这期间不会发生任何浪费 CPU 时间的繁忙查询。从线程的角度来看,它只是在等待 pthread_cond_wait() 调用返回。

假如另一个线程B锁定了mymutex并对数据条件做了修改。在对互斥对象解锁之后,线程B立即调用了函数 pthread_cond_broadcast(&mycond)。此操作之后,线程B的操作将使所有等待 mycond 条件变量的线程立即苏醒,这意味着第一个线程现在将苏醒。

pthread_mutex_lock(&mymutex);
//修改数据条件操作 
pthread_mutex_unlock(&mymutex);
pthread_cond_broadcast(&mycond);

您可能会认为在线程B调用 pthread_cond_broadcast(&mymutex) 之后,线程A的 pthread_cond_wait() 会立即返回。不是那样!实际上,pthread_cond_wait() 将执行最后一个操作:重新锁定 mymutex。一旦 pthread_cond_wait() 锁定了互斥对象,那么它将返回并允许线程A继续执行。

1)初始化和清除

和互斥对象一样,条件变量在使用时也需要初始化:

pthread_cond_t mycond;
pthread_cond_init(&mycond,NULL);

在释放或废弃条件变量之前,需要毁坏它,如下所示:

pthread_cond_destroy(&mycond);

2)等待

一旦初始化了互斥对象和条件变量,就可以等待某个条件,如下所示:

pthread_cond_wait(&mycond, &mymutex);

请注意,代码在逻辑上应该包含 mycond 和 mymutex。一个特定条件只能有一个互斥对象,而且条件变量应该表示互斥数据“内部”的一种特殊的条件更改。一个互斥对象可以用许多条件变量(例如,cond_empty、cond_full、cond_cleanup),但每个条件变量只能有一个互斥对象。

3)发送信号和广播

对于发送信号和广播,需要注意一点。如果线程更改某些共享数据,而且它想要唤醒所有正在等待的线程,则应使用 pthread_cond_broadcast 调用,如下所示:

pthread_cond_broadcast(&mycond);

在某些情况下,活动线程只需要唤醒第一个正在睡眠的线程。假设您只对队列添加了一个工作作业。那么只需要唤醒一个工作程序线程(再唤醒其它线程是不礼貌的!):

pthread_cond_signal(&mycond);

此函数只唤醒一个线程。

参见

POSIX 线程[维基百科]:https://zh.wikipedia.org/wiki/POSIX%E7%BA%BF%E7%A8%8B

POSIX 线程详解:http://www.ibm.com/developerworks/cn/linux/thread/posix_thread1/#icomments

POSIX 线程详解,第 2部分:https://www.ibm.com/developerworks/cn/linux/thread/posix_thread2/

示例

https://github.com/sbxfc/objc/tree/master/threads/pthread/pthread

thread

Comments