多线程的基本代码编写步骤
1.创建线程pthread_create()
2.终止线程的三种方法。线程取消pthread_cancel(一般在主线程取消), 线程终止pthread_exit(在其他线程执行), 或者使用线程返回return
3.线程等待pthread_join
需要等待的原因是
1.已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
2.创建新的线程不会复用刚才退出线程的地址空间。
创建线程
功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
线程终止
phread_exit函数
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
线程取消
pthread_cancel函数
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码
线程等待
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的
终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数
PTHREAD_ CANCELED。 - 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参
数。 - 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
三种情况代码示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread1(void *arg)
{
printf("thread 1 returning ... \n");
int *p = (int *)malloc(sizeof(int));
*p = 1;
return (void *)p;
}
void *thread2(void *arg)
{
printf("thread 2 exiting ...\n");
int *p = (int *)malloc(sizeof(int));
*p = 2;
pthread_exit((void *)p);
}
void *thread3(void *arg)
{
while (1)
{ //
printf("thread 3 is running ...\n");
sleep(1);
}
return NULL;
}
int main(void)
{
pthread_t tid;
void *ret;
// thread 1 return
pthread_create(&tid, NULL, thread1, NULL);
pthread_join(tid, &ret);
printf("thread return, thread id %X, return code:%d\n", tid, *(int *)ret);
free(ret);
// thread 2 exit
pthread_create(&tid, NULL, thread2, NULL);
pthread_join(tid, &ret);
printf("thread return, thread id %X, return code:%d\n", tid, *(int *)ret);
free(ret);
// thread 3 cancel by other
pthread_create(&tid, NULL, thread3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &ret);
if (ret == PTHREAD_CANCELED)
printf("thread return, thread id %X, return code:PTHREAD_CANCELED\n", tid);
else
printf("thread return, thread id %X, return code:NULL\n", tid);
}
互斥概念
互斥:多个执行流在同一时刻,只有一个执行流进入临界区。
对于Linux的互斥锁,加锁和解锁 的操作都是原子性的。
当线程A拿到锁后,其他线程虽然也可以切换到改代码块,但是锁是被 A拿走的,其他线程无法申请到锁,所以就无法进入临界区。这就是互斥
互斥锁的加锁和解锁原子性是如何实现的
以下是锁的其中一个实现方式。
交换的汇编xchgb是原子性的。
本质:将cpu寄存器中的值交换到线程的上下文中,变成了线程私有的。从而实现加锁解锁为原子性。
线程安全
重入:同一个函数被不同执行流执行调用,当前执行流还没执行完,其他执行流就进入了。这叫做重入。
可重入函数:在重入的基础上,运行结果不会出现任何问题,则该函数就是可重入函数。
函数是可重入的,那么他就一定是线程安全的。
线程不安全就是并发的时候出现问题,不管他是否是可重入或不可重入。
死锁
自己和对方互相申请对方的握着不放的资源,这就叫做死锁。
特殊情况:一把锁也能产生死锁。比如:一把锁连续申请两次加锁,这就会产生死锁。
产生死锁的四个必要条件:
互斥条件:一个资源每次只能被一个执行流使用
请求和保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:不能强行剥夺资源,比如申请了锁,还没使用完,不能强行剥夺锁。
循环等待条件:若干执行流形成头尾相连循环等待资源的关系
线程同步
线程互斥合理吗?
互斥可能导致饥饿问题:一个执行流,长时间得不到某种资源。
同步:在保证数据安全的前提下,让线程按照特定的顺序访问临界资源,从而有效避免饥饿问题。
静态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。
volatile
使用 volatile 关键字并不意味着就不需要加锁了。volatile 和锁机制解决的是不同层面的并发问题。
volatile 主要解决的是:
变量的可见性问题
确保一个线程对变量的修改对其他线程可见。
禁止指令重排序
防止编译器和 CPU 对指令进行重排优化,保证代码执行的顺序性。
但 volatile 并不能解决:
原子性问题
对共享变量的复合操作(如 i++)仍然可能出现数据竞争。
线程安全问题
复杂的并发操作,如链表的插入和删除,仍然需要加锁来保证线程安全。
因此,在多线程编程中,volatile 和锁机制是相辅相成的:
volatile 用于解决可见性和有序性问题。
锁机制用于解决原子性和线程安全问题。
简单的共享变量可以使用 volatile 关键字来保证可见性和有序性,但对于复杂的并发操作,仍然需要使用锁来确保线程安全。
条件变量
同步是使用条件变量来实现的。
互斥是使用互斥锁来实现的。
条件变量初始化
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)
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒全部
int pthread_cond_signal(pthread_cond_t *cond);
生产者消费者模型(同步和互斥的最经典场景 ) 重新认识条件变量
生产者消费者模式就是通过一个缓冲区来解决生产者和消费者的强耦合问题,提高了效率。生产者和消费者不直接通讯,而是通过缓冲区通信。
利用一个缓冲区(内存中特定的一种数据结构), 生产者(线程),消费者(线程)。
生产者和消费者线程之间的关系:互斥, 同步
生产者线程之间的关系:互斥
消费者线程之间的关系:互斥
解决的问题
利用一个缓冲区(内存中特定的一种数据结构)
1.解耦合
2.提高效率