CLOVER🍀

That was when it all began.

WSGIアプリケーションとプロセス数、スレッド数の関係を見る

これは、なにをしたくて書いたもの?

先日、WSGIサーバーとして、GunicornやuWSGIを動かしてみました。

uWSGIを試してみる - CLOVER🍀

Gunicornを試してみる - CLOVER🍀

この時に、これらのサーバーには起動時にプロセス数やスレッド数を与えることができるとわかりました。

ところで、複数プロセスや複数スレッドにした時に、アプリケーションから見たらどういうことを気にした方が
よさそうなんでしょうかね?

WSGIから見る

まずは、仕様であるWSGIから見るのが筋でしょう。

幸い、日本語訳もあるようですので。

PEP 333: Python Web Server Gateway Interface v1.0 — knzm.readthedocs.org 2012-12-31 documentation

ちなみに、あまり重厚な仕様でもなさそうなので、割と読める感じなのではないかと。

ここで、「スレッドサポート」について見てみます。

スレッドサポート

Thread support, or lack thereof, is also server-dependent. Servers that can run multiple requests in parallel, should also provide the option of running an application in a single-threaded fashion, so that applications or frameworks that are not thread-safe may still be used with that server.

スレッドサポート、あるいはその欠如もまた、サーバ依存である。複数の要求 を並列に実行することができるサーバは、スレッド・セーフでないアプリケー ションまたはフレームワークを依然としてそのサーバと共に使用できるように、 シングルスレッド方式でアプリケーションを実行するオプションも提供すべき である (should)。

…はい。

というわけで、複数スレッドで実行するような場合はアプリケーション側も意識すべきだというように見えます。だいぶ
ふわっとした感じですね。

ちょっといくつか気になるパターンを、WSGIを実装したサーバーであるGunicornとuWSGIで試してみることにしましょう。

環境とインストール

今回の環境は、こちら。

$ python3 -V
Python 3.6.7

GunicornとPythonをインストール。

$ pip3 install gunicorn
$ pip3 install uwsgi

バージョン。

$ pip3 freeze
...
gunicorn==19.9.0
uWSGI==2.0.18
...

プロセス数やスレッド数を変更してみる

まずは、プロセス数やスレッド数を調整しながら変化を見ていこうと思います。

サンプルアプリケーションは、こちら。
wsgi-app.py

import os
import threading

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])

    pid = os.getpid()
    thread_name = threading.current_thread().getName()

    return ['get response / pid = {}, thread-name = {}'.format(pid, thread_name).encode()]

このスクリプトを実行するプロセスのIDや、スレッド名を返すようにしています。

こちらを使って、変化を見ていきましょう。

Gunicorn

最初は、Gunicorn。とりあえず、オプションなしでGunicornを起動します。

$ gunicorn wsgi-app:application
[2019-04-16 23:56:02 +0900] [26240] [INFO] Starting gunicorn 19.9.0
[2019-04-16 23:56:02 +0900] [26240] [INFO] Listening at: http://127.0.0.1:8000 (26240)
[2019-04-16 23:56:02 +0900] [26240] [INFO] Using worker: sync
[2019-04-16 23:56:02 +0900] [26245] [INFO] Booting worker with pid: 26245

シングルプロセス、1スレッドです。

アクセス。

$ curl localhost:8000
get response / pid = 26245, thread-name = MainThread

複数回アクセスしても、スレッドは切り替わりません。

$ for i in `seq 300`; do curl -s localhost:8000; echo ; done | sort -u
get response / pid = 26245, thread-name = MainThread

プロセス数4、スレッド数2にしてみます。

$ gunicorn --workers 4 --threads 2 wsgi-app:application
[2019-04-17 00:23:22 +0900] [753] [INFO] Starting gunicorn 19.9.0
[2019-04-17 00:23:22 +0900] [753] [INFO] Listening at: http://127.0.0.1:8000 (753)
[2019-04-17 00:23:22 +0900] [753] [INFO] Using worker: threads
[2019-04-17 00:23:22 +0900] [756] [INFO] Booting worker with pid: 756
[2019-04-17 00:23:22 +0900] [757] [INFO] Booting worker with pid: 757
[2019-04-17 00:23:22 +0900] [758] [INFO] Booting worker with pid: 758
[2019-04-17 00:23:22 +0900] [760] [INFO] Booting worker with pid: 760

スレッド名が「MainThread」ではなくなりました。

$ curl localhost:8000
get response / pid = 760, thread-name = ThreadPoolExecutor-0_0

複数回アクセスすると、プロセスが4つ、スレッドが2つ、確認することができます。プロセス間でスレッド名が同じなので
スレッドが分かれているかどうかわかりにくいですね…。

$ for i in `seq 300`; do curl -s localhost:8000; echo ; done | sort -u
get response / pid = 756, thread-name = ThreadPoolExecutor-0_0
get response / pid = 756, thread-name = ThreadPoolExecutor-0_1
get response / pid = 757, thread-name = ThreadPoolExecutor-0_0
get response / pid = 758, thread-name = ThreadPoolExecutor-0_0
get response / pid = 758, thread-name = ThreadPoolExecutor-0_1
get response / pid = 760, thread-name = ThreadPoolExecutor-0_0

psコマンドで確認してみましょう。

$ ps aux -L | grep gunicorn
xxxxx   753   753  0.3    1  0.1  75024 23832 pts/2    S+   00:23   0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application
xxxxx   756   756  0.1    3  0.1 230944 21444 pts/2    Sl+  00:23   0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application
xxxxx   756   845  0.0    3  0.1 230944 21444 pts/2    Sl+  00:23   0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application
xxxxx   756   846  0.0    3  0.1 230944 21444 pts/2    Sl+  00:23   0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application
xxxxx   757   757  0.2    3  0.1 230944 21452 pts/2    Sl+  00:23   0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application
xxxxx   757   793  0.2    3  0.1 230944 21452 pts/2    Sl+  00:23   0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application
xxxxx   757   794  0.0    3  0.1 230944 21452 pts/2    Sl+  00:23   0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application
xxxxx   758   758  0.2    3  0.1 230944 21456 pts/2    Sl+  00:23   0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application
xxxxx   758   799  0.0    3  0.1 230944 21456 pts/2    Sl+  00:23   0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application
xxxxx   758   800  0.2    3  0.1 230944 21456 pts/2    Sl+  00:23   0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application
xxxxx   760   760  0.2    3  0.1 230944 21472 pts/2    Sl+  00:23   0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application
xxxxx   760   777  0.1    3  0.1 230944 21472 pts/2    Sl+  00:23   0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application
xxxxx   760   778  0.0    3  0.1 230944 21472 pts/2    Sl+  00:23   0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application

むしろ、1プロセスあたり、もっとたくさんスレッドがいるようにも見えます…どうなってるんでしょう…。

今度は、4プロセス、4スレッドにしてみます。

$ gunicorn --workers 4 --threads 4 wsgi-app:application
[2019-04-17 00:26:39 +0900] [1567] [INFO] Starting gunicorn 19.9.0
[2019-04-17 00:26:39 +0900] [1567] [INFO] Listening at: http://127.0.0.1:8000 (1567)
[2019-04-17 00:26:39 +0900] [1567] [INFO] Using worker: threads
[2019-04-17 00:26:39 +0900] [1570] [INFO] Booting worker with pid: 1570
[2019-04-17 00:26:39 +0900] [1571] [INFO] Booting worker with pid: 1571
[2019-04-17 00:26:39 +0900] [1572] [INFO] Booting worker with pid: 1572
[2019-04-17 00:26:39 +0900] [1573] [INFO] Booting worker with pid: 1573

確認。

$ curl localhost:8000
get response / pid = 27520, thread-name = ThreadPoolExecutor-0_0

複数回アクセス。

