使用do{}while(0);语句。由于受到教科书的影响(不要用会破坏程序结构的goto语句)

 

4.1 并发

  这几年并发技术受到前所未有的关注:CPU进入多核时代,连手机芯片都使用三核的CPU了(AP、BP和DSP都集成到一

颗芯片上)。天生具有并发能力的语言Erlang逐渐成为热点。网格和云计算开始进入实用阶段。还有一些新技术更是让

我闻所未闻,初学者也大可不必被这些铺天盖地的新名词吓倒。根据我的经验来看,这些技术或许能够改变产业的格局

,对人类生活造成重大影响,但从实现角度来看并无多少革新,相反,大部分都是传统技术的继承和改进。这几年我一

直在研究开源的基础软件,实际上我没有发现多少“新”东西或者核心技术。要说真正的核心技术还是如序言中说的:

战胜复杂度和应对变化。

  作为系统程序员,掌握基础理论和经典的设计方法,比去追逐一些所谓的新技术要实用得多,基础打扎实了,学习

新知识也是很容易的事。在接下来几节中,我们一起来学习传统的并发编程知识。在这里我们请读者完成下列任务。

  了解Linux下多线程编程的基本方法,以双向链表为载体实现传统的生产者—消费者模型:用一个线程往双向链表中

添加元素,用另一个线程从这里面取出。

  Linux下的多线程编程使用pthread(POSIX Thread)函数库,使用时需要包含头文件pthread.h,还要链接共享库

libpthread.so。这里顺便说一下gcc链接共享库的方式:-L用来指定共享库所在目录,不用指定系统库目录;-l用来指

定要链接的共享库,只需要指定库的名字就行了,如应该是-lpthread而不是 -llibpthread.so。这样做看起来有点怪,

其原因是共享库通常带有版本号,指定全文件名就意味着你要绑定到特定版本的共享库上,而只指定名字则在可以运行

时通过环境变量来选择要使用的共享库,这样能够给软件升级带来方便。

  pthread函数库的使用相对比较简单,读者可以在终端下运行man pthread_create来阅读相关函数的手册,也可以到

网上找些例子参考。具体使用方法我们就不讲了,这里介绍一下初学者常犯的错误。

  用临时变量作为线程参数的问题

  #include

  #include

  #include

  void* start_routine(void* param)

  {

  char* str = (char*)param;

  printf("%s:%s\n", __func__, str);

  return NULL;

  }

  pthread_t create_test_thread()

  {

  pthread_t id = 0;

  char str[] = "it is ok!";

  pthread_create(&id, NULL, start_routine, str);

  return id;

  }

  int main(int argc, char* argv[])

  {

  void* ret = NULL;

  pthread_t id = create_test_thread();

  pthread_join(id, &ret);

  return 0;

  }

  分析 由于新线程和当前线程是并发的,因此无法预测谁先谁后。可能create_test_thread 已经执行完了,str也已

经被释放了,新线程才得到这参数,此时它的内容已经无法确定了,打印出的字符串自然是随机的。

  线程参数共享的问题

  #include

  #include

  #include

  void* start_routine(void* param)

  {

  int index = *(int*)param;

  printf("%s:%d\n", __func__, index);

  return NULL;

  }

  #define THREADS_NR 10

  void create_test_threads()

  {

  int i = 0;

  void* ret = NULL;

  pthread_t ids[THREADS_NR] = {0};

  for(i = 0; i < THREADS_NR; i++)

  {

  pthread_create(ids + i, NULL, start_routine, &i);

  }

  for(i = 0; i < THREADS_NR; i++)

 {

  pthread_join(ids[i], &ret);

  }

  return ;

  }

  int main(int argc, char* argv[])

  {

  create_test_threads();

  return 0;

  }

  分析 由于新线程和当前线程是并发的,因此无法预测谁先谁后。i在不断变化,所以新线程得到的参数值是无法预

知的,打印出的字符串自然也是随机的。

  虚假并发

  #include

  #include

  #include

  void* start_routine(void* param)

  {

  int index = *(int*)param;

  printf("%s:%d\n", __func__, index);

  return NULL;

  }

  #define THREADS_NR 10

  void create_test_threads()

  {

  int i = 0;

  void* ret = NULL;

  pthread_t ids[THREADS_NR] = {0};

  for(i = 0; i < THREADS_NR; i++)

  {

  pthread_create(ids + i, NULL, start_routine, &i);

  pthread_join(ids[i], &ret);

  }

  return ;

  }

  int main(int argc, char* argv[])

  {

  create_test_threads();

  return 0;

  }

  分析 因为pthread_join会阻塞其他线程直到当前线程退出,所以这些线程实际上是串行执行的,一个退出了,才创

