ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

Linuxでスレッドごとに固有のIDを発行できないか

なるべく簡単なロジックで、おおむね被らないようなIDをスレッドごとに吐けないだろうか、それも同じスレッドなら何度呼び出しても同じIDになるやつ(=キャッシュが要らない)、と思ってこういうのを考えた。

とりあえずコード:

#define _GNU_SOURCE                                      
#include <stdio.h>               
#include <stdlib.h>
#include <unistd.h>       
#include <sys/syscall.h>       
                               
pid_t gettid(void) {                                    
  return syscall(SYS_gettid);                                                                                         
}                                              
                                                 
unsigned long long getthreadseq(void) {              
  pid_t tid = gettid();                              
  char fmt[] = "/proc/self/task/%d/stat";
  char statpath[sizeof(fmt)+10];
  unsigned long long starttime;         
                                          
  (void)snprintf(statpath, sizeof(fmt)+10, fmt, tid);
  FILE *fp = fopen(statpath, "r");                       
  int _nulld;
  unsigned _nullu;
  unsigned long _nulllu;
  long _nullld;
  char _nullc;
  char _nulls[65536];
  int r = fscanf(fp, "%d %s %c %d %d " "%d %d %d %u %lu " "%lu %lu %lu %lu %lu " "%ld %ld %ld %ld %ld " "%ld %llu %s",
    &_nulld, _nulls, &_nullc, &_nulld, &_nulld,
    &_nulld, &_nulld, &_nulld, &_nullu, &_nulllu,
    &_nulllu, &_nulllu, &_nulllu, &_nulllu, &_nulllu,
    &_nullld, &_nullld, &_nullld, &_nullld, &_nullld,
    &_nullld, &starttime, _nulls
  );
  fclose(fp);               
  if(r < 0) { perror("fscanf"); exit(1); }
                
  // printf("tid = %d, starttime = %llu\n", tid, starttime);
  return (starttime << 18) | tid;
}
                          
#include <pthread.h>           
void* thread_main(void* args) {
  unsigned long long id = getthreadseq();
  printf("seq = %llu\n", id);

  sleep(3);
  unsigned long long id2 = getthreadseq();
  printf("seq(again) = %llu | old = %llu\n", id2, id);
  return NULL;
}                              
                                
int main(int argc, char *argv[]) {
  int thcount = 10;
  if(argc > 1) {
    thcount = atol(argv[1]);            
    if(thcount <= 0) {          
      exit(127);
    }
  }

  pthread_t tids[thcount];
  for(int i=0; i<thcount; ++i){
    pthread_t t;
    pthread_create(&t, NULL, thread_main, (void *)NULL);
    tids[i] = t;
  }
  for(int i=0; i<thcount; ++i){
    pthread_join(tids[i], NULL);
  }
  return 0;
}

動かす

cc threadseq.c   -o threadseq -lpthread
$ ./threadseq 
seq = 280515858282
seq = 280515858284
seq = 280515858283
seq = 280515858281
seq = 280515858285
seq = 280515858287
seq = 280515858286
seq = 280515858288
seq = 280515858289
seq = 280515858290
seq(again) = 280515858284 | old = 280515858284
seq(again) = 280515858287 | old = 280515858287
seq(again) = 280515858282 | old = 280515858282
seq(again) = 280515858281 | old = 280515858281
seq(again) = 280515858285 | old = 280515858285
seq(again) = 280515858283 | old = 280515858283
seq(again) = 280515858289 | old = 280515858289
seq(again) = 280515858288 | old = 280515858288
seq(again) = 280515858286 | old = 280515858286
seq(again) = 280515858290 | old = 280515858290

Uniqueかどうか

$ ./threadseq 10000 | grep -v again | sort | uniq | wc -l
10000

やったこと

  • Thread ID を取得する
    • gettid(2) syscall で取る、glibcがラッパー関数を作っていないので syscall(SYS_gettid) で自分で定義する
  • /proc/self/task/tid/stat の 22 番目の項目が「starttime」という値で、起動時間により変わる何かなのでそれも取得する
    • Man page of PROC
    • システムが起動した時刻からのtick回数になる
  • Thread ID とstarttimeを用いてunsigned long longの値を生成する
    • 雑には (starttime << 18) | tid
    • Thread ID はいつか一周するが、同じstarttimeで同じThread IDになるスレッドが生成されることが極めて稀であることを利用する
    • 18 という左シフトは pid_max で変えると良いと思う

実際どうか

ほぼ同時に50000とか100000スレッド作っても被らないIDが出せているように見える。理屈の上では、同じ starttime の間に pid_max 個以上のスレッドができるとかぶるので、たぶん、 kernel.pid_max の値が大きい方が都合が良さそう(実験した環境は pid_max = 200000)。

$ ./threadseq 50000 | grep -v again | sort | uniq -c | head
      1 seq = 293374145140
      1 seq = 293374145141
      1 seq = 293374145142
      1 seq = 293374145143
      1 seq = 293374145144
      1 seq = 293374145145
      1 seq = 293374145146
      1 seq = 293374145147
      1 seq = 293374145148
      1 seq = 293374145149
$ ./threadseq 100000 | grep -v again | sort | uniq -c | head
      1 seq = 293718128074
      1 seq = 293718128075
      1 seq = 293718128076
      1 seq = 293718128077
      1 seq = 293718390222
      1 seq = 293718390223
      1 seq = 293718390224
      1 seq = 293718390225
      1 seq = 293718390226
      1 seq = 293718390227

ユニークなIDを発行する手段はいくらでもあるのだが、これについてはそのスレッドなら いつ呼んでも同じ値になる 、という便利な性質がある。パフォーマンス計測時のトレース時のIDとか、最悪被っても影響が小さいような何かに使えるかもしれない。

あと、ジャストアイデアなので考え方がアマ〜いという点があれば是非教えてください。