Linux 多线程编程(三)

1 线程安全

多线程编程环境中,多个线程同时调用某些函数可能会产生错误结果,这些函数称为非线程安全函数。如果库函数能够在多个线程中同时执行并且不会互相干扰,那么这个库函数就是线程安全( thread-safe)函数

2 互斥量

2.1 临界区

在计算机系统中有许多共享资源不允许用户并行使用。例如打印机设备,如果它同时进行两份文档打印,它的输出就会产生交错,从而都无法获得正确的文档。像打印机这样的共享设备被称为“ 排它性资源”,因为它一次只能由一个执行流访问。执行流必须以互斥的方式执行访问排它性资源的代码。
临界区是必须以互斥方式执行的代码段,也就是说在临界区的范围内只能有一个活动的执行线程。

2.2 什么是互斥量

互斥量( Mutex, 又称为互斥锁, 是一种用来保护临界区的特殊变量, 它可以处于锁定( locked) 状态, 也可以处于解锁( unlocked) 状态:

  • 如果互斥锁是锁定的, 就是一个特定的线程持有这个互斥锁;
  • 如果没有线程持有这个互斥锁,那么这个互斥锁就处于解锁状态。
每个互斥锁内部有一个线程等待队列,用来保存等待该互斥锁的线程。当互斥锁处于解锁状态时,一个线程试图获取这个互斥锁时, 这个线程就可以得到这个互斥锁而不会阻塞;当互斥锁处于锁定状态时,一个线程试图获取这个互斥锁时,这个线程将阻塞在互斥锁的等待线程队列内。


互斥量是最简单也是最有效的线程同步机制。程序可以用它来保护临界区,以获得对排它性资源的访问权。另外,互斥量只能被短时间地持有,使用完临界资源后应立即释放锁。

2.3 创建与销毁

2.3.1 创建互斥量

pthreads 使用 pthread_mutex_t 类型的变量来表示互斥量,同时在使用互斥量进行同步时需要先对它进行初始化,可以静态或动态方式对互斥量进行初始化。
1)静态初始化
对是静态分配的 pthread_mutex_t 变量来说值需要将 PTHREAD_MUTEX_INITIALIZER赋给变量就行了。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
2)动态初始化
对动态分配或者不使用默认互斥属性的互斥变量来说,需要调用 pthread_mutex_int()函数来执行初始化工作。 pthread_mutex_int()函数原型如下:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
参数 mutex 是一个指向要初始化的互斥量的指针;参数 attr 传递 NULL 来初始化一个带有默认属性的互斥量,否则就要用类似于线程属性对象所使用的方法,先创建互斥量属性对象,再用该属性对象来创建互斥量。

函数成功返回 0,否则返回一个非 0 的错误码, 下表列出 pthread_mutex_init 出错的错误码。
pthread_mutex_ nit 错误码表(0)
错误码 出错描述
EAGAIN 系统缺乏初始化互斥量所需的非内存资源
ENOMEM 系统缺乏初始化互斥量所需的内存资源
EPERM 调用程序没有适当的优先级
静态初始化程序通常比调用 pthread_mutex_init 更有效,而且在任何线程开始执行之前,确保变量被执行一次。

以下代码用来动态的初始化默认属性的互斥量 mylock
int error;
pthread_mutex_t mylock;
if (error = pthread_mutex_init(&mylock, NULL))
    fprintf(stderr, "Failed to initialize mylock : %s\n", strerror(error));

2.3.2 销毁互斥量

销毁互斥量使用 pthread_mutex_destroy()函数,原型如下:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数 mutex 指向要销毁的互斥量的指针。以下代码销毁了 mylock 互斥量:

int error;
pthread_mutex_t mylock;
if (error = pthread_mutex_destroy(&mylock))
    fprintf(stderr, "Failed to destroy mylock : %s\n", strerror(error));

2.4 加锁与解锁

2.4.1 加锁

线程试图锁定互斥量的过程称之为加锁
Pthreads 中有两个试图锁定互斥量的函数, pthread_mutex_lock()pthread_mutex_trylock()pthread_mutex_lock()函数会一直阻塞到互斥量可用为止,而 pthread_mutex_trylock()会尝试加锁,通常立即返回。函数原型如下:

int pthread_mutex_lock (pthread_mutex_t *mutex);
int pthread_mutex_trylock (pthread_mutex_t *mutex);
参数 mutex 是需要加锁的互斥量。函数成功返回 0,否则返回一个非 0 的错误码,其中另一个线程已持有锁的情况下,调用 pthread_mutex_trylock()函数是错误码为 EBUSY

2.4.2 解锁

解锁是线程将互斥量由锁定状态变为解锁状态。
pthread_mutex_unlock()函数用来释放指定的互斥量。函数原型如下:

int pthread_mutex_unlock (pthread_mutex_t *mutex);
参数 mutex 是需要加锁的互斥量。函数成功返回 0,否则返回一个非 0 的错误码。只有当线程需要进入临界区之前正确地获取适当的互斥量,并在线程离开临界区时释放互斥量。以下伪代码展示了互斥量保护临界区的基本用法:
pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mylock);
...临界区代码省略...
pthread_mutex_unlock(&mylock);

2.4.3 线程范例

下列程序清单是使用互斥量来保证多线程同时输出顺序例子,互斥量保证获取的线程打印完才让别的线程打印,杜绝了打印乱序的问题。
使用互斥量保护多线程同时输出
#include<stdio.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>

pthread_t tid[2];
pthread_mutex_t lock;

void* doPrint(void *arg)
{
	int id = (long)arg;
	int i = 0;
	 
	pthread_mutex_lock(&lock); /* 使用互斥量保护临界区 */
	printf("Job %d started\n", id);
	for (i = 0; i < 5; i++) 
	{
		printf("Job %d printing\n", id);
		usleep(10);
	}
	printf("Job %d finished\n", id);
	pthread_mutex_unlock(&lock);
	return NULL;
}

int main(void)
{
	long i = 0;
	int err;

	if (pthread_mutex_init(&lock, NULL) != 0) /* 动态初始化互斥量 */
	{
		printf("\n Mutex init failed\n");
		return 1;
	}
	
	while(i < 2)
	{
		err = pthread_create(&(tid[i]), NULL, &doPrint, (void*)i);
		if (err != 0)
		printf("Can't create thread :[%s]", strerror(err));
		i++;
	}
	
	pthread_join(tid[0], NULL);
	pthread_join(tid[1], NULL);
	pthread_mutex_destroy(&lock);

	return 0;
}
下图是上面程序清单的运行结果,可以看到 Job 0 先获取互斥锁并进行打印,所以Job 0 需要打印的完成后才让 Job 1 打印。

下图是将上面程序清单中修改为不使用互斥量(注释第 14 21 行)的输出,可见输出为乱序。


2.5 死锁和避免

2.5.1 什么是死锁

死锁是指两个或两个以上的执行序在执行过程中, 因争夺资源而造成的一种互相等待的现象。例如: 一个线程 T1 已锁定了一个资源 R1, 又想去锁定资源 R2,而此时另一个线程 T2 已锁定了资源 R2,却想去锁定资源R1,两个线程都想得到对方的资源,而不愿释放自己的资源,造成两个线程都在等待,而无法执行的情况,如下图所示。

                                                    死锁发生示意图

下面程序清单示例了死锁发生的情况,程序创建了两个线程,第一个线程先获取 mutexA锁,再获取 mutexB 锁;第二个线程先获取 mutexB 后获取 mutexA,这时死锁就可能发生。
死锁产生的范例
#include<stdio.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>

pthread_t tid[2];
pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER; /* 静态初始化互斥量 */
pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;

void * t1(void *arg) {
	pthread_mutex_lock(&mutexA); /* 线程 1 获取 mutexA */
	printf("t1 get mutexA\n");
	usleep(1000);
	pthread_mutex_lock(&mutexB); /* 线程 1 获取 mutexB */
	printf("t1 get mutexB\n");
	pthread_mutex_unlock(&mutexB); /* 线程 1 释放 mutexB */
	printf("t1 release mutexB\n");
	pthread_mutex_unlock(&mutexA); /* 线程 1 释放 mutexA */
	printf("t1 release mutexA\n");
	return NULL;
}

void * t2(void *arg) {
	pthread_mutex_lock(&mutexB);
	printf("t2 get mutexB\n");
	usleep(1000);
	pthread_mutex_lock(&mutexA);
	printf("t2 get mutexA\n");
	pthread_mutex_unlock(&mutexA);
	printf("t2 release mutexA\n");
	pthread_mutex_unlock(&mutexB);
	printf("t2 release mutexB\n");
	return NULL;
}