建下一个。当年一个同事写了一个这样的多线程测试程序,结果就是没有测试出一个潜伏的问题,直到产品运行时,这

个问题才暴露出来。

  访问线程共享的数据时要加锁,让访问串行化,否则就会出问题。比如,当你正在访问双向链表的某个结点时,它

可能已经被另外一个线程删掉了。加锁的方式有很多种,像互斥锁(mutex= mutual exclusive lock)、信号量

(semaphore)和自旋锁(spin lock)等都是常用的,它们的使用同样很简单,我们就不多说了。

  在加锁/解锁时,初学者常犯两类错误。

  一类错误是存在没有解锁的路径。初学者常见的做法就是,进入某个临界函数时加锁,在函数结尾的地方解锁,我

甚至见过这种写法。

  {

  /*这里加锁*/

  ...

  return...;

  /*这里解锁*/

  }

  如果你也犯了这种错误,那么请好好反思一下。有时候,return的地方太多,在某一处忘记解锁是可能的,就像内

存泄露一样,只是忘记解锁的后果更严重。像下面这个例子。

  Ret dlist_insert(DList* thiz, size_t index, void* data)

  {

  DListNode* node = NULL;

 DListNode* cursor = NULL;

  return_val_if_fail(thiz != NULL, RET_INVALID_PARAMS);

  dlist_lock(thiz);

  if((node = dlist_create_node(thiz, data)) == NULL)

  {

  dlist_unlock(thiz);

  return RET_OOM;

  }

  if(thiz->first == NULL)

  {

  thiz->first = node;

  dlist_unlock(thiz);

  return RET_OK;

  }

  ...

  dlist_unlock(thiz);

  return RET_OK;

  }

  如果一个函数有五六个甚至更多的地方需要返回,那么遗忘一两处也是很常见的,即使没有忘记,在每个返回的地

方都要去解锁和释放相关资源也是很麻烦的。在这种情况下,我们最好是实现单入口单出口的函数,常见的做法有以下

两种。

  (1) 使用goto语句。在Linux内核里大量使用了该方式。示例如下。

  Ret dlist_insert(DList* thiz, size_t index, void* data)

  {

  Ret ret = RET_OK;

  DListNode* node = NULL;

  DListNode* cursor = NULL;

  return_val_if_fail(thiz != NULL, RET_INVALID_PARAMS);

  dlist_lock(thiz);

  if((node = dlist_create_node(thiz, data)) == NULL)

  {

  ret = RET_OOM;

  goto done;

  }

  if(thiz->first == NULL)

  {

  thiz->first = node;

  goto done;

  }

  ...

  done:

  dlist_unlock(thiz);

  return ret;

  }

  (2) 使用do{}while(0);语句。由于受到教科书的影响(不要用会破坏程序结构的goto语句),我习惯了这种做法。

示例如下。

  Ret dlist_insert(DList* thiz, size_t index, void* data)

  {

  Ret ret = RET_OK;

  DListNode* node = NULL;

  DListNode* cursor = NULL;

  return_val_if_fail(thiz != NULL, RET_INVALID_PARAMS);

  dlist_lock(thiz);

  do

  {

  if((node = dlist_create_node(thiz, data)) == NULL)

  {

  ret = RET_OOM;

  break;

  }

  if(thiz->first == NULL)

  {

  thiz->first = node;

  break;

  }

  ...

  }while(0);

  dlist_unlock(thiz);

  return ret;

  }

  另一类错误是加锁顺序的问题。有时候为了提高效率,常常降低加锁的粒度,访问时不是用一个锁锁住整个数据结

构,而是用多个锁来控制数据结构的各个部分。这样一来,当一个线程访问数据结构的某个部分时,另外一个线程还可

以访问数据结构的其他部分。但是在有的情况下,你需要同时锁几个锁,这时就要注意了:所有线程一定要按相同的顺

序加锁,按相反的顺序解锁。否则就可能出现死锁,即两个线程都拿着对方需要的锁,却又互相等待对方先释放锁的情

况。更多详细知识http://www.cnblogs.com/ewialitwind/

posted @ 2012-05-22 15:01  程序英才  阅读(168)  评论(0)    收藏  举报