1--多线程的优点
多进程服务器的缺点:
① 创建进程的过程会带来一定的开销;
② 为了完成进程间的数据交换,需要特殊的 IPC 技术;
③ 进程间的上下文切换是创建进程时的最大开销;
多线程的优点:
① 线程的创建和上下文切换比进程的创建和上下文切换更快;
② 线程间交换数据时无需特殊技术;
2--进程和线程的差异
每个进程拥有独立的内存空间,拥有自己的数据区、堆区域和栈区域;
每个线程只拥有自己的栈区域,线程间共享数据区和堆区域;因此线程间上下文切换时不需要切换数据区和堆,可以利用数据区和堆区域交换数据;
进程:在操作系统构成单独执行流的单位;
线程:在进程构成单独执行流的单位;
进程是在操作系统内部生成多个执行流,线程就是在同一进程内部创建多条执行流;
3--线程创建
cpp
#include <pthread.h>
int pthread_create(pthread_t* restrict thread, const pthread_attr_t* restrict attr, void* (* start_routine)(void*), void* restrict arg);
// 成功时返回 0,失败时返回其他值
// thread 表示保存新创建线程 ID 的变量地址值
// attr 表示用于传递线程属性的参数,传递 NULL 时表示创建默认属性的线程
// start_routine 表示线程的入口函数
// arg 表示传递给线程入口函数的参数
cpp
// gcc thread1.c -o thread1 -lpthread
// ./thread1
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* thread_main(void* arg){ // 线程入口函数
int i;
int cnt = *((int*)arg);
for(i = 0; i < cnt; i++){
sleep(1);
puts("running thread");
}
return NULL;
}
int main(int argc, char* argv[]){
pthread_t t_id;
int thread_param = 5;
if(pthread_create(&t_id, NULL, thread_main, (void*)&thread_param) != 0){ // 创建线程
puts("pthread_create() error");
return -1;
}
sleep(10);
puts("end of main");
return 0;
}
4--线程使用
调用 pthread_join(ID) 可以使进程或线程进入等待状态,直到 ID 对应的线程终止为止;
cpp
#include <pthread.h>
int pthread_join(pthread_t thread, void** status);
// 成功时返回0,失败时返回其他值
// thread 表示线程ID,只有该线程终止后才会从函数返回
// status 表示保存线程返回值的指针变量地址值
cpp
// gcc thread2.c -o thread2 -lpthread
// ./thread2
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
void* thread_main(void *arg){
int i;
int cnt = *((int*)arg);
char* msg = (char*)malloc(sizeof(char)*50);
strcpy(msg, "Hello, I'm thread~ \n");
for(i = 0; i < cnt; i++){
sleep(1);
puts("running thread");
}
return (void*)msg;
}
int main(int argc, char* argv[]){
pthread_t t_id;
int thread_param = 5;
void* thr_ret;
// 创建线程
if(pthread_create(&t_id, NULL, thread_main, (void*)&thread_param) != 0){
puts("pthread_create() error");
return -1;
}
// 阻塞,等待线程返回
if(pthread_join(t_id, &thr_ret) != 0){
puts("pthread_join() error");
return -1;
}
// 打印线程返回值
printf("Thread return message: %s \n", (char*)thr_ret);
free(thr_ret);
return 0;
}
5--线程安全问题
多个线程同时调用函数执行临界区代码时,会出现问题;
根据临界区是否引起问题,函数可分为:线程安全函数和非线程安全函数;
线程安全函数被多个线程同时调用时不会引发问题,非线程安全函数被同时调用时会引发问题;
以下代码展示了多个线程同时访问临界区代码操作全局变量时会出现意向不到的问题;
cpp
// gcc thread4.c -D_REENTRANT -o thread4 -lpthread
// ./thread4
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREAD 100
long long num = 0;
void* thread_inc(void* arg){
int i;
for(i = 0; i < 50000000; i++){
num += 1;
}
return 0;
}
void* thread_des(void* arg){
int i;
for(i = 0; i < 50000000; i++){
num -= 1;
}
return 0;
}
int main(int argc, char* argv[]){
pthread_t thread_id[NUM_THREAD];
int i;
printf("sizeof long long: %ld \n", sizeof(long long));
for(i = 0; i < NUM_THREAD; i++){
// 各创建50个线程,分别执行对全局变量 num 的加减操作
if(i%2){
pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
}
else{
pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
}
}
for(i = 0; i < NUM_THREAD; i++){
pthread_join(thread_id[i], NULL);
}
printf("result: %lld \n", num);
return 0;
}
正常结果应为0,但实际结果并不是;这就是多线程同时访问临界区,会出现数据竞争等的问题;
线程访问变量 num 时应阻止其他线程访问,直到一个线程完成运算,这就是同步(Synchronization);线程同步用于解决线程访问顺序引发的问题;
6--互斥量
互斥量表示不允许多个线程同时访问,主要用于解决线程同步访问的问题;
cpp
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* attr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
// 成功时返回 0,失败时返回其他值
// mutex 表示创建和销毁互斥量时传递保存和销毁互斥量的变量地址值
// attr 传递即将创建的互斥量属性,没有特别需要指定的属性时传递 NULL
int pthread_mutex_lock(pthread_mutex_t* mutex); // 上锁
int pthread_mutex_unlock(pthread_mutex_t* mutex); // 解锁
// 成功时返回 0,失败时返回其他值
pthread_mutex_t mutex;
pthread_mutex_lock(&mutex);
// 临界区开始
// ...
// 临界区结束
pthread_mutex_unlock(&mutex);
7--信号量
利用二进制信号量完成控制线程程序为中心的同步方法;
cpp
#include <semaphore.h>
int sem_init(sem_t* sem, int pshared, unsigned int value);
int sem_destroy(sem_t* sem);
// 成功时返回 0,失败时返回其他值
// sem 表示信号量的变量地址值
// pshared 传递其他值时,创建可由多个进程共享的信号量;传递 0 时,创建只允许一个进程内部使用的信号量
// value 表示指定新创建的信号量的初始值
int sem_post(sem_t* sem); // 信号量增加1
int sem_wait(sem_t* sem); // 信号量减少1
// 成功时返回 0,失败时返回其他值
sem_wait(&sem); // 信号量变为0
// 临界区的开始
// ...
// 临界区的结束
sem_post(&sem); // 信号量变为1
8--线程销毁
线程销毁的 3 种方法:
① 调用 main 函数的返回语句;
② 调用 pthread_join() 函数;
③ 调用 pthread_detach() 函数;