8.1 线程概念
一个程序如何同时(宏观)完成多个任务?
- 使用fork和exec同时运行多个程序
- 使用线程在同一个程序中同时运行多个程序
一个进程至少包含一个线程,此外可以创建多个线程,线程之间彼此平等,称原来的线程为主线程。
进程是资源分配的最小单位,而线程是计算机中独立运行、CPU调度的最小单元。(只携带一个栈)
同一进程中的线程共享整个进程空间。
线程有时也被称为轻量级进程。
多进程与多线程的选择:
- 需要频繁创建和销毁,选择多线程
- 需要大量计算,选择多线程
- 在通信方面,线程间通信更加方便高效,并发处理间的相关性比较强的,选择多线程,可提高应用程序响应速度
- 对可靠性有一定要求,多进程更加安全可靠,因为多线程共享进程的地址空间,对资源进行同步互斥访问容易出现错误
- 若编程与调试都相对较简单的程序,选择多进程。
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1054933/1636462357078-bea18d0c-f501-4ecb-9891-26ee4578e78f.png#clientId=ub89e08c0-1aff-4&from=paste&height=452&id=u4ec32156&margin=%5Bobject%20Object%5D&name=image.png&originHeight=602&originWidth=1120&originalType=binary&ratio=1&size=73993&status=done&style=none&taskId=ud06fae8e-3926-43ba-89df-75192ca6c32&width=840)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1054933/1636463548773-7360ef3c-d995-4b74-9ef8-8ecf39377282.png#clientId=ub89e08c0-1aff-4&from=paste&height=307&id=uf2199895&margin=%5Bobject%20Object%5D&name=image.png&originHeight=614&originWidth=1076&originalType=binary&ratio=1&size=62655&status=done&style=none&taskId=u244dc93f-e4e5-4bd8-b561-af1713bed82&width=538)<br />linux系统内核级的线程实现机制符合POSIX(Portable Operating System Interface of UNIX/LINUX,可移植的操作系统接口)规范,对于用户级的多线程编程接口也遵循POSIX标准,称为Pthread(POSIX Thread)。<br />linux采用Pthread线程库实现对线程的访问与控制。linux下编写多线程应用程序需要使用头文件pthread.h,链接时也需要库libpthread.a,所以编译时需要给出-lpthread选项。<br />gcc -o thread thread.c -lpthread<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1054933/1636463572258-0edbdc11-c45d-4311-bec9-6f2094852c54.png#clientId=ub89e08c0-1aff-4&from=paste&height=315&id=u24a3d99b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=630&originWidth=980&originalType=binary&ratio=1&size=85458&status=done&style=none&taskId=uff9a9d65-df19-4e8e-8d17-fe8930960ba&width=490)
8.2 线程基本操作
8.2.1 线程创建
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1054933/1636463752364-ce8fdcf5-97e6-47bb-aaaf-564385c383e3.png#clientId=ub89e08c0-1aff-4&from=paste&height=350&id=u4de18bfc&margin=%5Bobject%20Object%5D&name=image.png&originHeight=699&originWidth=1073&originalType=binary&ratio=1&size=98410&status=done&style=none&taskId=u734927b6-dae2-4527-a17a-a708140c4a4&width=536.5)<br />start_routine:线程创建后调用的函数,也称线程函数的起始地址。
8.2.2 线程的退出与等待
线程结束方式有三种:正常结束;中途退出(pthread_exit);被其他线程强制退出(pthread_cancel)
//thread_create.c
//线程创建
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void *thread_fun();
int main(int argc, char **argv)
{
int rtn;
pthread_t thread_id;
rtn = pthread_create(&thread_id, NULL, &thread_fun, NULL);
if(rtn != 0)
{
perror("pthread_create error !");
exit(1);
}
sleep(1);//给创建的子进程执行时间1s,也是让主线程休息1s,等待新创建的线程结束后再退出;
//否则主线程返回或调用exit()退出,整个进程将会终止,进程中所有线程也会终止。
return 0;
}
void *thread_fun()
{
pthread_t new_thid;
new_thid = pthread_self();
printf("This is a new thread, thread ID is %u\n", new_thid);
printf("-----end-----\n");
}
线程可隐式退出(执行函数结束),也可显式调用pthread_exit();
void pthread_exit(void *value_ptr);
参数:指向返回状态的指针,可置为NULL。
线程内调用pthread_exit与exit的区别:
- pthread_exit结束当前主线程,进程不会结束,进程中的其他线程也不会终止
- exit结束当前进程,当前进程内的其他线程也被结束
有时一个线程为了等待某个线程执行结束,需要使用pthread_join挂起当前线程等待另一个线程结束。
默认线程退出后资源不主动释放,需要调用pthread_join等待并释放资源。
//thread_join.c
//线程等待
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void *thread_fun(void *ptr);
int main(int argc, char **argv)
{
int rtn1, rtn2;
pthread_t thread_id1;
pthread_t thread_id2;
char *message1 = "new_thread1";
char *message2 = "new_thread2";
rtn1 = pthread_create(&thread_id1, NULL, &thread_fun, (void*)message1);
if(rtn1 != 0)
{
perror("pthread_create error !");
exit(1);
}
rtn2 = pthread_create(&thread_id2, NULL, (void*)thread_fun, (void*)message2);
if(rtn2 != 0)
{
perror("pthread_create error !");
exit(1);
}
pthread_join(thread_id1, NULL);
pthread_join(thread_id2, NULL);
printf("thread1 return %d\n", rtn1);
printf("thread2 return %d\n", rtn2);
return 0;
}
void *thread_fun(void *ptr)
{
pthread_t new_thid;
char *message;
message = (char*)ptr;
new_thid = pthread_self();
printf("This is a new thread, thread ID is %u, message: %s\n", new_thid, message);
sleep(2);
printf("-----end-----\n");
}
虽然新创建的子线程sleep()休眠了一段时间,但在主线程中调用了pthread_join来等待新线程结束,所以主线程不会过早退出。
8.2.3 线程的取消
8.3 线程间通信
线程间共享进程的地址空间,因此线程间通信的难点在于对共享资源访问时的同步与互斥。线程间的同步互斥问题可以使用互斥锁、条件变量、信号量和读写锁来解决。
互斥锁使用前应先初始化,可使用函数pthread_mutex_init动态初始化,也可以使用静态初始化:
pthread_mutex_t mutex= PTHREAD_MUTEX_INITIALIZER;
//thread_mutex.c
//用互斥锁实现多线程的同步互斥
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int count = 0;
void *thread_fun();
int main(int argc, char **argv)
{
int rtn1, rtn2;
pthread_t thread_id1;
pthread_t thread_id2;
rtn1 = pthread_create(&thread_id1, NULL, &thread_fun, NULL);
rtn2 = pthread_create(&thread_id2, NULL, &thread_fun, NULL);
pthread_join(thread_id1, NULL);
pthread_join(thread_id2, NULL);
pthread_exit(0);
}
void *thread_fun()
{
pthread_mutex_lock(&mutex);
count++;
sleep(1);
printf("count = %d\n", count);
pthread_mutex_unlock(&mutex);
}
上述程序实现了对共享的全局变量count进行互斥访问。
当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞.
当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁.
通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁长期占用, 而等待的写模式锁请求长期阻塞.
读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁.