目录
5.1、 数据混乱:当主线程与子线程同时对共享资源进行操作的时候会出现主线程与子线程抢占资源的情况出现,所以需要同步机制来对着各种现象进行干预,确保每个线程能够对共享资源进行操作,不会抢占资源;
总结:通过对上面两种操作的时间对比明显原子操作的性能要优于互斥锁;
1、守护线程
1.1、特点:
后台服务进程
独立于控制终端
周期性执行某任务
不受用户登录注销影响
一般采用以d结尾名字(服务)
1.2.、进程组:
属性:
进程的组长:
组里边的第一进程
进程组的ID==进程中的组长的ID
进程中组长的选择:
进程中的第一个进程
进程组ID的设定:
进程组的ID就是组长的进程ID
1.3、会话:
属性:
创建一个会话注意事项:
不能是进程组长
创建会话的进程成为新进程组的组长:
有些lInux版本需要root权限执行此操作
创建出的新会话会丢弃原有的控制终端:
一般步骤;fork ,父亲死,儿子执行创建会话操作(setid)
获取进程所属的会话ID:
pid_t getsid(pid_t pid);
创建一个会话:
pid_t setid(void);
2、线程的概念:
创建线程后,地址空间没有变化,进程退出变成了,子线程—主线程,创建的子线程和主线程共用地址空间,子线程的PCB是从主线程拷贝来的,主线程-子线程除了栈不共享之外其余全部共享,如果创建5个子线程那么主线程的栈会被分成5份,,总之创建了几个子线程,主线程的栈就会被分成几份;
在Linux下线程是轻量级进程,对内核来说线程就是进程;
2.1、多线程和多进程的区别:
名称 | 始终共享的资源 |
多进程 | 代码、文件描述符、内存映射区--mmap |
多线程 | 堆、全局变量、节省资源 |
3、线程的创建
3.1、相关函数:
1.创建线程‐‐pthread_create
int pthread_create( pthread_t *thread), //线程ID = 无符号长整型
const pthread_attr_t *attr, //线程属性,NULL
void *(*start_routine)(void *), //线程处理函数
void *arg); //线程处理函数
参数:
pthread:传出参数,线程创建成功之后,会被设置一个合适的值
attr:默认传NULL
start_routine:子线程的处理函数
arg: 回调函数的参数
返回值:
成功:0
错误:错误号 //perror不能使用该函数打印错误信息
主线程先退出,子线程会被强制结束
验证线程直接共享全局变量
实验:子线程的创建:
#include<stdio.h>
#include<pthread.h>
void* myfunc(void *arg)
{
printf("child pthread id:%ld\n",pthread_self());
return 0;
}
int main()
{
pthread_t pthid;
pthread_create(&pthid,NULL,myfunc,NULL);
printf("parent pthread id:%ld\n",pthread_self());
for(int i=0; i<5; i++)
{
printf("i = %d\n",i);
}
sleep(3);
return 0;
}
~
结果:只执行了主函数中的子进程创建的并没有执行,是因为主线程与子线程在相互抢占资源
将主函数中加入休眠的延时动作,再次对文件进行编辑生程运行文件
结果:
2、单个线程退出--pthread_exit
函数原型: void pthread‐exit(void *retval);
retval指针:必须指向全局,堆
实验:
#include<stdio.h>
#include<pthread.h>
#include<string.h>
void* myfunc(void *arg)
{
printf("child pthread id:%ld\n",pthread_self());
for(int i=0; i<5 ; i++)
{
printf("child pthread %d\n",i);
}
return 0;
}
int main()
{
pthread_t pthid;
int ret;
ret = pthread_create(&pthid,NULL,myfunc,NULL);
if(ret != 0)
{
printf("error number is %d\n",ret);
printf("%s\n",strerror(ret));
}
printf("parent pthread id:%ld\n",pthread_self());
pthread_exit(NULL);
for(int i=0; i<5; i++)
{
printf("i = %d\n",i);
}
sleep(3);
return 0;
}
结果:在使用pthread_exit()函数以后后续的代码并没有用执行;
3、阻塞等待线程退出,获取线程退出状态--pthread_join
函数原型:
int pthread_join(pthread_t pthread, void **retval)
参数:
pthread:要回收的子线程的ID
retval:读取线程退出的携带信息
传出参数
void* ptr;
pthread_join(pthid,&ptr);
指向的内存和pthread_exit参数指向地址一致
注意: pthread_join()函数如果需要携带信息量时需要和pthread_exit()同时使用或return(返回量);
实验:等待子线程退出,阻塞主线程,并将子线程的返回量:“hello”输出;
#include<stdio.h>
#include<pthread.h>
#include<string.h>
#include<unistd.h>
#include <stdlib.h>
void* myfunc(void *arg)
{
char* msg = malloc(64);
printf("child pthread id:%ld\n",pthread_self());
for(int i=0; i<5 ; i++)
{
printf("child pthread %d\n",i);
}
strcpy(msg,"hello\n");
return (void*)msg;
}
int main()
{
pthread_t pthid;
void* msg = NULL;
int ret;
ret = pthread_create(&pthid,NULL,myfunc,NULL);
if(ret != 0)
{
printf("error number is %d\n",ret);
printf("%s\n",strerror(ret));
}
printf("parent pthread id:%ld\n",pthread_self());
pthread_join(pthid,&msg);
printf("return is %s",(char*)msg);
pthread_exit(NULL);
for(int i=0; i<5; i++)
{
printf("i = %d\n",i);
}
sleep(3);
return 0;
}
结果:
4、线程分离--pthread_detach
函数原型:int pthread_datach(pthread_t thread);
调用该函数之后不需要 pthread_join
子线程会自动回收自己的PCB
5、杀死线程:
函数原型:int pthread_cancel(pthread_t pthread);
注意: 在使用该函数时,线程里面要有一次系统函数的调用:write、read、printf,等;
6、比较两个线程ID是否相等(预留函数)--pthread_equal
函数原型:
int pthread_equal(pthread_t t1,pthread_t t2);
4、线程的分离属性
4.1、属性:
其决定了线程在终止之后,是否需要被其他线程(比如主线程)回收资源(通过pthread_join());一旦线程在创建时被设置为分离属性,就无法在join;
设置线程分离:
pthread_detach(pthread_t thread);
在创建时设置为分离属性:
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, thread_func, NULL);
pthread_attr_destroy(&attr);
5、线程同步:
5.1、 数据混乱:当主线程与子线程同时对共享资源进行操作的时候会出现主线程与子线程抢占资源的情况出现,所以需要同步机制来对着各种现象进行干预,确保每个线程能够对共享资源进行操作,不会抢占资源;
实验:主线程,子线程对一个全局变量同时进行相加:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
int counter = 0;
void* child_thread(void* arg) {
for (int i = 0; i < 10; ++i) {
printf("Child Thread: counter = %d\n", counter++);
usleep(100000);
}
return NULL;
}
int main() {
pthread_t tid;
if (pthread_create(&tid, NULL, child_thread, NULL) != 0) {
perror("Failed to create thread");
return 1;
}
for (int i = 0; i < 10; ++i) {
printf("Main Thread: counter = %d\n", counter++);
usleep(100000);
}
pthread_join(tid, NULL);
return 0;
}
结果:并没有代码想要实现的功能,而是分别交替相加到20;
5.2、实现线程同步机制的方法:
6、互斥量(互斥锁)
6.1、属性:
两个线程分别通过对共享资源上锁,阻塞其他线程,从而保证操作的独立性;操作过程结束后对该资源解锁后,鳍鱼津城才可再次对其进行操作;
6.2、 特点:
多个线程访问共享数据是串行的;
6.3、使用步骤:
创建互斥锁: pthread_mutex_t mutex;
初始化:pthread_mutex_init(&mutex,NULL); -- mutex = 1
找到线程共同操作的共享数据
加锁:操作共享资源之前加锁,pthread_mutex_lock(&mutex); //阻塞 --mutex = 0
pthread_mutex_trylock(&mutex); // 如果锁上锁直接返回,不阻塞
XXXXX共享数据操作 //临界区 ,越小越好
解锁:pthread_mutex_unlock(&mutex); // -- mutex = 1
阻塞在锁上的线程会被唤醒
销毁:pthread_mutex_destory(&mutex);
6.4、相关函数:
初始化互斥锁
pthread_mutex_init(
pthread_mutex_t* restrict mutex,
const pthread_mutexattr_t* restrict attr,
);
销毁互斥锁:
pthread_mutex_destory(pthread_mutex_t* mutex );
加锁
pthread_mutex_lock(pthread_mutex* mutex);
mutex:
没有被锁上,当前线程会将这把锁锁上
被锁上了:当前线程阻塞,锁被打开之后,线程解除阻塞
尝试加锁,失败返回,不阻塞
pthread_mutex_trylock(pthread_mutex_t* mutex);
没有锁上:当前线程会被这把锁加锁
如果锁上了:不会阻塞,返回
返回0:加锁 成功。没锁上:返回错误号
if( pthread_mutex_trylock(& mutex)==0)
{
//尝试加锁,并且成功了
//访问共享资源
XXXXXXXX
}
else
{
//错误处理
//或者等待,再次尝试加锁
}
解锁
pthread_mutex_unlock(pthread_mutex_t* mutex);
实验:对线程同步的实验添加互斥锁:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
int counter = 0;
//create rwlock
pthread_mutex_t mutex;
void* child_thread(void* arg) {
//子线程加锁
pthread_mutex_lock(&mutex);
for (int i = 0; i < 10; ++i) {
printf("Child Thread: counter = %d\n", counter++);
usleep(100000); // 模拟工作,100ms
}
//子线程解锁
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t tid;
//初始化互斥锁
pthread_mutex_init(&mutex,NULL);
// 创建子线程
if (pthread_create(&tid, NULL, child_thread, NULL) != 0) {
perror("Failed to create thread");
return 1;
}
//主线程加锁
pthread_mutex_lock(&mutex);
for (int i = 0; i < 10; ++i) {
printf("Main Thread: counter = %d\n", counter++);
usleep(100000); // 模拟工作,100ms
}
//主线程解锁
pthread_mutex_unlock(&mutex);
// 等待子线程结束
pthread_join(tid, NULL);
return 0;
}
结果:主线程,子线程执行各自操作后再执行另外操作,没有发生资源抢占;
7、原子操作:
7.1、属性:
一个操作在cpu层面是不可中断的,要么全部执行完,要么完全不执行;即使有多个线程来的访问这个变量,也不会出现数据冲突;
注意:没有一个普通函数能够单独实现原子操作;其实现需要以来硬件和特殊指令;
7.2、实现方法:
使用GCC内建函数:
__sync_fetch_and_add(&counter, 1); // 原子加1,返回旧值
__sync_add_and_fetch(&counter, 1); // 原子加1,返回新值
为什么它可以实现原子操作:
7.2、原子操作与互斥锁进行比较:
下面通过计时函数gettimeofday()来对原子操作和互斥函数之间的性能进行一个比较:
原子操作:通过创建两个线程来对counter进行++操作;
#include <stdio.h>
#include <pthread.h>
#include <sys/time.h>
int counter = 0;
void* thread_func(void* arg) {
for (int i = 0; i < 100000; ++i) {
__sync_fetch_and_add(&counter, 1); // 原子操作
}
return NULL;
}
int main() {
pthread_t t1, t2;
struct timeval start, end;
gettimeofday(&start, NULL); // 开始计时
pthread_create(&t1, NULL, thread_func, NULL);
pthread_create(&t2, NULL, thread_func, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
gettimeofday(&end, NULL); // 结束计时
long time_us = (end.tv_sec - start.tv_sec) * 1000000L + (end.tv_usec - start.tv_usec);
printf("With atomic: counter = %d\n", counter);
printf("Elapsed time: %ld microseconds\n", time_us);
return 0;
}
~
~
~
~
~
~
原子操作结果:
互斥锁:
#include <stdio.h>
#include <pthread.h>
#include <sys/time.h>
int counter = 0;
pthread_mutex_t lock;
void* thread_func(void* arg) {
for (int i = 0; i < 100000; ++i) {
pthread_mutex_lock(&lock);
counter++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main() {
pthread_t t1, t2;
struct timeval start, end;
pthread_mutex_init(&lock, NULL);
gettimeofday(&start, NULL); // 开始计时
pthread_create(&t1, NULL, thread_func, NULL);
pthread_create(&t2, NULL, thread_func, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
gettimeofday(&end, NULL); // 结束计时
long time_us = (end.tv_sec - start.tv_sec) * 1000000L + (end.tv_usec - start.tv_usec);
printf("With mutex: counter = %d\n", counter);
printf("Elapsed time: %ld microseconds\n", time_us);
pthread_mutex_destroy(&lock);
return 0;
}
互斥锁结果:
总结:通过对上面两种操作的时间对比明显原子操作的性能要优于互斥锁;
8、死锁:
8.1、造成原因:自己锁自己
示例:
for(int i = 0;i<MAX;i++)
{
pthread_mutex_lock(&mutex);
pthread_mutex_lock(&mutex);
int crt = number;
crt++;
number = crt;
printf("thread A id = %ld,number = %d\n",pthread_self(),number);
pthread_mutex_unlock(&mutex);
usleep(10);
}
上面的线程使用了两把锁,但是最后只解开了一把,另一把还在锁着;
8.2、两个线程互相阻塞:
让线程按照一定顺序去访问共享资源;在访问其他锁得时候需要先把自己的锁解开;
9、读写锁
9.1、属性:
1.读写锁是几把锁?
一把锁
pthread_rwlock_t lock;
2.读写锁的类型;
读锁-对内存做读操作
写锁-对内存做写操作
注意:写锁的优先级要高于读锁,多个进程可以同时拥有读锁,但是写锁不行,写锁只能独占;互斥锁是一个线程对一个资源操作,是串行的,读写锁是并行的可以共享;
9.2、读写锁特性:
1、线程A加读锁成功,又来了三个线程,做读操作,可以加锁成功
读共享-并行处理
2、线程A加写锁成功,又来了三个线程,做读操作,三个线程阻塞
写独占
3、线程A加读锁成功,又来了B线程加写锁阻塞,又来了C线程加读锁阻塞
读写不可以同时进行
写的优先级高
9.3、场景练习:
1、线程A加写锁成功,线程B请求读锁
线程B阻塞 (写锁优先级高于读锁)
2、线程A持有读锁,线程B请求写锁
线程B阻塞 (写锁与读锁不能同时存在,需要A进程释放读锁)
3、线程A拥有读写,线程B请求读锁
线程B阻塞 (写锁优先级高于读锁,需要A进程释放写锁)
4、线程A持有读锁,然后线程B请求写锁,然后线程C请求读锁
线程B阻塞,线程C阻塞 (需要A释放读锁)
线程B加锁,线程C阻塞 (B写锁的优先级高于读锁)
线程C加锁
5、线程A持有写锁,然后线程B请求读锁,然后线程C请求写
线程B阻塞,线程C阻塞 (需要A释放写锁)
线程C加锁,线程B阻塞 (写锁优先级高于读锁)
线程B加锁
9.4、主要操作函数:
初始化读写锁
pthread_rwlock_init(pthread_rwlock_t* restrict rwlock,
const pthread_rwlockattr_t* restrict attr );
销毁读写锁
pthread_rwlock_destroy(pthread_rwlock_t* rwlock):
加读锁
pthread_rwlock_rdlock(pthread_rwlock_t* rdlock);
阻塞:之前对这把锁加的是写锁的操作
尝试加读锁
pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
加锁成功:返回0
失败:返回错误号
加写锁
pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
阻塞:上一次加写锁还没解锁
阻塞:上一次加读锁还没解锁
尝试加写锁
pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
解锁
pthread_rwlock_unlock(pthread_rwlock_t* rwlock)
实验:三个线程不定时同时写一个全局变量,五个线程不定时读同一全局资源
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int number = 1000;
// 创建读写锁
pthread_rwlock_t lock;
void* write_func(void* arg)
{
while (1)
{
pthread_rwlock_wrlock(&lock);
number++;
printf("write: %ld, %d\n", pthread_self(), number);
pthread_rwlock_unlock(&lock);
usleep(500000);
}
return NULL;
}
void* read_func(void* arg)
{
while (1)
{
pthread_rwlock_rdlock(&lock);
printf("read : %ld, %d\n", pthread_self(), number);
pthread_rwlock_unlock(&lock);
usleep(500000);
}
return NULL;
}
int main()
{
pthread_rwlock_init(&lock, NULL);
pthread_t p[8];
for (int i = 0; i < 3; i++)
{
pthread_create(&p[i], NULL, write_func, NULL);
}
for (int i = 0; i < 5; i++)
{
pthread_create(&p[i + 3], NULL, read_func, NULL);
}
for (int i = 0; i < 8; i++)
{
pthread_join(p[i], NULL);
}
pthread_rwlock_destroy(&lock);
return 0;
}
结果:5个进程读。3个进程写;
10、条件变量
10.1、属性:
条件变量是为了阻塞进程,但它不是锁,需要使用条件变量与互斥量一起使用:
使用时,互斥量:保护一块共享数据;条件变量:引起阻塞;
10.2、两种状态:
1、条件不满足:阻塞线程;
2、条件满足:通知阻塞的线程开始工作;
10.3、主要函数:
初始化一个条件变量
pthread_cond_init(pthread_cond_t * restrict cond,
const pthread_condattr_t * restrict attr
);
销毁一个条件变量
pthread_cond_destroy(pthread_cond_t * cond);
阻塞等待一个条件变量
pthread_cond_wait(
pthread_cond_t *restrict cond,
pthread_mutex_t * restrict mutex
);
阻塞线程
将已经上锁的mutex解锁
该函数解除阻塞,对互斥锁加锁
限时等待一个条件变量
pthread_cond_timedwait(
pthread_cond_t * restrict cond,
pthread_mutex_t * restrict mutex,
const struct timespec * restrict abstime
);
唤醒至少一个阻塞在条件变量上的线程
pthread_cond_signal(pthread_cond_t* cond);
唤醒全部阻塞在条件变量上的线程
pthread_cond_broadcast(pthread_cond_t * cond);
实验:使用条件变量实现生产者,消费者模型:
实现思路:
工作流程:
烧饼----节点,
生产者-----生产节点;不断产生节点并使用头插法插入到头节点,流程如下:
创建一个新节点;将节点的next只想原来的头节点,更新头指针,使其指向新节点;
实现代码:
Node* head = NULL;
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = value;
// 新节点的 next 指向当前头节点
new_node->next = head;
// 更新头指针,指向新节点
head = new_node;
消费者----删除节点
先判断列表是否存在,一旦被唤醒就删除头节点,使用头删法;
实现代码:
struct Node* temp = head; // 保存当前头节点
head = head->next; // 把头指针移动到下一个节点
free(temp); // 释放原来的头节点
总体实现代码:
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
typedef struct node
{
int data;
struct node* next;
}Node;
//create head node
Node* head = NULL;
//create mutex
pthread_mutex_t mutex;
//create cond
pthread_cond_t cond;
void* produce()
{
while(1)
{
//create node
Node* pnew = (Node*)malloc(sizeof(Node));
//init node
pnew->data = rand()%1000;
//lock
pthread_mutex_lock(&mutex);
pnew->next = head;
head = pnew;
printf("produce: %ld,%d\n",pthread_self(),pnew->data);
//unlock
pthread_mutex_unlock(&mutex);
}
sleep(500);
return NULL;
}
void* customer()
{
while(1)
{
if(head==NULL)
{
continue;
}
//delete head node
Node* pdel = head;
head = head->next;
printf("customer: %ld,%d\n",pthread_self(),pdel->data);
free(pdel);
}
sleep(500);
return NULL;
}
int main()
{
pthread_t p1,p2;
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
pthread_create(&p1,NULL,produce,NULL);
pthread_create(&p2,NULL,customer,NULL);
pthread_join(p1,NULL);
pthread_join(p2,NULL);
return 0;
}
结果:
11、信号量:
11.1、类型
sem_t sem;
加强版的互斥锁
11.2、主要函数:
初始化信号量
sem_init(sem_t *sem,int pshared,unsigned int value);
0-线程同步
1-进程同步
value-最多有几个线程操作共享数据
销毁信号量
sem_destroy(sem_t *sem);
加锁
sem_wait(sem_t *sem);
调用一次相当于对sem做了一次 -- 操作
如果sem值为0,线程会阻塞
尝试加锁
sem_trywait(sem_t *sem);
sem == 0;加锁失败,不阻塞,直接发牛
限时尝试加锁
sem_timewait(sem_t *sem,xxxx);
解锁++
sem_post(sem_t *sem);
对sem做了++ 操作
11.3、信号量与互斥锁的区别:
互斥锁:串行操作;
信号量:并行操做;
11.3、实现生产者,消费者模型:
实现思路:
实现代码:
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<semaphore.h>
#include<unistd.h>
typedef struct node
{
int data;
struct node* next;
}Node;
//create head node
Node* head = NULL;
//create mutex
pthread_mutex_t mutex;
//create cond
pthread_cond_t cond;
sem_t producer_sem;
sem_t customer_sem;
void* produce()
{
while(1)
{
sem_wait(&producer_sem);
//create node
Node* pnew = (Node*)malloc(sizeof(Node));
//init node
pnew->data = rand()%1000;
pnew->next = head;
head = pnew;
printf("produce: %ld,%d\n",pthread_self(),pnew->data);
sem_post(&customer_sem);
}
sleep(500);
return NULL;
}
void* customer()
{
while(1)
{
sem_wait(&customer_sem);
//delete head node
Node* pdel = head;
head = head->next;
printf("customer: %ld,%d\n",pthread_self(),pdel->data);
free(pdel);
sem_post(&producer_sem);
}
sleep(500);
return NULL;
}
int main()
{
pthread_t thid[2];
sem_init(&producer_sem,0,4);
sem_init(&customer_sem,0,0);
pthread_create(&thid[0],NULL,produce,NULL);
pthread_create(&thid[1],NULL,customer,NULL);
for(int i=0;i<2;i++)
{
pthread_join(thid[i],NULL);
}
sem_destroy(&producer_sem);
sem_destroy(&customer_sem);
return 0;
}
结果: