1 互斥锁:上锁与解锁
-
互斥锁用于保护临界区(的共享数据),以保证任何时刻只有一个线程在执行其中的代码。
lock_the_mutex(...); 临界区 unlock_the_mutex(...);
-
下列3个函数给一个互斥锁上锁和解锁:
#include <pthread.h> // 成功则返回0,否则返回证得Exxx值 int pthread_mutex_lock(pthread_mutext_t *mptr); int pthread_mutex_trylock(pthread_mutex_t *mptr); int pthread_mutex_unlock(pthread_mutex_t *mptr);
若尝试给一个已由另外某个线程锁住的互斥锁上锁,则pthread_mutex_lock将阻塞到该互斥锁解锁为止。
2 生产者-消费者问题
- 生产者-消费者问题,即有界缓冲区问题。一个或多个生产者(进程或线程)创建一个个数据条目,然后由一个或多个消费者(线程或进程)处理。数据条目在生产者和消费者之间使用某种类型的IPC传递。
- 若生产者超前消费者(即管道被填满),内核就在生产者调用write时把它置于休眠状态,直到管道中有空余空间;反之,亦然。
- 隐式同步:【上述即为隐式同步】生产者与消费者不知道内核在执行同步。【Posix消息队列、System V消息队列】
- 显式同步:【共享内存区】
生产者-消费者例子:多个生产者线程、单个消费者线程
- Set_concurrency(nthreads):告诉线程系统我们希望并发运行多少线程
#include "unpipc.h"
#define MAXNITEMS 1000000
#define MAXNTHREADS 100
int nitems; // read-only by producer and consumer
struct
{
pthread_mutex_t mutex;
int buff[MAXNITEMS];
int nput; // nput是buff数组中下一次存放的元素下标
int nval; // nval是下一次存放的值(0、1、2等)
} shared = {
PTHREAD_MUTEX_INITIALIZER
};
// 在main函数前先声明
void *produce(void *), *consume(void *);
int main(int argc, char const *argv[])
{
int i, nthreads, count[MAXNTHREADS];
// 在tid_produce保存每个线程的线程ID
pthread_t tid_produce[MAXNTHREADS], tid_consume;
if (argc != 3)
err_quit("usage: prodcons2 <#items> <#threads>");
nitems = min(atoi(argv[1]), MAXNITEMS); // 指定生产者存放的条目数
nthreads = min(atoi(argv[2]), MAXNTHREADS); // 指定待创建生产者的数目
// 告诉线程系统我们希望并发运行多少线程
Set_concurrency(nthreads);
// start all the producer threads
for (i = 0; i < nthreads; ++i)
{
count[i] = 0; // 计数器初始化为0,若线程存放数据条目,则加1
Pthread_create(&tid_produce[i], NULL, produce, &count[i]);
}
// wait for all the producer threads
for (i = 0; i < nthreads; ++i)
{
// 等待所有生产者线程终止,同时输出每个线程的计数器值
Pthread_join(tid_produce[i], NULL);
printf("count[%d] = %d\n", i, count[i]);
}
// start, then wait for the consumer thread
Pthread_create(&tid_consume, NULL, consume, NULL);
Pthread_join(tid_consume, NULL);
exit(0);
}
void *produce(void *arg)
{
while (true) {
pthread_mutex_lock(&shared.mutex);
if (shared.nput >= nitems) {
Pthread_mutex_unlock(&shared.mutex);
return(NULL);
}
shared.buff[shared.nput] = shared.nval;
shared.nput++;
shared.nval++;
Pthread_mutex_unlock(&shared.mutex);
// count 元素的增加不属于临界区,因为每个线程有各自的计数器(main函数的count数组)
*((int *) arg) += 1;
}
}
void *consume(void *arg)
{
int i;
for (i = 0; i < nitems; ++i)
{
if (shared.buff[i] != i)
printf("buff[%d] = %d\n", i, shared.buff[i]);
}
return(NULL);
}
3 对比上锁与等待
-
使用互斥锁,使得生产者产生数据的同时,消费者线程就能处理它。
#include "unpipc.h" #define MAXNITEMS 1000000 #define MAXNTHREADS 100 int nitems; // read-only by producer and consumer struct { pthread_mutex_t mutex; int buff[MAXNITEMS]; int nput; // nput是buff数组中下一次存放的元素下标 int nval; // nval是下一次存放的值(0、1、2等) } shared = { PTHREAD_MUTEX_INITIALIZER }; // 在main函数前先声明 void *produce(void *), *consume(void *); int main(int argc, char const *argv[]) { int i, nthreads, count[MAXNTHREADS]; // 在tid_produce保存每个线程的线程ID pthread_t tid_produce[MAXNTHREADS], tid_consume; if (argc != 3) err_quit("usage: prodcons2 <#items> <#threads>"); nitems = min(atoi(argv[1]), MAXNITEMS); // 指定生产者存放的条目数 nthreads = min(atoi(argv[2]), MAXNTHREADS); // 指定待创建生产者的数目 // Create all producers and one consumer Set_concurrency(nthreads + 1); // start all the producer threads for (i = 0; i < nthreads; ++i) { count[i] = 0; // 计数器初始化为0,若线程存放数据条目,则加1 Pthread_create(&tid_produce[i], NULL, produce, &count[i]); } Pthread_create(&tid_consume, NULL, consume, NULL); // wait for all the producer threads for (i = 0; i < nthreads; ++i) { // 等待所有生产者线程终止,同时输出每个线程的计数器值 Pthread_join(tid_produce[i], NULL); printf("count[%d] = %d\n", i, count[i]); } Pthread_join(tid_consume, NULL); exit(0); } void *produce(void *arg) { while (true) { pthread_mutex_lock(&shared.mutex); if (shared.nput >= nitems) { Pthread_mutex_unlock(&shared.mutex); return(NULL); } shared.buff[shared.nput] = shared.nval; shared.nput++; shared.nval++; Pthread_mutex_unlock(&shared.mutex); *((int *) arg) += 1; } }
-
必须等到生产者生产了第i个条目,否则一直while循环,即轮询。
当生产者未生产第i个条目时,可以使用另外一种类型的同步,允许一个线程(或进程)休眠 到某个事件发生为止。
void consume_wait(int i) { while (true) { Pthread_mutex_lock(&shared.mutex); if (i < shared.nput) { Pthread_mutex_unlock(&shared.mutex); return; } Pthread_mutex_unlock(&shared.mutex); } } void *consume(void *arg) { int i; for (i = 0; i < nitems; ++i) { consume_wait(i); if (shared.buff[i] != i) printf("buff[%d] = %d\n", i, shared.buff[i]); } return(NULL); }
4 条件变量:等待与信号发送
-
互斥锁用于上锁,条件变量则用于等待。这两种不同类型的同步都是需要的。
-
条件变量是类型为pthread_cond_t的变量,以下两个函数使用了这些变量:
// 成功则返回0,否则返回正的Exxx值 #include <pthread.h> int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr); int pthread_cond_signal(pthread_cond_t *cptr);
这两个函数所等待或由之得以通知的“条件”,其定义由我们选择。每个条件变量总是有一个互斥锁与之关联。我们使用pthread_cond_wait等待某个条件为真时,还会指定其条件变量的地址和所关联的互斥锁的地址。
-
put结构包含:互斥锁变量mutex以及与之相关的两个变量nput和nval;互斥锁初始化为PTHREAD_MUTEX_INITIALIZER
struct { pthread_mutex_t mutex; int nput; // nput是buff数组中下一次存放的元素下标 int nval; // nval是下一次存放的值(0、1、2等) } put = { PTHREAD_MUTEX_INITIALIZER };
- nready结构包含:一个计数器、一个条件变量、一个互斥锁。互斥锁初始化为PTHREAD_MUTEX_INITIALIZER, 条件变量初始化为PTHREAD_COND_INITIALIZER
struct { pthread_mutex_t mutex; pthread_cond_t cond; int nready; // number ready for consumer } nready = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER };
5 互斥锁和条件变量的属性
-
以非默认属性初始化互斥锁和条件变量。
-
初始化或摧毁互斥锁和条件变量:
// 成功则返回0,否则返回正的Exxx值 #include <pthread.h> int pthread_mutex_init(pthread_mutex_t *mptr, const pthread_mutexattr_t *attr); int pthread_mutex_destroy(pthread_mutex_t *mptr); int pthread_cond_init(pthread_cond_t *cptr, const pthread_condattr_t *attr); int pthread_cond_destroy(pthread_cond_t *cptr);
-
初始化或摧毁互斥锁属性的数据类型pthread_mutexattr_t:
// 成功则返回0,否则返回正的Exxx值 #include <pthread.h> int pthread_mutexattr_init(pthread_mutexattr_t *attr); int pthread_mutexattr_destroy(pthread_mutexattr_t *attr); int pthread_condattr_init(pthread_condattr_t *attr); int pthread_condattr_destroy(pthread_condattr_t *cptr);
一旦某个互斥锁属性对象或某个条件变量属性对象已被初始化,就通过调用不同函数启用或禁止特定的属性。这个属性是用一下函数获得或存入的:
// 成功则返回0,否则返回正的Exxx值 #include <pthread.h> int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *valptr); int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int value); int pthread_condattr_getpshared(const pthread_condattr_t *attr, int *valptr); int pthread_condattr_setpshared(pthread_condattr_t *attr, int value);
6 小结
- 有时候一个线程获得某个互斥锁后,发现自己需要等待某个条件为真。则该线程就可以等待在某个条件变量上。条件变量总是有一个互斥锁与之关联。
- 把调用线程置于休眠状态的pthread_cond_wait函数在这么做之前先给所关联的互斥锁解锁,以后某个时刻唤醒该线程前再给该互斥锁上锁。
- 该条件变量由另外某个线程向它发送信号,而这个发送信号的线程既可以只唤醒一个线程(pthread_cond_signal),也可以唤醒等待相应条件变为真的所有线程(pthread_cond_broadcast)。