$ for i in `seq 300`; do curl -s localhost:8000; echo ; done | sort -u
get response / pid = 1570, thread-name = ThreadPoolExecutor-0_0
get response / pid = 1570, thread-name = ThreadPoolExecutor-0_2
get response / pid = 1570, thread-name = ThreadPoolExecutor-0_3
get response / pid = 1571, thread-name = ThreadPoolExecutor-0_0
get response / pid = 1571, thread-name = ThreadPoolExecutor-0_1
get response / pid = 1571, thread-name = ThreadPoolExecutor-0_2
get response / pid = 1572, thread-name = ThreadPoolExecutor-0_0
get response / pid = 1572, thread-name = ThreadPoolExecutor-0_1
get response / pid = 1572, thread-name = ThreadPoolExecutor-0_2
get response / pid = 1572, thread-name = ThreadPoolExecutor-0_3
get response / pid = 1573, thread-name = ThreadPoolExecutor-0_0
get response / pid = 1573, thread-name = ThreadPoolExecutor-0_1
get response / pid = 1573, thread-name = ThreadPoolExecutor-0_2

psコマンドで見ると、やっぱりたくさんいます…。

$ ps aux -L | grep gunicorn
xxxxx  1567  1567  0.1    1  0.1  75024 23832 pts/2    S+   00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1570  1570  0.0    5  0.1 378408 21448 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1570  1635  0.0    5  0.1 378408 21448 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1570  1636  0.0    5  0.1 378408 21448 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1570  1687  0.0    5  0.1 378408 21448 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1570  1691  0.0    5  0.1 378408 21448 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1571  1571  0.1    5  0.1 378408 21456 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1571  1602  0.0    5  0.1 378408 21456 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1571  1603  0.0    5  0.1 378408 21456 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1571  1625  0.0    5  0.1 378408 21456 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1571  1626  0.0    5  0.1 378408 21456 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1572  1572  0.1    5  0.1 378408 21460 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1572  1607  0.0    5  0.1 378408 21460 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1572  1608  0.0    5  0.1 378408 21460 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1572  1611  0.0    5  0.1 378408 21460 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1572  1612  0.0    5  0.1 378408 21460 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1573  1573  0.1    5  0.1 378408 21476 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1573  1585  0.0    5  0.1 378408 21476 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1573  1586  0.0    5  0.1 378408 21476 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1573  1598  0.0    5  0.1 378408 21476 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
xxxxx  1573  1599  0.0    5  0.1 378408 21476 pts/2    Sl+  00:26   0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application

これ、ちゃんと確認した方がよさそうですね…。

https://github.com/benoitc/gunicorn/blob/19.9.0/gunicorn/workers/gthread.py#L101

uWSGI

続いて、uWSGIを見てみます。

デフォルトで起動。

$ uwsgi --http :8000 --wsgi-file wsgi-app.py

...
spawned uWSGI worker 1 (and the only) (pid: 28159, cores: 1)

1プロセス、1ワーカーです。

確認。

$ curl localhost:8000
get response / pid = 28159, thread-name = uWSGIWorker1Core0

独自のスレッドですね。

複数回アクセス。

$ for i in `seq 300`; do curl -s localhost:8000; echo ; done | sort -u
get response / pid = 28159, thread-name = uWSGIWorker1Core0

続いて、プロセス数4、スレッド数2にしてみます。

$ uwsgi --http :8000 --master --processes 4 --threads 2 --wsgi-file wsgi-app.py

...
spawned uWSGI master process (pid: 28788)
spawned uWSGI worker 1 (pid: 28789, cores: 2)
spawned uWSGI worker 2 (pid: 28790, cores: 2)
spawned uWSGI worker 3 (pid: 28791, cores: 2)
spawned uWSGI worker 4 (pid: 28792, cores: 2)
spawned uWSGI http 1 (pid: 28793)

1回のリクエストで確認。

$ curl localhost:8000
get response / pid = 28792, thread-name = uWSGIWorker4Core0

複数アクセスすると、1プロセスあたり、スレッドが2つありそうなことが確認できます。

$ for i in `seq 300`; do curl -s localhost:8000; echo ; done | sort -u
get response / pid = 28789, thread-name = uWSGIWorker1Core0
get response / pid = 28789, thread-name = uWSGIWorker1Core1
get response / pid = 28790, thread-name = uWSGIWorker2Core0
get response / pid = 28790, thread-name = uWSGIWorker2Core1
get response / pid = 28791, thread-name = uWSGIWorker3Core0
get response / pid = 28791, thread-name = uWSGIWorker3Core1
get response / pid = 28792, thread-name = uWSGIWorker4Core0
get response / pid = 28792, thread-name = uWSGIWorker4Core1

