项目学习地址:【牛客网C++服务器项目学习】
day08
-
函数:void pthread_exit(void *retval);
-
功能:终止调用这个函数的线程。如果是一个进程的最后一个线程调用该函数,那么该进程会执行调用exit(3)终止该进程。此外,一个线程终止了,并不会影响线程共享的资源:文件描述符等。
-
参数:
- retval:这个参数是传递出去给另一个函数Pthread_join的
-
返回值:没有返回值,这个函数总会执行成功
-
-
函数:int pthread_equal(pthread_t t1, pthread_t t2);
- 功能:比较两个线程的ID是否相同。不同的操作系统对于线程ID的数据类型是不一样的额,有些是用int,有些则是结构体。
- 返回值:相同返回非0值,不相同返回0
-
函数:pthread_t pthread_self(void);
- 功能:返回调用此函数的线程的ID,这个ID和函数Pthread_create是一样的
-
函数:int pthread_join(pthread_t thread, void **retval);
-
功能:阻塞函数,等待thread线程号对应的线程退出结束,回收资源。
-
参数:
- thread:需要回收的线程号
- retval:从线程thread处回收的资源,用一个二级指针接收。
-
返回值:成功回收,返回0;执行失败,返回一个 错误号
-
-
函数:int pthread_detach(pthread_t thread);
- 功能:将线程号为thread的线程,标记为detach线程。该线程结束退出后,系统会自动回收它的资源,不再需要join函数了
- 返回值:成功分离,返回0;执行失败,返回一个错误号
-
函数:int pthread_cancel(pthread_t thread);
- 功能:取消一个线程。在取消点取消。
- 返回值:成功,返回0;执行失败,返回一个错误号
-
函数:int pthread_attr_init(pthread_attr_t *attr);
- 功能:初始化线程属性attr
- 返回值:成功,返回0;执行失败,返回一个错误号
-
函数:int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
- 功能:获取线程的栈大小(8MB)
- 返回值:成功,返回0;执行失败,返回一个错误号
一个进程可以创建多少个线程呢?
32位的linux,默认配置下大概是300个
64位的linux,理论个数为1000多w个,实际上会系统参数的性能的限制,我的服务器也就是1w1多个
Linux操作系统里一个进程最多可以创建多少个线程?
1.线程同步问题
-
互斥锁
- 函数:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
-
功能:创建并初始化互斥锁,上锁、解锁。
-
lock函数:每当有线程对一个互斥量进行加锁,互斥量计数加1;每当有线程对一个互斥量解锁,互斥量计数减1。只有当该互斥量的计数等于0时,该线程才有资格访问被锁住的内容;否则就是原地阻塞。如果互斥量计数为0,还继续解锁,将会返回一个错误。
-
UNLOCK函数:对互斥量解锁,当互斥量的计数等于0,由操作系统决定,调度哪一个因为互斥量被阻塞的线程。
-
trylock函数:lock函数和tryLock函数都是用于锁定对象,但他们之间有一定的区别:lock函数是阻塞的,因为它调用WaitForSingleObject函数时传递的第二个参数是INFINITE,表示无限等待下去,所以是阻塞的。tryLock函数时非阻塞的,调用后立即返回。因为它调用WaitForSingleObject函数时传递的第二个参数是0,表示不等待,立即返回。
-
-
返回值:成功返回0,失败返回错误号。
-
读写锁:
在进行读操作的时候加的锁:
pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
在进行写操作的时候加的锁:
pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
对读/写统一进行解锁:
pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
-
读写锁其实还是一种锁,是给一段临界区代码加锁,但是此加锁是在进行写操作的时候才会互斥,而在进行读的时候是可以共享的进行访问临界区的 ps:读写锁本质上是一种自旋锁
-
有时候,在多线程中,有一些公共数据修改的机会比较少,而读的机会却是非常多的,此公共数据的操作基本都是读,如果每次操作都给此段代码加锁,太浪费时间了而且也很浪费资源,降低程序的效率,因为读操作不会修改数据,只是做一些查询,所以在读的时候不用给此段代码加锁,可以共享的访问,只有涉及到写的时候,互斥的访问就好了
-
读写之间是互斥的—–>读的时候写阻塞,写的时候读阻塞,而且读和写在竞争锁的时候,写会优先得到锁
-
自旋锁的优缺点
优点:效率高,避免了线程之间调度的开销
缺点:浪费CPU资源
挂起等待锁的优缺点
优点: 不会浪费CPU的资源,比较灵活
缺点:效率不高,很可能会使临界区的代码不被任何线程执行,因为可能会是线程被CPU调度走了但是却没有被调度回来
2.生产者/消费者模型:
- 生产者与消费者问题也称有界缓冲区问题,两个进程共享一个固定大小的缓冲区,生产者放入信息,消费者拿出信息,当缓冲区已满时,生产者睡眠,待消费者取出时再唤醒生产者;当缓冲区为空时,消费者睡眠,待生产者放入数据时再唤醒它
- 关于生产者/消费者模型的具体细节不再赘述,可以百度相关解答。
3.条件变量:
-
条件变量是利用线程间共享的全局变量进行同步的一种机制。 主要包括两个动作:
-
一个线程等待”条件变量的条件成立”而挂起;
-
另一个线程使”条件成立”(给出条件成立信号)。
-
为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。 条件变量类型为 pthread_cond_t。
-
-
函数:int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
- 功能:用条件cond阻塞当前线程,等待别的线程用signal唤醒(讲不清楚。。。这个函数的API文档,描述的太长了)
-
函数:
- 功能:pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。
4.信号量
-
一、什么是信号量
- 线程的信号量与进程间通信中使用的信号量的概念是一样,它是一种特殊的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作。如果一个程序中有多个线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行。
- 而只有0和1两种取值的信号量叫做二进制信号量,在这里将重点介绍。而信号量一般常用于保护一段代码,使其每次只被一个执行线程运行。我们可以使用二进制信号量来完成这个工作
-
二、信号量的接口和使用
-
信号量的函数都以sem_开头,线程中使用的基本信号量函数有4个,它们都声明在头文件 semaphore.h中。
-
1、sem_init函数:该函数用于创建信号量,其原型如下:
- int sem_init(sem_t *sem, int pshared, unsigned int value);
- 该函数初始化由sem指向的信号对象,设置它的共享选项,并给它一个初始的整数值。pshared控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享,value为sem的初始值。调用成功时返回0,失败返回-1.
-
2、sem_wait函数:该函数用于以原子操作的方式将信号量的值减1。原子操作就是,如果两个线程企图同时给一个信号量加1或减1,它们之间不会互相干扰。它的原型如下:
- int sem_wait(sem_t *sem);
- sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.
-
3、sem_post函数:该函数用于以原子操作的方式将信号量的值加1。它的原型如下:
- int sem_post(sem_t *sem);
- 与sem_wait一样,sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.
-
4、sem_destroy函数:该函数用于对用完的信号量的清理。它的原型如下:
- int sem_destroy(sem_t *sem);
- 成功时返回0,失败时返回-1.
-
关于信号量,自己在linux0.11上实现过自定义的信号量函数,使用开关中断完成函数的原子操作。文章连接
——————
2021/12/12更新
写了一个小demo,测试了互斥锁和读写锁,对大量读操作的效率差别:
/*
此程序是为了比较
互斥锁和读写锁的在 高并发读操作上的效率差异
创建两个进程,父进程中使用互斥锁读取,子进程使用读写锁读取
每个进程中国,创建10个读线程,对全局变量区的某个变量进行读取
每个线程读取 COUNT 次,总计完成10*COUNT次读取
利用 gettimeofday 函数,对整个读取过程精确到微秒的计时
*/
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <time.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <stdlib.h>
//创建互斥锁
pthread_mutex_t mutex;
//创建读写锁
pthread_rwlock_t rwlock;
//全局变量,父子进程、线程间都是相同的
int data = 123456;
//读取次数
long COUNT = 1000000;
//互斥读函数
void *mFunc(void *arg)
{
int read = 0;
for (long i = 0; i < COUNT; ++i)
{
//加锁
pthread_mutex_lock(&mutex);
read = data;
//解锁
pthread_mutex_unlock(&mutex);
}
//结束线程
pthread_exit(NULL);
}
//读写锁读
void *rwFunc(void *arg)
{
int read = 0;
for (long i = 0; i < COUNT; ++i)
{
//加锁
pthread_rwlock_rdlock(&rwlock);
read = data;
//解锁
pthread_rwlock_unlock(&rwlock);
}
//结束线程
pthread_exit(NULL);
}
int main()
{
//创建两个进程,父进程中使用互斥锁读取,子进程使用读写锁读取
int pid = fork();
if (pid > 0)
{
//parent process
//初始化互斥锁
pthread_mutex_init(&mutex, NULL);
//创建十个线程,并开始计时
pthread_t mtids[100];
struct timeval start;
gettimeofday(&start, NULL);
for (int i = 0; i < 100; ++i)
{
pthread_create(&mtids[i], NULL, mFunc, NULL);
}
//在主线程中,调用join函数,回收线程,线程回收完成后,结束计时
for (int i = 0; i < 100; ++i)
{
pthread_join(mtids[i], NULL);
}
struct timeval end;
gettimeofday(&end, NULL);
long timediff = (end.tv_sec - start.tv_sec) * 1000000 + end.tv_usec - start.tv_usec;
printf("互斥锁 读全部线程执行完毕,总耗时: %ld us\n", timediff);
//回收子进程
wait(NULL);
}
else if (pid == 0)
{
//子进程
//初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
//创建
//创建十个线程,并开始计时
pthread_t rwtids[100];
struct timeval start;
gettimeofday(&start, NULL);
for (int i = 0; i < 100; ++i)
{
pthread_create(&rwtids[i], NULL, rwFunc, NULL);
}
//在主线程中,调用join函数,回收线程,线程回收完成后,结束计时
for (int i = 0; i < 100; ++i)
{
pthread_join(rwtids[i], NULL);
}
struct timeval end;
gettimeofday(&end, NULL);
long timediff = (end.tv_sec - start.tv_sec) * 1000000 + end.tv_usec - start.tv_usec;
printf("读写锁 读全部线程执行完毕,总耗时: %ld us\n", timediff);
//结束进程
exit(0);
}
return 0;
}
此外,还用信号量函数,写了一个生产者消费者小demo
/*
使用linux线程库的信号量函数,完成经典的生产者和消费者模型
*/
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <time.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <semaphore.h>
//缓冲区
int *buf;
int bufSize = 100;
//三个信号量
sem_t full, empty, mutex;
int bufPtr;
int count;
//生产者线程
void *producer(void *arg)
{
while (bufPtr < bufSize)
{
//信号量模型
sem_wait(&full);
sem_wait(&mutex);
buf[++bufPtr] = bufPtr;
sem_post(&mutex);
sem_post(&empty);
}
}
//消费者线程
void *consumer(void *arg)
{
while (1)
{
//信号量模型
sem_wait(&empty);
sem_wait(&mutex);
count = (count + 1) % __INT32_MAX__;
printf("pid[%ld], count[%d], data[%d]\n", pthread_self(), count, buf[bufPtr--]);
sem_post(&mutex);
sem_post(&full);
}
}
int main()
{
//初始化三个信号量
sem_init(&full, 0, bufSize);
sem_init(&empty, 0, 0);
sem_init(&mutex, 0, 1);
//初始化读写指针、缓冲区
bufPtr = -1;
count = 0;
buf = (int *)malloc(sizeof(int) * bufSize);
//创建6个线程,一个作生产者,5个消费者
pthread_t ppid, cpids[5];
pthread_create(&ppid, NULL, producer, NULL);
for (int i = 0; i < 5; ++i)
{
pthread_create(&cpids[i], NULL, consumer, NULL);
}
//detach分离,线程自动回收资源
pthread_detach(ppid);
for (int i = 0; i < 5; ++i)
{
pthread_detach(cpids[i]);
}
//主线程结束
pthread_exit(NULL);
return 0;
}