线程控制
Linux 中的线程是指轻量级的执行单元,相比于进程,具有以下特点:
(1)进程(Process)是正在执行的程序的实例。每个进程都有自己的地址空间、代码段、数据段和打开的文件描述符等资源。线程(Thread)是进程内的一个执行单元,它共享相同的地址空间和其他资源,包括文件描述符、信号处理等,但每个线程都有自己的栈空间。
(2)由于共享地址空间和数据段,同一进程的多线程之间进行数据交换比进程间通信方便很多,但也由此带来线程同步问题。
(3)同一进程的多线程共享大部分资源,除了每个线程独立的栈空间。这代表线程的创建、销毁、切换要比进程的创建、销毁、切换的资源消耗小很多,所以多线程比多进程更适合高并发。
线程创建
1. pthread_create
● #include<pthread.h>
● int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void*), void*arg);
pthread_t类型用来保存线程的唯一标识符。pthread_t类型在#include <pthreadtypes.h>中
线程终止
线程终止有以下几种方法:
- 线程函数执行return语句;
- 线程函数内部调用pthread_exit函数;
a. 线程调用pthread_exit方法后,该线程会被关闭(相当于return)。线程可以通过retval向其它线程传递信息,主线程中可以通过pthread_join获得需要的返回值。 - 其他线程调用pthread_cancel函数。
int pthread_join(pthread_t thread, void**retval);
该函数用来等待进程结束,并且若retVal不为NULL则获取返回值。
int pthread_detach(pthread_t thread);
该函数用来将进程标记为detach状态,进程默认是join状态,表示该进程资源要由主进程回收,但是若主进程没有调用pthread_join回收资源,则可能会造成资源浪费。而该函数可以将进程标识为detach状态,表示如果线程执行完毕,则自动回收,不需要主进程回收。
pthread_detach不会等待子线程结束,如果在后者执行完毕之前主线程退出,则整个进程退出,子线程被强制终止。
int pthread_cancel(pthread_t thread);
该函数用来取消线程,线程是否接受取消的请求,以及如果取消,是立刻取消还是延时取消,这个都需要设置。
● int pthread_setcancelstate(int state, int *oldstate);
设置线程是否可以取消
● int pthread_setcanceltype(int type, int *oldtype);
设置线程取消模式
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <math.h>
#include <string.h>
void *func(void *arg)
{
char *data = (char*)arg;
printf("%s\n",data);
int *ret = (int*)malloc(sizeof(int));
ret[0] = 22;
pthread_exit((void*)ret);
}
int main()
{
pthread_t pid;
char ch[] = "xiancheng";
pthread_create(&pid,NULL,func,ch);
int *ret;
pthread_join(pid,(void**)&ret);
printf("data is:%d",*ret);
return 0;
}
线程同步
当多个线程并发访问和修改同一个共享资源(如全局变量)时,如果没有适当的同步措施,就会遇到线程同步问题。这种情况下,程序最终的结果依赖于线程执行的具体时序,导致了竞态条件。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <math.h>
#include <string.h>
#define PTHREADSIZE 2000
void* func(void *arg)
{
int *data = arg;
(*data)++;
return NULL;
}
int main()
{
// 2000
pthread_t pid[PTHREADSIZE];
int count = 0;
for(int i=0;i<PTHREADSIZE;i++)
{
pthread_create(&pid[i],NULL,func,(void*)&count);
}
for(int i=0;i<PTHREADSIZE;i++)
{
pthread_join(pid[i],NULL);
}
printf("%d\n",count);
return 0;
}
以上这段代码在多次运行后,我们发现结果有时候是2000,有时候是1998,有时候是1997等等。这就是没有线程同步导致的。
想要解决线程同步问题,我们必须给资源加锁,使同一时间操作特定资源的线程只有一个。
常见的锁
互斥锁
● pthread_mutex_t mutex;
使用操作:初始化pthread_mutex_init,锁定pthread_mutex_lock,尝试锁定pthread_mutex_lock,解锁pthread_mutex_unlock,销毁pthread_mutex_destory
我们常常并不是使用初始化函数来初始化锁,而是使用一个宏来默认初始化锁
static pthread_mutex_t counter_mutex= PTHREAD_MUTEX_INITIALIZER;
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <math.h>
#include <string.h>
#define PTHREADSIZE 20000
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* func(void *arg)
{
int *data = arg;
pthread_mutex_lock(&mutex);
(*data)++;
pthread_mutex_unlock(&mutex);
return NULL;
}
int main()
{
// 2000
pthread_t pid[PTHREADSIZE];
int count = 0;
for(int i=0;i<PTHREADSIZE;i++)
{
pthread_create(&pid[i],NULL,func,(void*)&count);
}
for(int i=0;i<PTHREADSIZE;i++)
{
pthread_join(pid[i],NULL);
}
printf("%d\n",count);
return 0;
}
当我们改写代码后,我们就实现了线程的同步。在某些情况下,确实需要显式销毁互斥锁资源。如果互斥锁是动态分配的(使用pthread_mutex_init函数初始化),或者互斥锁会被跨多个函数或文件使用,不再需要时必须显式销毁它。但对于静态初始化,并且在程序结束时不再被使用的互斥锁(上述程序中的counter_mutex),显式销毁不是必需的。
锁类型 初始化方式 作用域 生命周期管理
全局/静态锁 静态初始化(宏) 全局或静态作用域 自动销毁
局部锁(栈) 动态初始化(函数) 函数内部 显式销毁
动态分配的锁(堆) 动态初始化(函数) 任意作用域 显式销毁并释放内存
读写锁
● pthread_rwlock_t
使用操作:初始化pthread_rwlock_init(),摧毁pthread_rwlock_destroy(),上读锁pthread_rwlock_rdlock(),上写锁pthread_rwlock_wrlock(),解锁pthread_rwlock_unlock()。
- 基本操作实验
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_rwlock_t rwlock;
int shared_data = 0;
void *pthread_read(void *argv)
{
pthread_rwlock_rdlock(&rwlock);
printf("This is %s,read data is %d\n",(char*)argv,shared_data);
pthread_rwlock_unlock(&rwlock);
return NULL;
}
void *pthread_write(void *argv)
{
int temp = shared_data+1;
sleep(1);
shared_data = temp;
printf("This is %s,write data is %d\n",(char*)argv,shared_data);
return NULL;
}
int main()
{
pthread_rwlock_init(&rwlock,NULL);
pthread_t write1,write2,read1,read2,read3,read4,read5,read6;
pthread_create(&write1,NULL,pthread_write,(void*)"write1");
pthread_create(&write2,NULL,pthread_write,(void*)"write2");
sleep(2);
pthread_create(&read1,NULL,pthread_read,(void*)"read1");
pthread_create(&read2,NULL,pthread_read,(void*)"read2");
pthread_create(&read3,NULL,pthread_read,(void*)"read3");
pthread_create(&read4,NULL,pthread_read,(void*)"read4");
pthread_create(&read5,NULL,pthread_read,(void*)"read5");
pthread_create(&read6,NULL,pthread_read,(void*)"read6");
pthread_join(write1,NULL);
pthread_join(write2,NULL);
pthread_join(read1,NULL);
pthread_join(read2,NULL);
pthread_join(read3,NULL);
pthread_join(read4,NULL);
pthread_join(read5,NULL);
pthread_join(read6,NULL);
return 0;
}
当我们只加读锁的时候,不加写锁,为了当我们read的时候,write全都工作完毕,我们在代码中加入了sleep,为了看出不加读锁的后果,我们在write中加了sleep,并且把加法操作拆分成了非原子操作。
This is write2,write data is 1
This is write1,write data is 1
This is read1,read data is 1
This is read3,read data is 1
This is read2,read data is 1
This is read4,read data is 1
This is read5,read data is 1
This is read6,read data is 1
当我们给写操作添加了读写锁之后,在观察输出结果
void *pthread_write(void *argv)
{
pthread_rwlock_wrlock(&rwlock);
int temp = shared_data+1;
sleep(1);
shared_data = temp;
printf("This is %s,write data is %d\n",(char*)argv,shared_data);
pthread_rwlock_unlock(&rwlock);
return NULL;
}
This is write1,write data is 1
This is write2,write data is 2
This is read6,read data is 2
This is read1,read data is 2
This is read3,read data is 2
This is read5,read data is 2
This is read2,read data is 2
This is read4,read data is 2
下面我们再改动一下:把sleep操作给删除掉,打乱创建write与read的顺序
pthread_create(&read1,NULL,pthread_read,(void*)"read1");
pthread_create(&read2,NULL,pthread_read,(void*)"read2");
pthread_create(&read3,NULL,pthread_read,(void*)"read3");
pthread_create(&write2,NULL,pthread_write,(void*)"write2");
pthread_create(&read4,NULL,pthread_read,(void*)"read4");
pthread_create(&read5,NULL,pthread_read,(void*)"read5");
pthread_create(&read6,NULL,pthread_read,(void*)"read6");
此时,我们观察输出结果,我们发现,当多次运行后,会产生不同的答案。也就是说此时读写进程执行的顺序是不确定的。
注意:线程的执行顺序是由操作系统内核调度的,其运行规律并不简单地为“先创建先执行”。
2. 饥饿实验
当我们给read操作加上一个sleep(1)之后,我们再运行代码,我们会发现,不论怎么运行代码,write与read可能顺序不确定,但是所有的read操作都是连在一起的。这种现象我们就叫作读饥饿,同样也有写饥饿。
Linux提供了可以修改的属性pthread_rwlockattr_t,默认情况下,属性中指定的策略为“读优先”,当写操作阻塞时,读线程依然可以获得读锁,从而在读操作并发较高时导致写饥饿问题。我们可以尝试将策略更改为“写优先”,当写操作阻塞时,读线程无法获取锁,避免了写线程持有锁的时间持续延长,使得写线程获取锁的等待时间显著降低,从而避免写饥饿问题。
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, intpref);
int main(){
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
// 设置写优先
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
pthread_rwlock_init(&rwlock, &attr);
pthread_rwlockattr_destroy(&attr);
}
自旋锁
在Linux内核中,自旋锁是一种用于多处理器系统中的低级同步机制,主要用于保护非常短的代码段或数据结构,以避免多个处理器同时访问共享资源。自旋锁相对于其他锁的优点是它们在锁被占用时会持续检查锁的状态(即“自旋”),而不是让线程进入休眠。这使得自旋锁在等待时间非常短的情况下非常有效,因为它避免了线程上下文切换的开销。
自旋锁主要用于内核模块或驱动程序中,避免上下文切换的开销。不能在用户空间使用。
条件变量
线程间切换函数:
当需要两个线程之间协同工作的时候,此时我们可以使用条件变量来实现。
下面,我们来学习几个函数:
等待函数
将自身阻塞,并释放锁的函数
1.int pthread_cond_wait(pthread_cond_t *restrictcond, pthread_mutex_t *restrictmutex);
● 返回值:成功返回0,失败返回错误码
● 调用该方法的线程必须持有mutex锁。调用该方法的线程会阻塞并临时释放mutex锁,并等待其他线程调用pthread_cond_signal或pthread_cond_broadcast唤醒。被唤醒后该线程会尝试重新获取mutex锁。
2. int pthread_cond_timedwait(pthread_cond_t *restrictcond,pthread_mutex_t *restrictmutex, const struct timespec *restrictabstime);
● 返回值:成功返回0,失败返回ETIMEDOUT,其他失败返回对应的错误码。
● 该方法与第一个方法一样,但是添加了超时时间。
唤醒函数
将其他因该条件变量阻塞的线程唤醒,如果没有其他线程因为该条件变量阻塞,那么什么也不做
int pthread_cond_signal(pthread_cond_t *cond);
● 返回值:成功返回0,失败返回错误码
● 唤醒因cond而阻塞的线程,如果有多个线程因为cond阻塞,那么随机唤醒一个。如果没有线程在等待,这个函数什么也不做。int pthread_cond_broadcast(pthread_cond_t *cond);
● 返回值:成功返回0,失败返回错误码
● 唤醒所有正在等待条件变量cond的线程。如果没有线程在等待,这个函数什么也不做。
条件变量函数- pthread_cond_t定义变量
● 常用操作:初始化pthread_cond_init,等待pthread_cond_wait,定时等待pthread_cond_timedwait,信号pthread_cond_signal,广播pthread_cond_broadcast,销毁pthread_cond_destroy
当初始化全局/静态变量的时候,我们通常使用宏进行初始化:
static pthread_cond_tcond= PTHREAD_COND_INITIALIZER;
对于动态分配的条件变量(例如,通过malloc分配的条件变量),应使用pthread_cond_init函数进行初始化。
对于宏初始化的条件变量/锁,不需要显式销毁,对于函数初始化的条件变量/锁,需要显示销毁。
条件变量的自定义属性
如果需要自定义条件变量的属性(例如,改变其pshared属性以支持进程间同步),则需要使用pthread_cond_init和pthread_condattr_t类型的属性对象。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count;
// 定义两个独立条件变量
static pthread_cond_t cond_producer = PTHREAD_COND_INITIALIZER;
static pthread_cond_t cond_consumer = PTHREAD_COND_INITIALIZER;
// 生产者线程
void *producer(void *arg) {
int item = 1;
while (1) {
pthread_mutex_lock(&mutex);
while (count == BUFFER_SIZE) {
pthread_cond_wait(&cond_producer, &mutex); // 等待生产者条件
}
buffer[count] = item++;
item%=1024;
count++;
printf("白月光发送幸运数字 %d\n", buffer[count - 1]);
pthread_cond_signal(&cond_consumer); // 唤醒消费者
pthread_mutex_unlock(&mutex);
}
}
// 消费者线程
void *consumer(void *arg) {
while (1) {
pthread_mutex_lock(&mutex);
while (count == 0) {
pthread_cond_wait(&cond_consumer, &mutex); // 等待消费者条件
}
printf("收到幸运数字 %d\n", buffer[--count]);
pthread_cond_signal(&cond_producer); // 唤醒生产者
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_t pid_consumer,pid_producer;
count=0;
pthread_create(&pid_consumer,NULL,consumer,0);
pthread_create(&pid_producer,NULL,producer,0);
pthread_join(pid_consumer,NULL);
pthread_join(pid_producer,NULL);
return 0;
}
注意:
● 对于while (count == BUFFER_SIZE)判断条件,我们要使用循环判断,因为有时候可能会出现假唤醒。
● 对于两个工作线程来说,我们要使用两个条件变量,而不是共用一个,否则可能会发生死锁。
信号量
在Linux中,信号量是用来协调进程或线程的执行的,并不承担传输数据的职责。用来解决多个进程或线程间的同步与互斥问题。
信号量本质上是一个非负整数变量,可以被用来控制对共享资源的访问。它主要用于两种目的:互斥和同步。
基于不同的目的,信号量可以分为两类:用于实现互斥的“二进制信号量”和用于同步的“计数信号量”。
● 二进制信号量:只能是0/1
● 计数信号量:任意非0整数
在Linux中,根据是否具有唯一的名称,分为有名信号量(named semaphore)和无名信号量(unnamed semaphore)。这两种信号量特性有所不同:
● 有名信号量:
○ 有名信号量在系统范围内是可见的,可以在任意进程之间进行通信。它们通过名字唯一标识,这使得不同的进程可以通过这个名字访问同一个信号量对象。
● 无名信号量:
○ 无名信号量不是通过名称标识,而是直接通过sem_t结构的内存位置标识。无名信号量在使用前需要初始化,在不再需要时应该销毁。它们不需要像有名信号量那样进行创建和链接,因此设置起来更快,运行效率也更高。
在当前Linux系统中,有名信号量在临时文件系统中的对应文件位于/dev/shm目录下,创建它们时可以像普通文件一样设置权限模式,限制不同用户的访问权限
信号量主要提供了两个操作:P操作和V操作。P用来-1,V用来+1,若信号量为0,则阻塞。
无名信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
● pshared:指明信号量是线程间共享还是进程间共享的
○ 0: 信号量是线程间共享的,应该被置于所有线程均可见的地址(如,全局变量或在堆中动态分配的变量)
○ 非0: 信号量是进程间共享的,应该被置于共享内存区域,任何进程只要能访问共享内存区域,即可操作进程间共享的信号量
● 返回值:成功返回0,失败返回-1int sem_destroy(sem_t *sem);
int sem_post(sem_t *sem);
相当于V操作,给信号量+1int sem_wait(sem_t *sem);
相当于P操作,给信号量-1,若为0,则阻塞到有可用信号量- time_t time(time_t *tloc);将时间戳记录在tloc并且也作为返回值。
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <pthread.h>
int shard_num = 0;
sem_t unnamed_sem;
void *plusOne(void *argv)
{
sem_wait(&unnamed_sem);
int temp = shard_num+1;
shard_num=temp;
sem_post(&unnamed_sem);
}
int main()
{
pthread_t tid[10000];
sem_init(&unnamed_sem,0,1);
for(int i=0;i<10000;i++)
{
pthread_create(&tid[i],NULL,plusOne,NULL);
}
for(int i=0;i<10000;i++)
{
pthread_join(tid[i],NULL);
}
printf("sharm_num is %d\n",shard_num);
sem_destroy(&unnamed_sem);
return 0;
}
注意:线程比进程的资源共享程度更高,可以用于进程间通信的方式,通常也可以用于线程间通信。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
char *path = "sharememory";
int fd = shm_open(path,O_CREAT|O_RDWR,0664);
if(fd<0)
{
perror("shm_open");
exit(EXIT_FAILURE);
}
// adjust share memory size;
ftruncate(fd,sizeof(int));
int *shareNum = mmap(NULL,sizeof(int),PROT_WRITE|PROT_READ,MAP_SHARED,fd,0);
if(shareNum==NULL)
{
perror("mmap");
exit(EXIT_FAILURE);
}
*shareNum=0;
pid_t cpid = fork();
if(cpid<0)
{
perror("fork");
exit(EXIT_FAILURE);
}
else if(cpid==0)
{
// children
int temp = *shareNum+1;
sleep(1);
*shareNum = temp;
}
else
{
// father;
int temp = *shareNum+1;
sleep(1);
*shareNum = temp;
waitpid(cpid,NULL,0);
printf("this is father, child finished\n");
printf("the final value is %d\n",*shareNum);
}
close(fd);
if(-1==munmap(shareNum,sizeof(int)))
{
perror("munmap");
exit(EXIT_FAILURE);
}
if(cpid>0)
{
if(-1==shm_unlink(path))
{
perror("shm_unlink");
exit(EXIT_FAILURE);
}
}
return 0;
}
注意:
● 不论是主线程还是子线程,都需要close(fd)以及munmap(shareNum,sizeof(int))
● shm_unlink(path)函数只能在一个线程中调用,我们选择在父线程中调用。
结果:
我们发现,当我们运行的时候,结果是1,这就是发生了资源竞争,当其中一个线程在写的时候,另一个线程同时也进行写,那么这两个线程+1后的结果1样,看起来就好像有一个线程被吃掉了。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <semaphore.h>
int main()
{
char *path = "sharememory";
char *sem_path = "sem";
int fd = shm_open(path,O_CREAT|O_RDWR,0664);
int sem_fd = shm_open(sem_path,O_CREAT|O_RDWR,0664);
if(fd<0)
{
perror("shm_open");
exit(EXIT_FAILURE);
}
// adjust share memory size;
ftruncate(fd,sizeof(int));
ftruncate(sem_fd,sizeof(sem_t));
int *shareNum = mmap(NULL,sizeof(int),PROT_WRITE|PROT_READ,MAP_SHARED,fd,0);
sem_t *sem_shareNum = mmap(NULL,sizeof(sem_t),PROT_WRITE|PROT_READ,MAP_SHARED,sem_fd,0);
if(shareNum==NULL)
{
perror("mmap");
exit(EXIT_FAILURE);
}
if(sem_shareNum==NULL)
{
perror("sem_mmap");
exit(EXIT_FAILURE);
}
*shareNum=0;
sem_init(sem_shareNum,1,1);
pid_t cpid = fork();
if(cpid<0)
{
perror("fork");
exit(EXIT_FAILURE);
}
else if(cpid==0)
{
// children
sem_wait(sem_shareNum);
int temp = *shareNum+1;
sleep(1);
*shareNum = temp;
sem_post(sem_shareNum);
}
else
{
// father;
sem_wait(sem_shareNum);
int temp = *shareNum+1;
sleep(1);
*shareNum = temp;
sem_post(sem_shareNum);
waitpid(cpid,NULL,0);
printf("this is father, child finished\n");
printf("the final value is %d\n",*shareNum);
}
close(fd);
close(sem_fd);
munmap(sem_shareNum,sizeof(sem_t));
if(-1==munmap(shareNum,sizeof(int)))
{
perror("munmap");
exit(EXIT_FAILURE);
}
if(cpid>0)
{
shm_unlink(sem_path);
if(-1==shm_unlink(path))
{
perror("shm_unlink");
exit(EXIT_FAILURE);
}
}
return 0;
}
mmap详解
mmap函数是用来进行内存映射的,内存映射,简而言之就是将内核空间的一段内存区域映射到用户空间。映射成功后,用户对这段内存区域的修改可以直接反映到内核空间,相反,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间与用户空间两者之间需要大量数据传输等操作的话效率是非常高的。当然,也可以将内核空间的一段内存区域同时映射到多个进程,这样还可以实现进程间的共享内存通信。
mmap可以将某文件映射至内存(进程空间),如此可以把对文件的操作转为对内存的操作,以此避免更多的lseek()与read()、write()操作,这点对于大文件或者频繁访问的文件而言尤其受益。
需注意,直接对该段内存写时不会写入超过当前文件大小的内容。
采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。**对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。**实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,**数据内容一直保存在共享内存中,并没有写回文件。**共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
通常使用mmap()的三种情况: 提高I/O效率、匿名内存映射、共享内存进程通信。
使用方式:
fd=open(name, flag, mode); //open可以是shm_open等等一系列open
if(fd<0)
ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0);
无名信号量实际上可以用于任意进程间的通信,而不仅限于父子进程。在非父子进程通信时,共享资源的初始化和释放要格外注意,必须按照合理的顺序进行。
对于无名信号量在使用的时候,必须要保证sem_init的时候,必须要第二个参数为非零值,然后对于sem_t sem来说,他必须位于任何进程都能访问的区域例如共享内存,否则,sem就不会起作用。
信号量还能用来控制线程的执行顺序,使得线程按照一定的顺序执行,类似于当线程1执行完成后发送一个通知给线程2,线程2接收到通知之后,立刻开始运行,当执行完毕后发送一个通知给线程1。
二进制信号量和计数信号量的划分更多地是从控制效果来说的:二进制信号量起到了互斥锁的作用,当多个进程或线程访问共享资源时,确保同一时刻只有一个进程或线程进入了临界区,起到了“互斥”的作用;而计数信号量起到了“控制顺序”的作用,明确了“谁先执行”、“谁后执行”。
本例只展示了一个生产者和一个消费者在缓冲区为1时的协同工作,如果我们增加生产者和消费者的数量,信号量的取值范围自然就不再是0和1了。
有名信号量
有名信号量的名称形如/somename,是一个以斜线(/)打头,\0字符结尾的字符串,长度最长为251。
有名信号量通常用于进程间通信,这是因为线程间通信可以有更高效快捷的方式(全局变量等),不必“杀鸡用牛刀”。但要注意的是,正如上文提到的,可以用于进程间通信的方式通常也可以用于线程间通信。
#include <semaphore.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
char *sem_name = "/named_sem";
char *shm_name = "/named_sem_shm";
// 初始化有名信号量
sem_t *sem = sem_open(sem_name, O_CREAT, 0666, 1);
// 初始化内存共享对象
int fd = shm_open(shm_name, O_CREAT | O_RDWR, 0666);
// 调整内存共享对象的大小
ftruncate(fd, sizeof(int));
// 将内存共享对象映射到内存空间
int *value = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 初始化共享变量指针指向位置的值
*value = 0;
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
}
sem_wait(sem);
int tmp = *value + 1;
sleep(1);
*value = tmp;
sem_post(sem);
// 每个进程都应该在使用完毕后关闭对信号量的连接
sem_close(sem);
if (pid > 0)
{
waitpid(pid, NULL, 0);
printf("子进程执行结束,value = %d\n", *value);
// 有名信号量的取消链接只能执行一次
sem_unlink(sem_name);
}
// 父子进程都解除内存共享对象的映射,并关闭相应的文件描述符
munmap(value, sizeof(int));
close(fd);
// 只有父进程应该释放内存共享对象
if (pid > 0)
{
if (shm_unlink(shm_name) == -1)
{
perror("shm_unlink");
}
}
return 0;
}
注意:有名信号量需要使用sem_opensem_closesem_unlink等函数,而对比无名信号量来说
总结
● 对于无名信号量来说:
○ 线程间通信:sem_t semsem_initsem_waitsem_postsem_destroy
○ 进程间通信:sem_t sem定义在共享内存中sem_initsem_waitsem_post
■ 保证sem_t类型的变量对于任何进程来说都是可以访问的,保证sem_init的时候必须是多线程共享模式
● 对于有名信号量来说:
○ 进程间通信:sem_t *semsem_opensem_waitsem_postsem_closesem_unlink
■ sem_open返回一个sem_t类型的指针,要求path必须是以/开头,以’\0’结尾的字符串
■ /dev/shm路径下能够找到打开的sem_t类型的文件。
(1)可用于进程间通信的方式通常都可以用于线程间通信。
(2)无名信号量和有名信号量均可用于进程间通信,有名信号量是通过唯一的信号量名称在操作系统中唯一标识的。无名信号量用于进程间通信时必须将信号量存储在进程间可以共享的内存区域,作为内存地址直接在进程间共享。而内存区域的共享是通过内存共享对象的唯一名称来实现的。
(3)无名信号量和有名信号量都可以作为二进制信号量和计数信号量使用。
(4)二进制信号量和计数信号量的区别在于前者起到了互斥锁的作用,而后者起到了控制进程或线程执行顺序的作用。而不仅仅是信号量取值范围的差异。
(5)信号量是用来协调进程或线程协同工作的,本身并不用于传输数据。
线程池
线程池是一种用于管理和重用多个线程的设计模式。它通过维护一个线程池(线程的集合),可以有效地处理并发任务而无需每次都创建和销毁线程。这种方法可以减少线程创建和销毁的开销,提高性能和资源利用率。
Glib库线程池工作流程
(1)线程池创建:首先创建一个线程池,指定任务函数和其他参数。线程池会创建一定数量的线程,这些线程进入等待状态,准备执行任务,或在提交任务后才创建线程(取决于配置)。线程池中的所有任务执行的都是同一个任务函数。
(2)任务队列:线程池维护一个任务队列。当我们向线程池提交任务时,任务会被放入这个队列中。实际上,放入任务队列的是我们在提交任务时传递的任务数据。
(3)线程执行任务:线程池中的线程从任务队列中取出任务数据,然后调用任务函数,执行任务。执行完成后,线程不会退出,而是继续从任务队列中取下一个任务执行。如果没有待执行的任务,线程通常在等待一段时间后被回收(取决于具体的配置)。
相关类型
1. GFunc
a. typedef void(*GFunc)(gpointer data, gpointer user_data);
2. gpointer
a. typedef void *gpointer
3. gint
a. typedef int gint
4. gboolean
a. typedef gint gboolean
5. Gerror
struct GError{
GQuark domain;
gint code;
gchar *message;
}
记录已经发生的错误信息
6. GThreadPool 线程池对象
struct GThreadPool
{
GFunc func; // 线程池中执行的任务
gpointer user_data; // 线程池中共享的用户数据指针,会在每个任务函数调用的时候传递给任务函数
gboolean exclusive; // 标记当前线程池是否独占线程
}
相关函数
8. g_thread_pool_new
GThreadPool *g_thread_pool_new(GFunc func, gpointer user_data, gint max_threads, gboolean exclusive,GError **error);
用来创建一个线程池
● func:线程池中执行的函数
● user_data:func的参数
● max_threads:线程池的容量
● exclusive:线程是否独占标记位
● error:报告错误信息,可以为NULL
● GThreadPool* 线程池实例指针
9. g_thread_pool_push
gboolean g_thread_pool_push(GThreadPool *pool,gpointer data,GError **error);
● pool:线程池实例
● data:传递给每个任务的数据
● error:错误信息
● gboolean:成功返回TRUE,失败返回FALSE
10. g_thread_pool_free
void g_thread_pool_free (GThreadPool *pool,gboolean immediate,gboolean wait_);
● immediate:是否立刻释放线程,FALSE,TRUE
● wait_:当前函数是否阻塞等待所有任务完成?TRUE,FALSE
#include <glib.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void task_func(gpointer data, gpointer user_data)
{
int task_num = *(int*)data;
free(data);
printf("Executing task is %d...\n", task_num);
sleep(1);
printf("Task %d completed\n", task_num);
}
int main() {
// 创建线程池
GThreadPool *thread_pool = g_thread_pool_new(task_func, NULL, 5, TRUE, NULL);
// 向线程池添加任务
for (int i = 0; i < 10; i++) {
int *tmp = malloc(sizeof(int));
*tmp = i + 1;
g_thread_pool_push(thread_pool, tmp, NULL);
}
// 等待所有任务完成
g_thread_pool_free(thread_pool, FALSE, TRUE);
printf("All tasks completed\n");
return 0;
}
注意:编译此程序需要先安装sudo apt-get install libglib2.0-dev
执行命令的时候,执行gcc your_program.c -o output_name $(pkg-config --cflags --libs glib-2.0 gthread-2.0)