int main(void) {
	int err;
	err = pthread_create(&(tid[0]), NULL, &t1, NULL ); /* 创建线程 1 */
	
	if (err != 0)
		printf("Can't create thread :[%s]", strerror(err));
	err = pthread_create(&(tid[1]), NULL, &t2, NULL); /* 创建线程 2 */
	
	if (err != 0)
		printf("Can't create thread :[%s]", strerror(err));
	
	pthread_join(tid[0], NULL);
	pthread_join(tid[1], NULL);
	return 0;
}
下图为程序清单的运行结果, t1 线程获取 mutexA 后等待 mutexBt2 线程获取mutexB 后等待 mutexA,两个线程互相等待,进入死锁。

                        

2.5.1 死锁的避免

当多个线程需要相同的一些锁, 但是按照不同的顺序加锁, 死锁就很容易发生, 如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。 例如,规定程序内有三个互斥锁的加锁顺序为 mutexA->mutexB->mutexC,则线程 t1t2t3 线程操作伪代码如下所示:
t1                         t2                         t3
lock(mutexA)         lock(mutexA)                  lock(mutexB)
lock(mutexB)         lock(mutexC)                  lock(mutexC)
lock(mutexC)

3 条件变量

3.1 为什么需要条件变量 

在多线程编程中仅使用互斥锁来完成互斥是不够用的, 如以下情形:
假设有两个线程
t1 t2, 需要这个两个线程循环对一个共享变量 sum 进行自增操作,那么 t1 t2 只需要使用互斥量即可保证操作正确完成,线程执行代码如所示:

pthread_mutex_t sumlock= PTHREAD_MUTEX_INITIALIZER;
void * t1t2(void) {
    pthread_mutex_lock(&sumlock);
    sum++;
    pthread_mutex_unlock(&sumlock);
}

如果这时需要增加另一个线程 t3,需要 t3 count 大于 100 时将 count 值重新置 0 值,那么可以 t3 可以实现如下:

void * t3 (void) {
    pthread_mutex_lock(&sumlock);
    if (sum >= 100) {
        sum = 0;
        pthread_mutex_unlock(&sumlock);
    } else {
        pthread_mutex_unlock(&sumlock);
        usleep(100);
    }
}
以上代码存在以下问题:
    1) sum 在大多数情况下不会到达 100 , 那么对 t3 的代码来说 , 大多数情况下, 走的是 else 分支, 只是 lock unlock, 然后 sleep() 。 这浪费了 CPU 处理时间。
    2) 为了节省 CPU 处理时间, t3 会在探测到 sum 没到达 100 的时候 usleep() 一段时间。这样却又带来另外一个问题, 亦即 t3 响应速度下降。 可能在 sum 到达 200 的时候, t3 才会醒过来。
    这样时间与效率出现了矛盾,而条件变量就是解决这个问题的好方法。


3.2 创建与销毁

3.2.1 创建条件变量

Pthreads pthread_cond_t 类型的变量来表示条件变量。程序必须在使用 pthread_cond_t 变量之前对其进行初始化。

1) 静态初始化

对于静态分配的变量可以简单地将 PTHREAD_COND_INITIALIZER 赋值给变量来初始化默认行为的条件变量。

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

2)动态初始化

