难道每当想要同时做几件事时都得创建进程吗?
不见得,
有以下几个原因:
1.创建进程要花时间
有的机器新建进程只要花一丁点时间。虽然时间很短,但还是需要时间。
如果你想要执行的任务才用几十毫秒,每次都创建进程就很低效。
2.共享数据不方便
当创建子进程时,子进程会自动包含父进程所有数据的副本。但这些只是
副本,如果子进程想把数据发回父进程,就需要借助管道之类的东西。
3.进程真的很难
创建进程需要写很多代码,这会让代码又乱又长。
你需要线程
普通进程一次只做一件事
到目前为止,你写过的所有程序都是单线程,这就好比
进程中只有一个人在干活。
多雇几名员工:使用线程
你可以雇多名员工在店里干活,同样,也可以在一个进程
中使用多个线程。所有线程能访问同一段堆存储器,读
写同一个文件,使用同一个网络套接字进行通信。当一个
线程修改了某个全局变量,其他线程马上就能看到。
也就是说,可以为每个线程都分配一个独立的任务,让这
些线程同时执行。
如何创建线程
你可以使用很多线程库,这里我们将使用最流行的一
种:POSIX线程库,也叫 pthread 。可以在Cygwin、Linux
和Mac上使用 pthread 。
假设你想在独立的线程中运行这个函数:
void* does_not(void *a)
{
int i = 0;
for (i = 0; i < 5; i++) {
sleep(1);
puts("Does not!");
}
return NULL;
}
注意:void指针可以指向存储器中任何类型的数据,线程函
数的返回类型必须是 void* !
用pthread_create创建线程
每个线程都需
要把信息保存在一个叫 pthread_t 的数据结构中,然后就可以用
pthread_create() 创建并运行线程。
#include <pthread.h>
- ...
pthread_t t0; //它保存了线程的所有信息
pthread_create(&t0, NULL, does_not, NULL);
代码将以独立线程运行这两个函数。还没完,如果程序运行完这段代
码就结束了,线程也会随之灭亡,因此必须等待线程结束:
// 函数返回的void指针会保存在这里
void* result;
//pthread_join()函数会等待线程结束。
pthread_join(t0,&result);
pthread_join() 会接收线程函数的返回值,并把它保存在一个
void 指针变量中。一旦两个线程都结束了,程序就可以顺利退出了。
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
// 打印错误的函数
void error(char *msg) {
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(1);
}
void* does_not(void *a) {
int i = 0;
for (i = 0; i < 5; i++) {
sleep(1);
puts("Does not!");
}
return NULL;
}
void* does_too(void *a) {
int i = 0;
for (i = 0; i < 5; i++) {
sleep(1);
puts("Does too!");
}
return NULL;
}
int main() {
pthread_t t0; //它保存了线程的所有信息
pthread_t t1;
// 创建线程
if (pthread_create(&t0, NULL, does_not, NULL) == -1) //does_not是线程将运行的函数名
error("无法创建线程t0"); //每次都应该检查错误。
if (pthread_create(&t1, NULL, does_too, NULL) == -1) //does_not是线程将运行的函数名
error("无法创建线程t1"); //每次都应该检查错误。
// 函数返回的void指针会保存在这里
void* result;
//pthread_join()函数会等待线程结束。
if(pthread_join(t0,&result)==-1)
error("无法回收线程t0");
if(pthread_join(t1,&result)==-1)
error("无法回收线程t1");
return 0;
}
运行结果:
如果不能编译,可以在编译时使用 pthread 库:
命令:gcc dchapter12.c -lpthread -o dchapter12
练习:
派对开始了,倒计数啤酒瓶数。下面这段代码运行了20个线程,总共
有200万瓶啤酒。看看你能否找到丢失的代码,搞定以后干杯庆祝一
下。
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
// 打印错误的函数
void error(char *msg) {
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(1);
}
int beers = 2000000;
void* drink_lots(void* param) {
int i;
for (i = 0; i < 100000; i++) {
beers = beers - 1;
}
return NULL;
}
int main() {
pthread_t threads[20];
int t;
printf("%i bottles of beer on the wall\n%i bottles of beer\n", beers,
beers);
// 创建20个线程来运行该函数
for (t = 0; t < 20; t++) {
// if (pthread_create(&threads[t], NULL, drink_lots, (void*)thread_no) == -1)
if (pthread_create(&threads[t], NULL, drink_lots, NULL) == -1)
error("无法创建线程");
}
// 等待所有线程结束
void* result;
for (t = 0; t < 20; t++) {
if (pthread_join(threads[t], &result) == -1)
error("无法回收线程");
}
printf("There are now %i bottles of beer on the wall\n", beers);
return 0;
}
运行结果:
结果显示,大多数情况下,代码中两个线程并没有把beers变量减为0!---原因就是:
多个线程同时访问同一个数据,就会导致线程安全问题,数据就会出错!
增设红绿灯---互斥锁
假设两辆车想要驶过一段羊肠小道。为了防止交通事故,
你可以增设红绿灯,它可以防止两辆车同时访问共享资
源。
如果想防止两个或多个线程访问共享数据资源,也可以
采取相同的方法:增设红绿灯。这样两个线程就不能同
时读取相同数据,并把它写回。
用来防止线程发生车祸的
红绿灯就叫互斥锁,它们是把
代码变为线程安全最简单的方法。
用互斥锁来管理交通
// 创建互斥锁
pthread_mutex_t a_lock = PTHREAD_MUTEX_INITIALIZER;
互斥锁必须对所有可能发生冲突的线程可见,也就是说它
是一个全局变量。
PTHREAD_MUTEX_INITIALIZER 实际上是一个
宏,当编
译器看到它,就会插入创建互斥锁的代码。
pthread_mutex_lock(&a_lock);
/* 含有共享数据的代码从这里开始 */
....这里的代码是线程安全的
/* ... 代码结束了 */
pthread_mutex_unlock(&a_lock);
线程函数可以接收一个void指针作为参数,并返回一个void指针
值。通常你希望把某个整型值传给线程,并让它返回某个整型值,
一种方法是用 long ,因为它的大小和void指针相同,可以把它保
存在void指针变量中。
void* do_stuff(void* param)
{
long thread_no = (long)param; // 线程编号
printf("Thread number %ld\n", thread_no);
return (void*)(thread_no + 1);
}
用互斥锁来修改上面的beers.c代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
// 打印错误的函数
void error(char *msg) {
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(1);
}
// 创建互斥锁
pthread_mutex_t a_lock = PTHREAD_MUTEX_INITIALIZER;
int beers = 2000000;
void* drink_lots(void* param) {
// 线程函数可以接收一个void指针类型的参数,这里将参数转化为long类型
long thread_no = (long) param;
int i;
for (i = 0; i < 100000; i++) {
pthread_mutex_lock(&a_lock);
/* 含有共享数据的代码从这里开始 */
beers = beers - 1;
/* ... 同步代码结束了 */
pthread_mutex_unlock(&a_lock);
// 现在线程安全了
}
// 打印线程号
// printf("Thread number %ld :", thread_no);
printf("beers = %i\n", beers);
return NULL;
// 返回时将其类型转化为void指针
// return (void*)(thread_no + 1);
}
int main() {
pthread_t threads[20];
printf("%i bottles of beer on the wall\n%i bottles of beer\n", beers,
beers);
// 创建20个线程来运行该函数
int t;
for (t = 0; t < 20; t++) {
// if (pthread_create(&threads[t], NULL, drink_lots, NULL) == -1)
if (pthread_create(&threads[t], NULL, drink_lots, (void*) t) == -1)
error("无法创建线程");
}
// 等待所有线程结束
void* result;
for (t = 0; t < 20; t++) {
if (pthread_join(threads[t], &result) == -1)
error("无法回收线程");
// printf("Thread %ld returned %ld\n", t, (long)result);
}
printf("There are now %i bottles of beer on the wall\n", beers);
return 0;
}
运行结果:
可以看到,这下线程安全了!
问: 怎样设计高效的多线程程序?
答: 减少线程需要访问的共享
数据的数量。如果线程无需访问很多
共享数据,那么多个线程等一个线
程的情况就很少出现,速度会大大
提高。
问: 线程要比进程快?
答: 通常是这样,因为创建进
程要比创建线程花更多时间。
问: 听说互斥锁会引发“死锁”,那是什么玩意儿?
答: 假设你有两个线程,它们
都想得到互斥锁A和B。倘若第一个
线程得到了A,第二个线程得到了B,
这两个线程就会陷入死锁。因为第一
个线程无法得到B,第二个线程无法
得到A,它俩都停滞不前。