您现在的位置是:首页 >技术教程 >C多线程、锁、同步、信号量网站首页技术教程
C多线程、锁、同步、信号量
一 线程函数
1.1 创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
thread 是线程变量地址
attr是线程属性,一般为NULL
start_rount 是函数指针
arg 是函数指针指向函数的参数
1.2 线程退出
void pthread_exit(void *retval);
retval可以把退出值带回去,例子见线程回收
1.3 线程回收
int pthread_join(pthread_t thread, void **retval);
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
struct Test
{
int num;
int age;
};
void* func(void* arg)
{
struct Test *temp = (struct Test*)arg;
for(int i = 0 ;i < 5; i++)
{
printf("in_sonthread i = %d
",i);
}
temp->age = 12;
temp->num = 21;
pthread_exit(temp);
return NULL;
}
int main()
{
pthread_t pid;
struct Test jxk;
struct Test* jxktemp = &jxk;
void* ans;
pthread_create(&pid,NULL,func,&jxk);
for(int i = 0; i < 5 ;i++)
{
printf("in_main,i = %d
",i);
}
pthread_join(pid,&ans);
struct Test* jxk2 = (struct Test*)ans;
printf("num is %d,age is %d
",jxk2->num,jxk2->age);
return 0;
}
1.4 线程分离:
某些情况下,程序的主线程有自己的其他业务,如果让主线程负责子线程的资源回收,调用pthrad_join()只要子线程不退出,主线程就会一致阻塞,主线程的任务也不能执行了。
线程库提供了线程分离函数 pthread_detach(),调用这个函数后指定的子线程可以和主线程分离,当子线程退出是,其占用的内核资源就被操作系统的其他进程接管并回收了。线程分离后,在主线程中使用pthread_join() 就会收不到子线程资源了。
int pthread_detach(pthread_t thread);
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
struct Test
{
int num;
int age;
};
void* func(void* arg)
{
struct Test *temp = (struct Test*)arg;
for(int i = 0 ;i < 5; i++)
{
printf("in_sonthread i = %d
",i);
}
temp->age = 12;
temp->num = 21;
printf("子线程:%ld
",pthread_self());
pthread_exit(temp);
return NULL;
}
int main()
{
pthread_t pid;
struct Test jxk;
pthread_create(&pid,NULL,func,&jxk);
printf("主线程:%ld
",pthread_self());
pthread_detach(pid);
pthread_exit(NULL);
return 0;
}
主线程打印了自己的id后便退出了线程,子线程会继续运行
1.5 其他线程函数
1.5.1 线程取消
线程取消就是在某些特定情况下,在一个线程中杀死另一个线程。使用这个函数杀死一个线程需要分两步:
- 在线程A中调用线程取消函数 pthread_cancel,指定杀死线程B,这时候线程B是死不了的
- 在线程B中进行一个系统调用(从用户区切换到内核区),否则线程B可以一直运行。
int pthread_cancel(pthread_t thread);
1.5.2 线程ID比较
在Linux中,线程ID本质就是一个无符号长整型,因此可以直接使用比较操作符比较两个线程的ID。但是线程库是可以跨平台使用的,在某些平台上 pthread_t 可能不是一个单纯的整型,这种情况下比较两个线程的ID必须要使用线程比较函数。
int pthread_equal(pthread_t t1, pthread_t t2);
二 线程同步
假设有4个线程ABCD,当前一个线程A对内存中的共享资源进行访问的时候,其他线程BCD都不可以对这块内存进行操作,直到线程A对这块内存访问完毕为止,BCD中的一个才能访问这块内存,剩下的两个需要继续阻塞等待,以此类推,直到所有的线程都完成对这块内存的操作。
线程内对这块内存的访问方式就称之为线程同步。所谓线程同步不是说多个线程同时对内存进行访问,而是按照先后顺序依次进行的。
例子:两个线程数数字
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
int Number;
void* func1(void* arg)
{
for(int i = 0; i < 50; i++)
{
int cur = Number;
cur++;
usleep(100);
Number = cur;
printf("thread A %ld, number is %d
",pthread_self(),Number);
}
return NULL;
}
void* func2(void* arg)
{
for(int i = 0; i < 50; i++)
{
int cur = Number;
cur++;
Number = cur;
usleep(50);
printf("thread B %ld, number is %d
",pthread_self(),Number);
}
return NULL;
}
int main()
{
pthread_t pidA,pidB;
pthread_create(&pidA,NULL,func1,NULL);
pthread_create(&pidB,NULL,func2,NULL);
printf("main thread %ld
",pthread_self());
sleep(1);
}
2.1 互斥锁
2.1.1定义
pthread_mutex_t mutex;
2.1.2 初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
//attr 是锁属性,一般为NULL
restrict 是一个关键字,用来修饰指针,只有这个关键字修饰的指针可以访问指向的内存地址,其他指针都不行。
// p = mutex, p也不能访问mutex的地址
2.1.3 销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
2.1.4 加锁 、 常试锁、解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//不会阻塞,会返回一个错误号
int pthread_mutex_unlock(pthread_mutex_t *mutex);
2.1.5 互斥锁使用
解决上诉数数问题:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
int Number;
pthread_mutex_t numMutex;
void* func1(void* arg)
{
for(int i = 0; i < 50; i++)
{
pthread_mutex_lock(&numMutex);
int cur = Number;
cur++;
Number = cur;
printf("thread A %ld, number is %d
",pthread_self(),Number);
pthread_mutex_unlock(&numMutex);
usleep(100);
}
return NULL;
}
void* func2(void* arg)
{
for(int i = 0; i < 50; i++)
{
pthread_mutex_lock(&numMutex);
int cur = Number;
cur++;
Number = cur;
printf("thread B %ld, number is %d
",pthread_self(),Number);
pthread_mutex_unlock(&numMutex);
usleep(50);
}
return NULL;
}
int main()
{
pthread_t pidA,pidB;
if(pthread_mutex_init(&numMutex,NULL) != 0)
{
fprintf(stderr,"init mutex lock failed;
");
return 0;
}
pthread_create(&pidA,NULL,func1,NULL);
pthread_create(&pidB,NULL,func2,NULL);
printf("main thread %ld
",pthread_self());
pthread_join(pidA,NULL);
pthread_join(pidB,NULL);
pthread_mutex_destroy(&numMutex);
}
2.2 死锁
造成死锁的场景:
多个线程访问共享资源时,需要加锁,如果锁使用不当,就会造成死锁这种现象。如果线程死锁造成的后果是:所有的线程被阻塞,并且进行的阻塞是无法解开的(能解开就不会被阻塞了);
- 加锁之后忘记解锁
- 加锁之后,在解锁之前,程序由其他出口跳出当前函数逻辑(异常、满足条件的return等)
- 重复加锁,造成死锁
2.3 如何避免死锁
- 避免多次锁定,多检查
- 对共享资源访问完毕之后,一定要解锁,或者在加锁的时候先使用trylock
- 如果程序中有多把锁,可以控制对锁的访问顺序(顺序访问资源、但在有些情况下做不到),另外也可以在对其他互斥锁做加锁操作之前,先释放当前线程拥有的互斥锁。
- 项目程序可以引入一些专门用于死锁检测的模块。
2.4 读写锁
2.4.1 基本信息(特点和记录信息)
读写锁是互斥锁的升级版,在做读操作的时候可以提高程序的执行效率,如果所有的线程都是做读操作,那么读是并行的,但是使用互斥锁,读操作也是串行的。
**读写锁是一把锁。**类型是 pthrad_rwlock_t,有了类型就可以创建一把互斥锁了。
pthread_rwlock_t rwlock;
这把锁记录了这些信息:
- 锁的状态:锁定/打开
- 锁定的是什么操作:读操作/写操作,使用读写锁锁定读操作,需要先解锁才能去锁定写操作,反之亦然
- 哪个线程把这把锁锁上了。
读写锁的特点:
- 使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的。
- 使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的。
- 使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问两个临界区,访问写锁临界区的线程继续运行,访问读锁的临界区线程阻塞,因为写锁的有点急比读锁高。
2.4.2 类型定义 、 初始化函数 和 销毁函数
//类型
pthread_rwlock_t rwlock;
//初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
//rwlock,读写锁地址
//attr 读写锁属性,一般为NULL
//销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
2.4.3 读锁 和 尝试读函数
//在程序中进行读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//调用这个函数,如果读写锁是打开的,那么加锁成功;
//如果读写锁已经锁定了读操作,依然可以加锁成功,因为读锁是共享的;
//如果读写锁已经锁定了写操作,调用这个函数会被阻塞。
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//调用这个函数,如果读写锁是打开的,那么加锁成功;
//如果读写锁已经锁定了读操作,依然可以加锁成功,因为读锁是共享的;
//如果读写锁已经锁定了写操作,调用这个函数加锁失败,但是线程不会被阻塞,可以在程序中对函数返回值进行判断,添加加锁失败后的处理动作。
2.4.4 写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//调用者函数,如果读写锁是打开的,那么加锁成功;
//如果已经锁定了读操作 或 写操作,调用这个函数线程会阻塞。
2.4.5 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
2.4.6 读写锁的使用
例:8个线程同时操作一个全局变量,三个线程不定时写资源,5个线程不定时读资源。
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
int Number;
pthread_rwlock_t rwLock;
void* readNum(void* arg)
{
for(int i = 0; i < 50; i++)
{
pthread_rwlock_rdlock(&rwLock);
printf("Thread read, id = %ld, number = %d
",pthread_self(),Number);
pthread_rwlock_unlock(&rwLock);
usleep(rand() % 5);
}
return NULL;
}
void* writeNum(void* arg)
{
for(int i = 0; i < 50; i++)
{
pthread_rwlock_wrlock(&rwLock);
int cur = Number;
cur++;
Number = cur;
printf("Thread write, id = %ld, number = %d
",pthread_self(),Number);
pthread_rwlock_unlock(&rwLock);
usleep(5);
}
return NULL;
}
int main()
{
pthread_t pidA[5],pidB[3];
if(pthread_rwlock_init(&rwLock,NULL) != 0)
{
fprintf(stderr,"init mutex lock failed;
");
return 0;
}
for(int i = 0; i < 5; i++)
{
pthread_create(&pidA[i],NULL,readNum,NULL);
}
for(int i = 0; i < 3; i++)
{
pthread_create(&pidB[i],NULL,writeNum,NULL);
}
//阻塞,资源回收
for(int i = 0; i < 5; i++)
{
pthread_join(pidA[i],NULL);
}
for(int i = 0; i < 3; i++)
{
pthread_join(pidB[i],NULL);
}
pthread_rwlock_destroy(&rwLock);
printf("main thread %ld
",pthread_self());
return 0;
}
2.5 条件变量
严格意义上说,条件变量的主要作用不是用来处理线程同步,而是进行线程的阻塞。
2.5.1 数据类型、初始化 和 释放
pthread_cond_t cond;
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
//cond 条件变量地址
//attr 条件变量属性,一般为NULL
int pthread_cond_destroy(pthread_cond_t *cond);
2.5.2 wait 和 timewait
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);
// 只会阻塞一定的时间长度,时间过了之后就会继续往下执行
time_t mytim = time(NULL);
struct timespec tmsp;
tmsp.tv_nsec = 0;
tmsp.tv_sec = tume(NULL) + 100; //线程阻塞100s
2.5.3 signal 和 broadcast
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
调用以上两个函数的任意一个,都可以唤醒被 pthread_cond_wait 或者 pthread_cond_timewait 阻塞的线程;
区别在于:
- pthread_cond_signal 是唤醒最少一个被阻塞的线程(总个数不定);
- pthread_cond_broadcast是唤醒所有被阻塞的线程。
2.6 信号量
信号量用在多线程多任务同步的,一个线程完成了某一动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一资源,而是流程上的概念,比如:有AB两个线程,B线程要等A线程完成某一个任务后在进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。
信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程或消费者线程的运行,
类型为 sem_t ,对应头文件是 <semaphore.h>
#include<semaphore.h>
sem_t sem;
2.6.1 类型定义、初始化 和 销毁
//类型定义
sem_t sem;
//初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
// sem:信号量变量地址
// pshared:
// 0:线程同步
// 非0:进程同步
//value: 初始化当前信号量拥有的资源(>=0,如果资源数为0,线程就会被阻塞)
//销毁
int sem_destroy(sem_t *sem);
2.6.2 sem_wai 和 sem_trywait
//调用函数嗲用sem中的资源树就会消耗一个,资源数 -1
int sem_wait(sem_t* sem);
当调用这个函数,并且sem中的资源数>0,线程不会阻塞,线程会占用sem中的一个资源,因此资源数-1,直到sem中的资源减为0时,资源被耗尽,因此线程也就阻塞了。
//调用函数嗲用sem中的资源树就会消耗一个,资源数 -1
int sem_trywait(sem_t* sem);
当调用这个函数,并且sem中的资源数>0,线程不会阻塞,线程会占用sem中的一个资源,因此资源数-1,直到sem中的资源减为0时,资源被耗尽,线程不会阻塞,直接返回错误,因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况。
2.6.3 sem_timedwait
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};
2.6.4 sem_getvalue
int sem_getvalue(sem_t *sem, int *sval);
2.6.5 sem_post
int sem_post(sem_t *sem);
2.6.6 使用
例子1:
一个空位,不会出现问题,但是当生产着刚开始给的value大时会出问题,因为访问了公共资源。
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
#include<semaphore.h>
struct Node
{
int number;
struct Node* next;
};
struct Node* head = NULL;
sem_t semp,semc;
void* consumerFunc(void *)
{
while(1)
{
//查看是否可以消费
sem_wait(&semc);
//消费
struct Node* node = head;
printf("消费者,id : %ld, number:%d
",pthread_self(),node->number);
head = head->next;
free(node);
//消费结束,提示生产者,继续生产
sem_post(&semp);
sleep(rand() % 3);
}
}
void* producerFuc(void *)
{
while(1)
{
//看是否可以生产
sem_wait(&semp);
//生产资源
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->number = rand() % 1000;
newNode->next = head;
head = newNode;
printf("生产者,id: %ld,number: %d
",pthread_self(),newNode->number);
//提示消费者可以消费了
sem_post(&semc);
sleep(rand() % 3);
}
return NULL;
}
int main()
{
pthread_t consumer[5],producer[5];
//初始化信号量,只有一个空位供给生产消费
sem_init(&semp,0,1);
sem_init(&semc,0,0);
//创建线程
for(int i = 0; i < 5; i++)
{
pthread_create(&consumer[i],NULL,producerFuc,NULL);
}
for(int i = 0; i < 5; i++)
{
pthread_create(&producer[i],NULL,consumerFunc,NULL);
}
//阻塞回收
for(int i = 0; i < 5; i++)
{
pthread_join(consumer[i],NULL);
}
for(int i = 0; i < 5; i++)
{
pthread_join(producer[i],NULL);
}
//信号量释放
sem_destroy(&semp);
sem_destroy(&semc);
return 0;
}
加锁解决问题:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
#include<semaphore.h>
struct Node
{
int number;
struct Node* next;
};
struct Node* head = NULL;
sem_t semp,semc;
pthread_mutex_t mutex;
void* consumerFunc(void *)
{
while(1)
{
//查看是否可以消费
sem_wait(&semc);
pthread_mutex_lock(&mutex);
//加在 sem_wait下,避免死锁
//消费
struct Node* node = head;
printf("消费者,id : %ld, number:%d
",pthread_self(),node->number);
head = head->next;
free(node);
pthread_mutex_unlock(&mutex);
//消费结束,提示生产者,继续生产
sem_post(&semp);
sleep(rand() % 3);
}
}
void* producerFuc(void *)
{
while(1)
{
//看是否可以生产
sem_wait(&semp);
pthread_mutex_lock(&mutex);
//生产资源
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->number = rand() % 1000;
newNode->next = head;
head = newNode;
printf("生产者,id: %ld,number: %d
",pthread_self(),newNode->number);
pthread_mutex_unlock(&mutex);
//提示消费者可以消费了
sem_post(&semc);
sleep(rand() % 3);
}
return NULL;
}
int main()
{
pthread_t consumer[5],producer[5];
//初始化信号量,只有一个空位供给生产消费
sem_init(&semp,0,1);
sem_init(&semc,0,0);
//初始化mutex
pthread_mutex_init(&mutex,NULL);
//创建线程
for(int i = 0; i < 5; i++)
{
pthread_create(&consumer[i],NULL,producerFuc,NULL);
}
for(int i = 0; i < 5; i++)
{
pthread_create(&producer[i],NULL,consumerFunc,NULL);
}
//阻塞回收
for(int i = 0; i < 5; i++)
{
pthread_join(consumer[i],NULL);
}
for(int i = 0; i < 5; i++)
{
pthread_join(producer[i],NULL);
}
//信号量释放
sem_destroy(&semp);
sem_destroy(&semc);
pthread_mutex_destroy(&mutex);
return 0;
}
加锁是加在了sem_wait下,这样可以避免死锁,不然会出现一种情况:
A线程拿到了sem的资源,但是B线程先锁定阻塞在了mutex位置,就会导致最后生产着无法生产,消费者无法消费,死锁了。