実際、psコマンドで見ても、ワーカープロセスあたり、2スレッドが子として作られていました。

プロセス数、スレッド数もともに4にしてみましょう。

$ uwsgi --http :8000 --master --processes 4 --threads 4 --wsgi-file wsgi-app.py

...
spawned uWSGI master process (pid: 29460)
spawned uWSGI worker 1 (pid: 29461, cores: 4)
spawned uWSGI worker 2 (pid: 29462, cores: 4)
spawned uWSGI worker 3 (pid: 29463, cores: 4)
spawned uWSGI worker 4 (pid: 29464, cores: 4)
spawned uWSGI http 1 (pid: 29465)

1リクエストで確認。

$ curl localhost:8000
get response / pid = 29461, thread-name = uWSGIWorker1Core0

複数回のリクエストで確認。

$ for i in `seq 300`; do curl -s localhost:8000; echo ; done | sort -u
get response / pid = 29461, thread-name = uWSGIWorker1Core0
get response / pid = 29461, thread-name = uWSGIWorker1Core1
get response / pid = 29461, thread-name = uWSGIWorker1Core2
get response / pid = 29461, thread-name = uWSGIWorker1Core3
get response / pid = 29462, thread-name = uWSGIWorker2Core0
get response / pid = 29462, thread-name = uWSGIWorker2Core1
get response / pid = 29462, thread-name = uWSGIWorker2Core2
get response / pid = 29462, thread-name = uWSGIWorker2Core3
get response / pid = 29463, thread-name = uWSGIWorker3Core0
get response / pid = 29463, thread-name = uWSGIWorker3Core1
get response / pid = 29463, thread-name = uWSGIWorker3Core2
get response / pid = 29463, thread-name = uWSGIWorker3Core3
get response / pid = 29464, thread-name = uWSGIWorker4Core0
get response / pid = 29464, thread-name = uWSGIWorker4Core1
get response / pid = 29464, thread-name = uWSGIWorker4Core2
get response / pid = 29464, thread-name = uWSGIWorker4Core3

やはり、4対4です。これはわかりやすいですね。

アプリケーションの初期化処理をどう考える?

ところで、WSGIの仕様はアプリケーションのエントリポイントが定義されているだけです。

Djangoのようなアプリケーションフレームワークでは、起動時に初期化処理とかあったのですが、どうなっているのでしょう?

DjangoWSGI向けのファイルを見てみます。
xxproject/xxproject/wsgi.py