对动态分配或者不使用默认属性的条件变量来说可以使用 pthread _cond_init()来初始化。函数原型如下:
int pthread_cond_init (pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

参数 cond 是一个指向需要初始化 pthread_cond_t 变量的指针,参数 attr 传递 NULL 值时, pthread_cond_init()cond 初始化为默认属性的条件变量。 

函数成功将返回 0;否则返回一个非 0 的错误码。

静态初始化程序通常比调用 pthread_cond_init()更有效,而且在任何线程开始执行之前,确保变量被执行一次。


以下代码示例了条件变量的初始化。

pthread_cond_t cond;
int error;
if (error = pthread_cond_init(&cond, NULL));
    fprintf(stderr, "Failed to initialize cond : %s\n", strerror(error));
3.2.2 销毁条件变量
函数 pthread_cond_destroy()用来销毁它参数所指出的条件变量,函数原型如下:
int pthread_cond_destroy(pthread_cond_t *cond);
函数成功调用返回 0 ,否则返回一个非 0 的错误码。以下代码演示了如何销毁一个条件变量。
pthread_cond_t cond;
int error;
if (error = pthread_cond_destroy(&cond))
    fprintf(stderr, "Failed to destroy cond : %s\n", strerror(error));


3.3 等待与通知

3.3.1 等待

条件变量是与条件测试一起使用的,通常线程会对一个条件进行测试,如果条件不满足就会调用条件等待函数来等待条件满足。
条件等待函数有 pthread_cond_wait()pthread_cond_timedwait()和两个,函数原型如下:

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
    pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

    pthread_cond_wait()函数在条件不满足时将一直等待, 而 pthread_cond_timedwait()将只等待一段时间。 

参数 cond 是一个指向条件变量的指针,参数 mutex 是一个指向互斥量的指针,线程在调用前应该拥有这个互斥量,当线程要加入条件变量的等待队列时,等待操作会使线程释放这个互斥量。 pthread_timedwait()的第三个参数 abstime 是一个指向返回时间的指针,如果条件变量通知信号没有在此等待时间之前出现,等待将超时退出, abstime 是个绝对时间,而不是时间间隔。

以上函数成功调用返回 0,否则返回非 0 的错误码,其中 pthread_cond_timedwait() 函数如果 abstime 指定的时间到期,错误码为 ETIMEOUT

以下代码使得线程进入等待,直到收到通知并且满足 a 大于等于 b 的条件。

pthread_mutex_lock(&mutex)
while(a < b)
    pthread_cond_wait(&cond, &mutex)
pthread_mutex_unlock(&mutex)

3.3.2 通知

当另一个线程修改了某参数可能使得条件变量所关联的条件变成真时,它应该通知一个或者多个等待在条件变量等待队列中的线程。
条件通知函数有 pthread_cond_signal()pthread_cond_broadcast()函数,其中 pthread_cond_signal 函数可以唤醒一个在条件变量等待队列等待的线程,而 pthread_cond_broadcast函数可以所有在条件变量等待队列等待的线程。函数原型如下:

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
参数 cond 是一个指向条件变量的指针。函数成功返回 0 ,否则返回一个非 0 的错误码。


3.3.3 线程范例

条件变量范例
#include<stdio.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>

pthread_t tid[3];
int sum = 0;
pthread_mutex_t sumlock = PTHREAD_MUTEX_INITIALIZER; /* 静态初始化互斥量 */
pthread_cond_t cond_sum_ready = PTHREAD_COND_INITIALIZER; /* 静态初始化条件变量 */

void * t1t2(void *arg)
{
	int i;
	long id = (long)arg;
	
	for (i = 0; i < 60; i++) 
	{
		pthread_mutex_lock(&sumlock); /* 使用互斥量保护临界变量 */
		sum++;
		printf("t%ld: read sum value = %d\n", id + 1 , sum);
		pthread_mutex_unlock(&sumlock);
		if (sum >= 100)
			pthread_cond_signal(&cond_sum_ready); /* 发送条件通知,唤醒等待线程 */
	}
	return NULL;
}
void * t3(void *arg) 
{
	pthread_mutex_lock(&sumlock);
	while(sum < 100) /* 不满足条件将一直等待 */
	pthread_cond_wait(&cond_sum_ready, &sumlock); /* 等待条件满足 */
	sum = 0;
	printf("t3: clear sum value\n");
	pthread_mutex_unlock(&sumlock);
	return NULL;
}

int main(void) 
{
	int err;
	long i;
	for (i = 0; i < 2; i++) 
	{
		err = pthread_create(&(tid[i]), NULL, &t1t2, (void *)i); /* 创建线程 1 线程 2 */
		if (err != 0) 
		{
			printf("Can't create thread :[%s]", strerror(err));
		} 
	}
	err = pthread_create(&(tid[2]), NULL, &t3, NULL); /* 创建线程 3 */
	if (err != 0)
		printf("Can't create thread :[%s]", strerror(err));
	for (i = 0; i < 3; i++)
		pthread_join(tid[i], NULL);
	
	return 0;
}

上面程序清单的可能运行结果如下所示, sum 累加到 100 时发送条件通知,但程序结果中 sum 计算到 103 时, t3 才被调用,这是因为 signal wait 调用之间有间隙存在。

t1: read sum value = 1
t1: read sum value = 2
...
t2: read sum value = 100
t1: read sum value = 101
t1: read sum value = 102
t1: read sum value = 103
t3: clear sum value
t2: read sum value = 1
.......
t2: read sum value = 17



猜你喜欢

转载自blog.csdn.net/faihung/article/details/80530772