回顾
上篇文章【Linux多线程编程】2.线程创建与回收 简单介绍了如何创建一个线程并且回收它,末尾给出了如下这段代码,本文将从这段代码入手介绍线程资源、线程共享资源、线程独占资源,并在最后引出多线程安全访问资源的方法。
/*
* test_pthread_worker.c
*/
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <signal.h> /* for sigaction */
#include <errno.h>
#include <unistd.h>
int total = 0; // 1
static void *producer_thread(void *UnusedArg) {
while (1) {
sleep(1); // 2
total += 5;
printf("%s produce 5 tools, now total %d\n", __func__, total);
}
return NULL;
}
static void *consumer_thread(void *UnusedArg) {
while (1) {
sleep(1); // 3
total -= 5;
printf("%s consume 5 tools, now total %d\n", __func__, total);
}
return NULL;
}
int main()
{
int rc;
pthread_t producer;
pthread_t consumer;
rc = pthread_create(&producer, NULL, producer_thread, NULL);
if (rc != 0) {
printf("Could not create producer_thread\n");
return -1;
}
rc = pthread_create(&consumer, NULL, consumer_thread, NULL);
if (rc != 0) {
printf("Could not create consumer_thread\n");
return -1;
}
pthread_join(producer, NULL);
pthread_join(consumer, NULL);
return 0;
}
线程资源
上文我们了解到,线程是运行在进程里的,通常由进程创建并负责回收。一个线程产生后是要去执行一段代码的,执行完毕后线程退出,被进程回收。
线程想要成功运行代码,必须获取一些资源,包括但不限于CPU资源、内存资源、硬件资源等。这些资源由线程所在的进程提供,从而多个线程运行在同一进程中则会争夺这些资源。
下面列出几种常见的线程间共享资源:
堆 :也就是代码中通过malloc/zalloc/calloc 分配的资源
全局变量:例如上例中的total
静态变量:用static声明的变量,包括静态全局变量以及静态局部变量,
打开的文件
线程内还有自己独占的资源,以下列出常见的线程独占资源
线程ID:上例中 pthread_t 类型的变量, 每个线程都有自己唯一的ID,用于区分不同的线程。
线程运行栈:可以理解为线程的执行函数
线程执行函数内声明的普通局部变量
所谓线程共享资源,描述为线程抢占资源更为贴切些,由于资源是共享的,所以线程间就会去争夺这些资源。如果不加一些限制,则多线程并发访问共享资源,极容易引发各种数据问题,甚至造成死锁,阻塞整个进程。
例如在上述代码中,我们的本意是——创建一个生产者,创建一个消费者,生产者不断生产货品,每1s产生5个货品(total),然后将货品放到加工台,由另一名工人(消费者)拿到这些货品进行处理,消费者每拿到5个货品处理的时间为1s,处理完后再次去加工台拿取新的一批货品。
设想下,如果上图中棕白的小兔子压面饼的速度过快或者过慢会产生什么情况?
很明显,如果速度过快,白色扯面团的小兔子还没有产出新的面团,棕白小兔子就压面饼,从而产生“空的压面饼动作”,换到程序里就是 total 变为了负值;
如果速度过慢,棕色拿剪刀的小兔子就会剪出多余的馅料,但没有新的压好的面饼产生,造成馅料的浪费,想想生产线上都是一坨坨馅料,这不是生产事故了吗?
当然,兔兔并不是机器,在没有面饼的时候可以停下来等待,但程序是机器,设定好既定的操作后就会一直运行,有没有办法让程序拥有兔兔的脑袋等一等呢?这就是下一篇文章我们将介绍的线程同步机制——锁。
但本文中,我们仍然就多线程资源访问不合理造成的问题展开讨论,加深对多线程资源的理解,也就是本文最开始展示的那部分有问题的生产者消费者代码。
代码解释
上例代码中三个比较重要的地方我用注释标注了出来
int total = 0; // 1
static void *producer_thread(void *UnusedArg) {
while (1) {
sleep(1); // 2
}
//...
}
static void *consumer_thread(void *UnusedArg) {
while (1) {
sleep(1); // 3
}
//...
}
main
函数中我们依次创建了producer
和consumer
两个线程,然后各自执行producer_thread/consumer_thread
两个函数,函数体基本都是一个死循环,然后每隔1s对全局变量total
的值+5/-5
。
这里total
就是一个线程共享资源,producer
和consumer
会去竞争这个资源,谁拿到谁操作,而对total
的加/减操作从操作系统底层看可能是如下的伪代码(涉及到寄存器相关知识,这里不做展开,读者只需要知道对变量的加减操作在内核层面并不是“原子的”,而是多个步骤组成,如果在中间某个步骤,其他的执行者切入修改了数据,最终会导致得到的结果不符合预期。所谓原子,指一系列操作要么全部成功,要么全部失败。)
// total +
tmp = total; // 先存储 total 的值
total = tmp + 5; // 再增加 total 的值
// total -
tmp = total;
total = tmp - 5;
现在代入两个线程,再来看上述步骤是否安全。
设某时刻 total = 15
时刻1:producer
执行 total += 5
,在内核层面,实际上理解为tmp = total; total = total + 5
两个步骤。
时刻2:producer
刚执行完tmp=total;
;consumer
开始执行total -= 5
,并且将 total -= 5
执行完毕
时刻3:此时 total = 10
,producer
继续执行剩下的total = tmp + 5
,tmp依旧是producer保存的15
,执行完毕
时刻4:此时producer
和consumer
的加减操作都执行完毕,total = 20
上述过程中,total初始值为15,producer和consumer各运转一次,也就是预期最终值total=15;
但是确由于非原子性的操作,造成数据混乱,最终total=20。
上述只是介绍了线程对于共享资源处理的其中一种可能导致的结果,更多的读者可以自行发散。
现在回到我们的代码中,运行后会发现出现了 total 值为负数的情况,根据上述的理论也解释的通了。
有没有办法能解决多线程并发访问共享资源带来的上述问题吗?
有!
这就是我们下节要介绍的线程锁。具体内容在下节展开。