1. 可重入函数
- 如果一个函数访问了全部变量,静态局部变量,还有堆里的内容,那么这个函数就是不可重入函数。
- 线程之间是异步的,如果T1,T2都要访问进程数据段的资源(比如T1写,T2读),会造成不确定性。这时的T1,T2对应的函数都是不可重入函数。所以需要用线程同步来解决这种问题。
2. 临界资源
2.1 临界资源
2.2 临界区
3. 线程同步的三种方式
3.1 互斥锁(mutex)
- 互斥锁是一种互斥设备,用来保证共享数据操作的完整性。互斥锁类型的对象标记用来保证在任何时刻,只能有一个线程访问该对象。
- 互斥锁类型:pthread_mutex_t
下面是和互斥锁类型相关的操作。
3.1.1 pthread_mutex_init(3)
3.1.2 pthread_mutex_destroy(3)
3.1.2 pthread_mutex_lock(3)
3.1.3 pthread_mutex_trylock(3)
3.1.4 pthread_mutex_unlock(3)
3.1.5 代码示例
#include "t_stdio.h"
#include <pthread.h>
#include <unistd.h>
// 定义一个mutex锁类型的变量,不能写在main函数里,因为这样 mutex 就是局部变量了,handle 函数里是访问不到
pthread_mutex_t mutex;
int val = 0; // 全局变量在数据段
//线程的执行函数
void *handle(void *arg){
int tmp;
for(int i = 0; i < 100; i++){
// 访问到全局变量再加锁, 尽量缩小锁的范围
// 加锁
pthread_mutex_lock(&mutex);
tmp = val;
tmp++;
printf("tip:%lu\ttmp=%d\n",pthread_self(), tmp);
val = tmp;
// 解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(void){
// 初始化mutex锁
pthread_mutex_init(&mutex, NULL);
// 创建两个线程
pthread_t tid, pid;
pthread_create(&tid, NULL, handle, NULL);
// sleep(2);
pthread_create(&pid, NULL, handle, NULL);
// 等待线程的汇合
pthread_join(tid, NULL);
pthread_join(pid, NULL);
// 销毁mutex锁
pthread_mutex_destroy(&mutex);
return 0;
}
$ gcc mutex.c -lpthread
$ ./a.out
tip:140704450934528 tmp=1
tip:140704450934528 tmp=2
....
# 两个线程交替执行,第一个线程执行完把锁释放,第二个线程就可以执行
tip:139724164568832 tmp=100
tip:139724156176128 tmp=101
....
tip:140704442541824 tmp=200
3.2 条件变量 (condition variable)
- 条件不满足的情况下,线程会停下来,不会占用CPU(释放处理器)。以便其他的线程继续使用CPU。这不是盲等。
- 条件变量为真时,发送给 等待条件变量为真 的线程。条件变量不满足时,就等待条件变量满足。等两种方式: 死等,和规定等待时间。
3.2.1 pthread_cond_t
下面是 pthread_cond_t 类型的相关操作函数
3.2.2 pthread_cond_init(3)
3.2.3 pthread_cond_destory(3)
3.2.4 pthread_cond_signal(3)
3.2.5 pthread_cond_boradcast(3)
3.2.6 pthreaad_cond_wait(3)
3.2.7 pthread_cond_timewait(3)
3.2.8 实例分析、代码示例
1. 生产者线程负责生产一个新节点,然后把新节点插入到链表头部。
- 向链表中添加新节点的时候,有两种情况,链表头为空和头不为空.(head 和 new都是节点的地址)
- head == NULL头为空时,head = new;
- head != NULL头不为空时,head里的值是一个节点的地址(head已经指向了一个节点)。这时只需要让new所指向节点的next变为head指向节点的地址,然后再把head的值变为new的值。new就成了新的head.
- new -> next = head;
- head = new;
- 第二种情况的处理方法也能用于第一种,只不过这样的话,最后链表中最后一个节点的next是NULL
2. 消费者线程负责从链表头部摘取一个节点。
- 从链表头部摘取一个节点:
- 取:tmp = head;
- 摘:head = head -> next;
cond.c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
// 定义一个链表节点类型
typedef struct node{
int data;
struct node *next;
}node_t;
// 定义链表头指针
typedef node_t *list_t;
list_t head =NULL;
// 定义锁类型的变量
pthread_mutex_t mutex;
//定义条件变量
pthread_cond_t cond;
// "生产者线程"
void *product(void *arg){
node_t *new = NULL;
while(1){
// 生产一个新节点
new = (node_t *)malloc(sizeof(node_t));
new->data = rand()%10+1;
new->next = NULL;
printf("p: %d\n", new->data);
// 访问到全局变量,加锁
pthread_mutex_lock(&mutex);
//并插入到链表的头部
new -> next = head;
head = new;
// 解锁
pthread_mutex_unlock(&mutex);
// 告知那些等待条件变为真的线程
pthread_cond_signal(&cond);
sleep(rand()%3+1);
}
return NULL;
}
// "消费者线程"
void *consume(void *arg){
node_t *tmp;
while(1){
// 加锁
pthread_mutex_lock(&mutex);
// 从链表头部摘取一个节点.
while (head == NULL) {
// pthread_cond_wait 条件不满足的情况下,线程会停下来, 然后解锁,等到条件变为真。解锁是原子操作,不解锁的话 product 是无法访问head的
// 等条件变量为真时(即等product中 pthread_cond_signal(&cond); 执行以后,条件变为真,且head不再是NULL),
// 重新加锁,不会在进入这个循环, 除非等消费完了,head=NULL
pthread_cond_wait(&cond, &mutex);
}
tmp = head;
head = head -> next;
// 解锁
pthread_mutex_unlock(&mutex);
printf("c: %d\n", tmp->data);
// 消费该节点
free(tmp);
tmp = NULL;
sleep(rand()%3+1);
}
return NULL;
}
int main(void) {
srand(time(NULL));
// 初始化锁类型的变量
pthread_mutex_init(&mutex, NULL);
// 初始化条件变量
pthread_cond_init(&cond, NULL);
// 创建两个线程,“生产者” “消费者”
pthread_t pid, cid;
pthread_create(&pid, NULL, product, NULL);
pthread_create(&cid, NULL, consume, NULL);
// 等待线程汇合
pthread_join(pid, NULL);
pthread_join(pid, NULL);
// 销毁锁
pthread_mutex_destroy(&mutex);
// 销毁条件变量
pthread_cond_destroy(&cond);
return 0;
}
$ gcc cond.c -lpthread
$ ./a.out
# 生产
p: 2
# 消费, 消费的一定是前面生产的
c: 2
p: 3
c: 3
p: 8
c: 8
# 连着生产了两次
p: 2
p: 10
# 又碰巧连着生产了两次
c: 10
c: 2
p: 3
p: 10
c: 10
c: 3
3.3 信号量
下面是有关信号量类型sem_t的操作函数
3.3.1 sem_init(3)
- pshared 非0用于多进程间共享数据的保护。
3.3.2 sem_destroy(3)
3.3.3 sem_post(3)
3.3.4 sem_wait(3)
3.3.5 sem_trywait(3)
3.3.6 sem_timedwait(3)
3.3.7 实例分析、代码示例
队列: 先进先出
生产者线程实现:
- 生产者线程中 sem_wait(&p) 看下可p是不是0。如果是就不能再生产了。然后就阻塞等待,消费者线程中消费以后,sem_post(&p) 给可生产数 p加1;
- 这样p就大于0了,生产者线程就可以继续往下执行,给p减1后,给que[n] 一个随机数
- n=(n+1)%7; 当n+1==7 时,n==0,限制生产
- 最后sem_post(&c) 给可消费数加1, 这是如果消费者线程sem_wait(&c),正等着,那么它将被唤醒,给c减1,继续消费
s
消费者线程实现 : - 消费者线程中 sem_wait(&c), 如果c是 0,就不能再消费了,然后就阻塞等待。等待生产者线中的sem_post(&c)给c加1
- 然后消费者线程就可以继续执行,给c减1后, que[m] = -1,相当于消费了,-1不在1-500随机数里。
- 最后sem_post(&p) 给生产数加1, 如果生产者线程中 sem_wait(&p) 正等着,那么它将被唤醒,给p减1,继续生产
sem.c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <semaphore.h>
// 定义队列
int que[7];
// 定义信号量类型变量, 分别用于生产者(可生产数量),消费者(可消费数量)
sem_t p, c;
// "生产者线程"
void *product(void *arg){
int n = 0;
while(1){
// 如果p的值=0,阻塞。p不=0就减1继续执行
sem_wait(&p);
que[n] = rand()%500+1;
printf("p: index: %d\tvalue: %d\n",n, que[n]);
n = (n+1)%7;
// 可消费数量加1
sem_post(&c);
sleep(rand()%3+1);
}
return NULL;
}
// "消费者线程"
void *consume(void *arg){
int m = 0; int tmp;
while(1){
sem_wait(&c);
tmp = que[m];
printf("c: index: %d\tvalue: %d\n",m, que[m]);
que[m] = -1;
m = (m+1)%7;
// 可生产数量加1
sem_post(&p);
sleep(rand()%3+1);
}
return NULL;
}
int main(void) {
srand(time(NULL));
// 初始化信号量,指定信号量初始值
sem_init(&p, 0, 7);
sem_init(&c, 0, 0);
// 创建两个线程,“生产者” “消费者”
pthread_t pid, cid;
pthread_create(&pid, NULL, product, NULL);
pthread_create(&cid, NULL, consume, NULL);
// 等待线程汇合
pthread_join(pid, NULL);
pthread_join(pid, NULL);
// 销毁信号量
sem_destroy(&p);
sem_destroy(&c);
return 0;
}
$ gcc sem.c -lpthread
$ ./a.out
p: index: 0 value: 176
c: index: 0 value: 176
p: index: 1 value: 115
c: index: 1 value: 115
p: index: 2 value: 281
c: index: 2 value: 281