"""
WSGI config for mysite project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'xxproject.settings')

application = get_wsgi_application()

「application」に関数の戻り値を格納しておしまいみたいです。

この中身を見てみます。

https://github.com/django/django/blob/2.2/django/core/wsgi.py

def get_wsgi_application():
    """
    The public interface to Django's WSGI support. Return a WSGI callable.
    Avoids making django.core.handlers.WSGIHandler a public API, in case the
    internal WSGI implementation changes or moves in the future.
    """
    django.setup(set_prefix=False)
    return WSGIHandler()

セットアップをして、別のクラスのインスタンスを返しているようです。

セットアップの中身は、こちら。

https://github.com/django/django/blob/2.2/django/__init__.py

実際のWSGIのエントリポイントとしては、こちらが機能します、と。

https://github.com/django/django/blob/2.2/django/core/handlers/wsgi.py#L130

つまり、委譲先の関数(として機能するもの)を返す前に、初期化処理を入れ込んでいるというわけですね。

これをマネて、こんなソースコードを用意しました。
wsgi-app-with-startup.py

import os
import threading

def startup():
    print('application startup!!')

    return Handler()

class Handler():
    def __call__(self, environ, start_response):
        start_response('200 OK', [('Content-Type', 'text/plain')])

        pid = os.getpid()
        thread_name = threading.current_thread().getName()

        return ['get response / pid = {}, thread-name = {}'.format(pid, thread_name).encode()]

application = startup()

実際の処理は、Handlerというクラスが行いますが、Handlerのインスタンスを返す前に初期化処理としてメッセージ出力を
行います。

こちらを、起動するプロセス数を変えたりしながら動作を確認してみましょう。

Gunicorn

まずは、Gunicorn。

$ gunicorn wsgi-app-with-startup:application
[2019-04-17 00:09:44 +0900] [30839] [INFO] Starting gunicorn 19.9.0
[2019-04-17 00:09:44 +0900] [30839] [INFO] Listening at: http://127.0.0.1:8000 (30839)
[2019-04-17 00:09:44 +0900] [30839] [INFO] Using worker: sync
[2019-04-17 00:09:44 +0900] [30842] [INFO] Booting worker with pid: 30842
application startup!!

初期化処理が呼ばれました。

アクセスしてみても、初期化処理はもう呼ばれません。

$ curl localhost:8000
get response / pid = 30842, thread-name = MainThread

次に、プロセス数を4、スレッド数を2にしてみます。

$ gunicorn --workers 4 --threads 2 wsgi-app-with-startup:application
[2019-04-17 00:10:09 +0900] [31224] [INFO] Starting gunicorn 19.9.0
[2019-04-17 00:10:09 +0900] [31224] [INFO] Listening at: http://127.0.0.1:8000 (31224)
[2019-04-17 00:10:09 +0900] [31224] [INFO] Using worker: threads
[2019-04-17 00:10:09 +0900] [31227] [INFO] Booting worker with pid: 31227
application startup!!
[2019-04-17 00:10:09 +0900] [31228] [INFO] Booting worker with pid: 31228
application startup!!
[2019-04-17 00:10:09 +0900] [31229] [INFO] Booting worker with pid: 31229
application startup!!
[2019-04-17 00:10:09 +0900] [31230] [INFO] Booting worker with pid: 31230
application startup!!

初期化処理が4回呼ばれました…。

アクセスしてみても、ここから初期化処理が再度呼ばれることはないのですが…。

$ curl localhost:8000
get response / pid = 31227, thread-name = ThreadPoolExecutor-0_0
uWSGI

続いて、uWSGI。

デフォルト状態で起動してみます。

$ uwsgi --http :8000 --wsgi-file wsgi-app-with-startup.py

...

*** Operational MODE: single process ***
application startup!!
WSGI app 0 (mountpoint='') ready in 0 seconds on interpreter 0x557b141ec140 pid: 31884 (default app)
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI worker 1 (and the only) (pid: 31884, cores: 1)

初期化処理は呼び出されましたね。

アクセスしても、初期化処理はもう呼び出されません。

$ curl localhost:8000
get response / pid = 31884, thread-name = uWSGIWorker1Core0

では、プロセス数を4、スレッド数を2にしてみます。

$ uwsgi --http :8000 --master --processes 4 --threads 2 --wsgi-file wsgi-app-with-startup.py

...

*** Operational MODE: preforking+threaded ***
application startup!!
WSGI app 0 (mountpoint='') ready in 0 seconds on interpreter 0x5612b14c5270 pid: 32534 (default app)
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI master process (pid: 32534)
spawned uWSGI worker 1 (pid: 32535, cores: 2)
spawned uWSGI worker 2 (pid: 32536, cores: 2)
spawned uWSGI worker 3 (pid: 32537, cores: 2)
spawned uWSGI worker 4 (pid: 32538, cores: 2)
spawned uWSGI http 1 (pid: 32539)

すると、uWSGIの場合は初期化処理が1回しか呼び出されませんでした。

アクセスしても、初期化処理は呼ばれたりしません。

$ curl localhost:8000
get response / pid = 32535, thread-name = uWSGIWorker1Core0

GunicornとuWSGIというWSGIサーバーでも、挙動の面でいろいろ違いがありそうですね。

こうなると、ひとつのアプリケーション内でインスタンスの数が制限することを前提にしているものとか、
アプリケーション内で同じインスタンスを見ていることが前提になっているものとかがあったら、
どうなるんだろうとかいろいろ気になります。

使う時には、気になるところをちゃんと確認した方が良さそうですね…。