进程与线程
进程:拥有PCB,有独立地址空间。最小的分配资源单位,多个线程共享进程的资源。
线程:拥有PCB,没有独立地址空间。轻量级的进程,本质是进程。是CPU最小的执行和调度单位。
线程的优缺点
- 可以在一个进程内实现并发。
- 开销少,创建线程比创建进程要快。
- 数据通信、数据共享方便,同时也增加了开发的难度。
线程的创建和终止
线程创建、等待线程退出、查看线程
pthread_create
的 api 如下(第一个参数是线程id号,第二个参数用来设置线程属性,第三个是线程函数,第四个参数是运行函数的参数)
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
pthread_join
的 api 如下(第一个是线程id,第二个默认空)作用是等待线程结束。
int pthread_join(pthread_t thread, void **retval);
查看线程的命令如下:(本例中一个主线程,两个子线程)
[wsm@iZ2vchi15pdv9v0bwzok5kZ pthread]$ ps -ef | grep book1
wsm 22312 18702 0 14:06 pts/5 00:00:00 ./book1
wsm 22347 19007 0 14:07 pts/6 00:00:00 grep --color=auto book1
[wsm@iZ2vchi15pdv9v0bwzok5kZ pthread]$ ps -Lf 22312
UID PID PPID LWP C NLWP STIME TTY STAT TIME CMD
wsm 22312 18702 22312 0 3 14:06 pts/5 Sl+ 0:00 ./book1
wsm 22312 18702 22313 0 3 14:06 pts/5 Sl+ 0:00 ./book1
wsm 22312 18702 22314 0 3 14:06 pts/5 Sl+ 0:00 ./book1
线程非正常终止
- 如果主线程退出,全部线程将强行终止
- 在子线程调用exit()将终止整个进程(调用pthread_exit(0);或return 0;不会)
- 缺省行为是终止程序的信号将导致整个进程终止
终止线程的三种方法
- 线程可以简单地从线程函数中返回,返回值是线程的退出码
- 线程可以被同一进程中的其它线程调用pthread_cancel()取消
- 在线程函数中,调用pthread_exit()退出
return 0;
if (ii == 3) pthread_cancel(thid2);
if (ii == 3) pthread_exit(0);
pthread_exit 可以写到不在主线程函数里面的地方
#include <iostream>
#include <pthread.h>
void f()
{
pthread_exit(0);
}
void* hello(void* arg) {
std::cout << "Hello from thread!" << std::endl;
for(int i = 0; i < 5; i ++)
{
std::cout << i << std::endl;
if (i == 2) f();
}
return nullptr;
}
int main() {
pthread_t tid; // 线程标识符
// 创建一个新线程,将 hello 函数作为线程函数
if (pthread_create(&tid, nullptr, hello, nullptr) != 0) {
std::cerr << "Error creating thread" << std::endl;
return 1;
}
// 等待新线程完成
if (pthread_join(tid, nullptr) != 0) {
std::cerr << "Error joining thread" << std::endl;
return 2;
}
// 主线程继续执行
std::cout << "Hello from main!" << std::endl;
return 0;
}
线程参数的传递
- 创建的多个线程并不保证哪个线程先运行
- 不能用全局变量代替线程函数的参数
- 数据类型的强制转换
如何传递整形参数
int var = 1;
if (pthread_create(&thid1,NULL,thmain1, (void *)(long)var)!=0) { printf("pthread_create failed.\n"); exit(-1); }
int 转成 void*
,4字节转8字节,要先转成long。在线程里面使用的时候要先转成long再转成int。
如何传递地址参数
传的时候注意给每个线程传一个专属的地址。
把多个参数放到一个结构体中就可以通过传地址一次传多个参数。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thmain(void *arg); // 线程的主函数。
struct st_args
{
int no; // 线程编号。
char name[51]; // 线程名。
};
int main(int argc,char *argv[])
{
pthread_t thid=0;
// 创建线程。
struct st_args *stargs=new struct st_args;
stargs->no=15; strcpy(stargs->name,"测试线程");
if (pthread_create(&thid,NULL,thmain,stargs)!=0) { printf("pthread_create failed.\n"); exit(-1); }
// 等待子线程退出。
printf("join...\n");
pthread_join(thid,NULL);
printf("join ok.\n");
}
void *thmain(void *arg) // 线程主函数。
{
struct st_args *pst=(struct st_args *)arg;
printf("no=%d,name=%s\n",pst->no,pst->name);
delete pst;
printf("线程开始运行。\n");
}
线程退出状态
线程退出的状态
struct st_ret *pst=0;
pthread_join(thid,(void **)&pst);
printf("retcode=%d,message=%s\n",pst->retcode,pst->message);
线程资源的回收,让多线程程序有计划的退出
分离线程的apipthread_detach(pthread_self());
,线程结束后,由系统回收资源。
detach后不需要join
如何实现有计划的退出?先create主线程,主线程再将计划的清理函数入栈出栈
// 本程序演示线程资源的回收(线程清理函数)。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thmain(void *arg); // 线程主函数。
void thcleanup1(void *arg); // 线程清理函数1。
void thcleanup2(void *arg); // 线程清理函数2。
void thcleanup3(void *arg); // 线程清理函数3。
int main(int argc,char *argv[])
{
pthread_t thid;
// 创建线程。
if (pthread_create(&thid,NULL,thmain,NULL)!=0) { printf("pthread_create failed.\n"); exit(-1); }
int result=0;
void *ret;
printf("join...\n");
result=pthread_join(thid,&ret); printf("thid result=%d,ret=%ld\n",result,ret);
printf("join ok.\n");
}
void *thmain(void *arg) // 线程主函数。
{
pthread_cleanup_push(thcleanup1,NULL); // 把线程清理函数1入栈(关闭文件指针)。
pthread_cleanup_push(thcleanup2,NULL); // 把线程清理函数2入栈(关闭socket)。
pthread_cleanup_push(thcleanup3,NULL); // 把线程清理函数3入栈(回滚数据库事务)。
for (int ii=0;ii<3;ii++)
{
sleep(1); printf("pthmain sleep(%d) ok.\n",ii+1);
}
pthread_cleanup_pop(1); // 把线程清理函数3出栈。
pthread_cleanup_pop(1); // 把线程清理函数2出栈。
pthread_cleanup_pop(1); // 把线程清理函数1出栈。
}
void thcleanup1(void *arg)
{
// 在这里释放关闭文件、断开网络连接、回滚数据库事务、释放锁等等。
printf("cleanup1 ok.\n");
};
void thcleanup2(void *arg)
{
// 在这里释放关闭文件、断开网络连接、回滚数据库事务、释放锁等等。
printf("cleanup2 ok.\n");
};
void thcleanup3(void *arg)
{
// 在这里释放关闭文件、断开网络连接、回滚数据库事务、释放锁等等。
printf("cleanup3 ok.\n");
};
线程的取消
pthread_cancel
有两个模式,异步撤销(立即撤销)和延迟撤销。延迟撤销到达撤销点时才会撤销。
比如这个程序,不加撤销点的话将把循环跑满。
int main(int argc,char *argv[])
{
pthread_t thid;
// 创建线程。
if (pthread_create(&thid,NULL,thmain,NULL)!=0) { printf("pthread_create failed.\n"); exit(-1); }
usleep(100); pthread_cancel(thid);
int result=0;
void *ret;
printf("join...\n");
result=pthread_join(thid,&ret); printf("thid result=%d,ret=%ld\n",result,ret);
printf("join ok.\n");
printf("var=%d\n",var);
}
void *thmain(void *arg) // 线程主函数。
{
// pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL);
for (var=0;var<400000000;var++)
{
;
pthread_testcancel(); // 撤销点
}
return (void *) 1;
}
线程和信号
- 在多线程程序中,外部向进程发送信号不会中断系统调用
- 在多线程中,信号的处理是所有线程共享的(子线程会覆盖主线程的信号处理)
- 进程中的信号可以送达单个线程,会中断系统调用
线程安全
- 多个线程访问共享资源(全局和静态)的时候会冲突
- 三个概念:原子性、可见性、顺序性
如何解决线程安全?
- volatile关键字
- 原子操作(原子类型)
- 线程同步(锁)
原子操作本质是总线锁,CPU和内存通过总线进行数据交换,执行完汇编后再放开锁。硬件级别的锁,比线程库提供的锁快十倍左右。
线程同步
- 互斥锁
- 自旋锁
- 读写锁
- 条件变量
- 信号量
互斥锁
初始化互斥锁有两种方式
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; // 声明互斥锁。
pthread_mutex_init(&mutex,NULL); // 初始化互斥锁。
加锁解锁
pthread_mutex_lock(&mutex); // 加锁。
var++;
pthread_mutex_unlock(&mutex); // 解锁。
销毁互斥锁
pthread_mutex_destroy(&mutex); // 销毁锁。
自旋锁
初始化
pthread_spinlock_t spin; // 声明自旋锁。
pthread_spin_init(&spin,PTHREAD_PROCESS_PRIVATE); // 初始化自旋锁。
加锁解锁
pthread_spin_lock(&spin); // 加锁。
var++;
pthread_spin_unlock(&spin); // 解锁。
销毁
pthread_spin_destroy(&spin); // 销毁锁。
读写锁
- 只要没有线程持有写锁,任意线程都可以成功申请读锁。
- 只有再不加锁状态时,才能成功申请写锁。
注意事项
- 读写锁适合于对读的次数远大于写的情况
- Linux系统优先考虑读锁,这种实现方式有可能导致写入线程饿死的情况
// 本程序演示线程同步-读写锁。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
pthread_rwlock_t rwlock=PTHREAD_RWLOCK_INITIALIZER; // 声明读写锁并初始化。
void *thmain(void *arg); // 线程主函数。
void handle(int sig); // 信号15的处理函数。
int main(int argc,char *argv[])
{
signal(15,handle); // 设置信号15的处理函数。
pthread_t thid1,thid2,thid3;
// 创建线程。
if (pthread_create(&thid1,NULL,thmain,NULL)!=0) { printf("pthread_create failed.\n"); exit(-1); }
sleep(1);
if (pthread_create(&thid2,NULL,thmain,NULL)!=0) { printf("pthread_create failed.\n"); exit(-1); }
sleep(1);
if (pthread_create(&thid3,NULL,thmain,NULL)!=0) { printf("pthread_create failed.\n"); exit(-1); }
// 等待子线程退出。
pthread_join(thid1,NULL); pthread_join(thid2,NULL); pthread_join(thid3,NULL);
pthread_rwlock_destroy(&rwlock); // 销毁锁。
}
void *thmain(void *arg) // 线程主函数。
{
for (int ii=0;ii<100;ii++)
{
printf("线程%lu开始申请读锁...\n",pthread_self());
pthread_rwlock_rdlock(&rwlock); // 加锁。
printf("线程%lu开始申请读锁成功。\n\n",pthread_self());
sleep(5);
pthread_rwlock_unlock(&rwlock); // 解锁。
printf("线程%lu已释放读锁。\n\n",pthread_self());
if (ii==3) sleep(8);
}
}
void handle(int sig) // 信号15的处理函数。
{
printf("开始申请写锁...\n");
pthread_rwlock_wrlock(&rwlock); // 加锁。
printf("申请写锁成功。\n\n");
sleep(10);
pthread_rwlock_unlock(&rwlock); // 解锁。
printf("写锁已释放。\n\n");
}
读锁线程有sleep 8秒的操作,目的是测试,写锁可以被申请到 :joy:
条件变量
- 与互斥锁一起使用
- 实现生产消费者模型
- 实现通知功能
条件变量初使用
// 本程序演示线程同步-条件变量。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
pthread_cond_t cond=PTHREAD_COND_INITIALIZER; // 声明条件变量并初始化。
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; // 声明互斥锁并初始化。
void *thmain(void *arg); // 线程主函数。
void handle(int sig); // 信号15的处理函数。
int main(int argc,char *argv[])
{
signal(15,handle); // 设置信号15的处理函数。
pthread_t thid1,thid2,thid3;
// 创建线程。
if (pthread_create(&thid1,NULL,thmain,NULL)!=0) { printf("pthread_create failed.\n"); exit(-1); }
if (pthread_create(&thid2,NULL,thmain,NULL)!=0) { printf("pthread_create failed.\n"); exit(-1); }
if (pthread_create(&thid3,NULL,thmain,NULL)!=0) { printf("pthread_create failed.\n"); exit(-1); }
// 等待子线程退出。
pthread_join(thid1,NULL); pthread_join(thid2,NULL); pthread_join(thid3,NULL);
pthread_cond_destroy(&cond); // 销毁条件变量。
pthread_mutex_destroy(&mutex); // 销毁互斥锁。
}
void *thmain(void *arg) // 线程主函数。
{
while (true)
{
printf("线程%lu开始等待条件信号...\n",pthread_self());
pthread_cond_wait(&cond,&mutex); // 等待条件信号。
printf("线程%lu等待条件信号成功。\n\n",pthread_self());
}
}
void handle(int sig) // 信号15的处理函数。
{
printf("发送条件信号...\n");
pthread_cond_signal(&cond); // 唤醒等待条件变量的一个线程。
// pthread_cond_broadcast(&cond); // 唤醒等待条件变量的全部线程。
}
信号量
演示信号量的简单使用
// 本程序演示线程同步-信号量。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
volatile int var;
sem_t sem; // 声明信号量。
void *thmain(void *arg); // 线程主函数。
int main(int argc,char *argv[])
{
sem_init(&sem,0,1); // 初始化信号量。
pthread_t thid1,thid2;
// 创建线程。
if (pthread_create(&thid1,NULL,thmain,NULL)!=0) { printf("pthread_create failed.\n"); exit(-1); }
if (pthread_create(&thid2,NULL,thmain,NULL)!=0) { printf("pthread_create failed.\n"); exit(-1); }
// 等待子线程退出。
printf("join...\n");
pthread_join(thid1,NULL);
pthread_join(thid2,NULL);
printf("join ok.\n");
printf("var=%d\n",var);
sem_destroy(&sem); // 销毁信号量。
}
void *thmain(void *arg) // 线程主函数。
{
for (int ii=0;ii<1000000;ii++)
{
sem_wait(&sem); // 加锁。
var++;
sem_post(&sem); // 解锁。
}
}
生产消费者模型
条件变量的P操作解开互斥锁